├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ ├── contracts-docs.yml │ ├── contracts.yml │ ├── format.yml │ └── npm.yml ├── .gitignore ├── .openzeppelin └── mainnet.json ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.js ├── .solhint.json ├── .solhintignore ├── CODEOWNERS ├── README.adoc ├── SECURITY.md ├── config_tenderly.sh ├── contracts ├── governance │ ├── BaseTokenholderGovernor.sol │ ├── Checkpoints.sol │ ├── GovernorParameters.sol │ ├── IVotesHistory.sol │ ├── ProxyAdminWithDeputy.sol │ ├── StakerGovernor.sol │ ├── StakerGovernorVotes.sol │ ├── TokenholderGovernor.sol │ └── TokenholderGovernorVotes.sol ├── staking │ ├── IApplication.sol │ ├── IApplicationWithDecreaseDelay.sol │ ├── IApplicationWithOperator.sol │ ├── IStaking.sol │ └── TokenStaking.sol ├── test │ ├── SimplePREApplicationStub.sol │ ├── TestGovernorTestSet.sol │ ├── TestStakerGovernorVotes.sol │ ├── TestStakingCheckpoints.sol │ ├── TestToken.sol │ ├── TestTokenholderGovernorVotes.sol │ ├── TokenStakingTestSet.sol │ └── UpgradesTestSet.sol ├── token │ └── T.sol ├── utils │ └── SafeTUpgradeable.sol └── vending │ └── VendingMachine.sol ├── deploy ├── 00_resolve_nucypher_pre_application.ts ├── 00_resolve_nucypher_token.ts ├── 01_deploy_t.ts ├── 02_mint_t.ts ├── 04_deploy_vending_machine_nu.ts ├── 05_transfer_t.ts ├── 07_deploy_token_staking.ts ├── 30_deploy_tokenholder_timelock.ts ├── 31_deploy_tokenholder_governor.ts ├── 32_configure_tokenholder_timelock.ts ├── 51_transfer_ownership_t.ts ├── 52_transfer_ownership_token_staking.ts └── 53_transfer_upgradeability_token_staking.ts ├── deployments └── mainnet │ ├── .chainId │ ├── NuCypherToken.json │ ├── T.json │ ├── TokenStaking.json │ ├── TokenholderGovernor.json │ ├── TokenholderTimelock.json │ ├── VendingMachineNuCypher.json │ └── solcInputs │ ├── 9a2f45de388a8a02f9e065c8da7e4649.json │ └── ecfd0a8dc1d11e9f659c5fce34a455a2.json ├── docgen-templates └── common.hbs ├── docs ├── rfc-1-staking-contract.adoc └── rfc-2-vending-machine.adoc ├── external └── mainnet │ ├── KeepToken.json │ ├── KeepTokenStaking.json │ ├── NuCypherStakingEscrow.json │ └── NuCypherToken.json ├── hardhat.config.ts ├── package.json ├── slither.config.json ├── test ├── governance │ ├── GovernorParameters.test.js │ ├── ProxyAdminWithDeputy.test.js │ ├── StakerGovernor.test.js │ ├── StakerGovernorVotes.test.js │ ├── TokenholderGovernor.test.js │ └── TokenholderGovernorVotes.test.js ├── helpers │ └── contract-test-helpers.js ├── staking │ └── TokenStaking.test.js ├── token │ └── T.test.js └── vending │ └── VendingMachine.test.js ├── tsconfig.export.json ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | export/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-keep"], 3 | "parserOptions": { 4 | "ecmaVersion": 2017, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "es6": true, 9 | "mocha": true 10 | }, 11 | "rules": { 12 | "new-cap": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.github/workflows/contracts-docs.yml: -------------------------------------------------------------------------------- 1 | name: Solidity docs 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - releases/mainnet/v** 8 | release: 9 | types: 10 | - "published" 11 | workflow_dispatch: 12 | 13 | jobs: 14 | docs-detect-changes: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | path-filter: ${{ steps.filter.outputs.path-filter }} 18 | steps: 19 | - uses: actions/checkout@v3 20 | if: github.event_name == 'pull_request' 21 | - uses: dorny/paths-filter@v2 22 | if: github.event_name == 'pull_request' 23 | id: filter 24 | with: 25 | filters: | 26 | path-filter: 27 | - './contracts/**' 28 | - './.github/workflows/contracts-docs.yml' 29 | 30 | # This job will be triggered for PRs which modify contracts. It will generate 31 | # the archive with contracts documentation in Markdown and attatch it to the 32 | # workflow run results. Link to the archive will be posted in a PR comment. 33 | # The job will also be run after manual triggering and after pushes to the 34 | # `releases/mainnet/**` branches. 35 | contracts-docs-publish-preview: 36 | name: Publish preview of contracts documentation 37 | needs: docs-detect-changes 38 | if: | 39 | needs.docs-detect-changes.outputs.path-filter == 'true' 40 | || github.event_name == 'push' 41 | || github.event_name == 'workflow_dispatch' 42 | uses: keep-network/ci/.github/workflows/reusable-solidity-docs.yml@main 43 | with: 44 | publish: false 45 | addTOC: false 46 | commentPR: false 47 | exportAsGHArtifacts: true 48 | 49 | # This job will be triggered for releases which name starts with 50 | # `refs/tags/v`. It will generate contracts documentation in 51 | # Markdown and sync it with a specific path of 52 | # `threshold-network/threshold` repository. If changes will be detected, 53 | # a PR updating the docs will be created in the destination repository. The 54 | # commit pushing the changes will be verified using GPG key. 55 | contracts-docs-publish: 56 | name: Publish contracts documentation 57 | needs: docs-detect-changes 58 | if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') 59 | uses: keep-network/ci/.github/workflows/reusable-solidity-docs.yml@main 60 | with: 61 | publish: true 62 | addTOC: false 63 | verifyCommits: true 64 | destinationRepo: threshold-network/threshold 65 | destinationFolder: ./docs/app-development/staking-contract-and-dao/staking-contract-and-dao-api 66 | destinationBaseBranch: main 67 | userEmail: 38324465+thesis-valkyrie@users.noreply.github.com 68 | userName: Valkyrie 69 | rsyncDelete: true 70 | secrets: 71 | githubToken: ${{ secrets.THRESHOLD_DOCS_GITHUB_TOKEN }} 72 | gpgPrivateKey: ${{ secrets.THRESHOLD_DOCS_GPG_PRIVATE_KEY_BASE64 }} 73 | gpgPassphrase: ${{ secrets.THRESHOLD_DOCS_GPG_PASSPHRASE }} 74 | -------------------------------------------------------------------------------- /.github/workflows/contracts.yml: -------------------------------------------------------------------------------- 1 | name: Solidity 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | # We intend to use `workflow dispatch` in two different situations/paths: 11 | # 1. If a workflow will be manually dspatched from branch named 12 | # `dapp-development`, workflow will deploy the contracts on the selected 13 | # testnet and publish them to NPM registry with `dapp-dev-` 14 | # suffix and `dapp-development-` tag. Such packages are meant 15 | # to be used locally by the team developing Threshold Token dApp and may 16 | # contain contracts that have different values from the ones used on 17 | # mainnet. 18 | # 2. If a workflow will be manually dspatched from a branch which name is not 19 | # `dapp-development`, the workflow will deploy the contracts on the 20 | # selected testnet and publish them to NPM registry with `` 21 | # suffix and tag. Such packages will be used later to deploy public 22 | # Threshold Token dApp on a testnet, with contracts resembling those used 23 | # on mainnet. 24 | workflow_dispatch: 25 | inputs: 26 | environment: 27 | description: "Environment (network) for workflow execution, e.g. `sepolia`" 28 | required: true 29 | upstream_builds: 30 | description: "Upstream builds" 31 | required: false 32 | upstream_ref: 33 | description: "Git reference to checkout (e.g. branch name)" 34 | required: false 35 | default: "main" 36 | 37 | jobs: 38 | contracts-detect-changes: 39 | runs-on: ubuntu-latest 40 | outputs: 41 | system-tests: ${{ steps.filter.outputs.system-tests }} 42 | steps: 43 | - uses: actions/checkout@v3 44 | if: github.event_name == 'pull_request' 45 | 46 | - uses: dorny/paths-filter@v2 47 | if: github.event_name == 'pull_request' 48 | id: filter 49 | with: 50 | filters: | 51 | system-tests: 52 | - './contracts/staking/**' 53 | - './test/system/**' 54 | 55 | contracts-build-and-test: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v3 59 | 60 | - uses: actions/setup-node@v3 61 | with: 62 | # Using fixed version, because 18.16 was sometimes causing issues with 63 | # artifacts generation during `hardhat compile` - see 64 | # https://github.com/NomicFoundation/hardhat/issues/3877 65 | node-version: "18.15.0" 66 | cache: "yarn" 67 | 68 | - name: Install dependencies 69 | run: yarn install 70 | 71 | - name: Build contracts 72 | run: yarn build 73 | 74 | - name: Run tests 75 | if: github.ref != 'refs/heads/dapp-development' 76 | run: yarn test 77 | 78 | contracts-deployment-dry-run: 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@v3 82 | 83 | - uses: actions/setup-node@v3 84 | with: 85 | # Using fixed version, because 18.16 was sometimes causing issues with 86 | # artifacts generation during `hardhat compile` - see 87 | # https://github.com/NomicFoundation/hardhat/issues/3877 88 | node-version: "18.15.0" 89 | cache: "yarn" 90 | 91 | - name: Install dependencies 92 | run: yarn install 93 | 94 | - name: Deploy contracts 95 | run: yarn deploy 96 | 97 | contracts-deployment-testnet: 98 | needs: [contracts-build-and-test] 99 | if: | 100 | github.event_name == 'workflow_dispatch' 101 | && github.ref != 'refs/heads/dapp-development' 102 | runs-on: ubuntu-latest 103 | steps: 104 | - uses: actions/checkout@v3 105 | 106 | - uses: actions/setup-node@v3 107 | with: 108 | # Using fixed version, because 18.16 was sometimes causing issues with 109 | # artifacts generation during `hardhat compile` - see 110 | # https://github.com/NomicFoundation/hardhat/issues/3877 111 | node-version: "18.15.0" 112 | cache: "yarn" 113 | registry-url: "https://registry.npmjs.org" 114 | 115 | - name: Install dependencies 116 | run: yarn install --frozen-lockfile 117 | 118 | - name: Configure tenderly 119 | env: 120 | TENDERLY_TOKEN: ${{ secrets.TENDERLY_TOKEN }} 121 | run: ./config_tenderly.sh 122 | 123 | - name: Deploy contracts 124 | env: 125 | CHAIN_API_URL: ${{ secrets.SEPOLIA_ETH_HOSTNAME_HTTP }} 126 | CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY: ${{ secrets.TESTNET_ETH_CONTRACT_OWNER_PRIVATE_KEY }} 127 | KEEP_CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY: ${{ secrets.TESTNET_ETH_CONTRACT_OWNER_PRIVATE_KEY }} 128 | run: yarn deploy --network ${{ github.event.inputs.environment }} 129 | 130 | - name: Bump up package version 131 | id: npm-version-bump 132 | uses: keep-network/npm-version-bump@v2 133 | with: 134 | environment: ${{ github.event.inputs.environment }} 135 | branch: ${{ github.ref }} 136 | commit: ${{ github.sha }} 137 | 138 | - name: Publish to npm 139 | env: 140 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 141 | run: npm publish --access=public --network=${{ github.event.inputs.environment }} --tag ${{ github.event.inputs.environment }} 142 | 143 | - name: Notify CI about completion of the workflow 144 | uses: keep-network/ci/actions/notify-workflow-completed@v2 145 | env: 146 | GITHUB_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} 147 | with: 148 | module: "github.com/threshold-network/solidity-contracts" 149 | url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 150 | environment: ${{ github.event.inputs.environment }} 151 | upstream_builds: ${{ github.event.inputs.upstream_builds }} 152 | upstream_ref: ${{ github.event.inputs.upstream_ref }} 153 | version: ${{ steps.npm-version-bump.outputs.version }} 154 | 155 | - name: Upload files needed for etherscan verification 156 | uses: actions/upload-artifact@v3 157 | with: 158 | name: Artifacts for etherscan verifcation 159 | path: | 160 | ./deployments 161 | ./package.json 162 | ./yarn.lock 163 | 164 | contracts-etherscan-verification: 165 | needs: [contracts-deployment-testnet] 166 | runs-on: ubuntu-latest 167 | steps: 168 | - uses: actions/checkout@v3 169 | 170 | - name: Download files needed for etherscan verification 171 | uses: actions/download-artifact@v3 172 | with: 173 | name: Artifacts for etherscan verifcation 174 | 175 | - uses: actions/setup-node@v3 176 | with: 177 | # Using fixed version, because 18.16 was sometimes causing issues with 178 | # artifacts generation during `hardhat compile` - see 179 | # https://github.com/NomicFoundation/hardhat/issues/3877 180 | node-version: "18.15.0" 181 | cache: "yarn" 182 | 183 | - name: Install needed dependencies 184 | run: yarn install --frozen-lockfile 185 | 186 | - name: Verify contracts on Etherscan 187 | env: 188 | ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} 189 | CHAIN_API_URL: ${{ secrets.SEPOLIA_ETH_HOSTNAME_HTTP }} 190 | run: | 191 | yarn run hardhat --network ${{ github.event.inputs.environment }} \ 192 | etherscan-verify --license GPL-3.0 --force-license 193 | 194 | # This job is responsible for publishing packages from `dapp-development` 195 | # branch, which are slightly modified to help with the process of testing some 196 | # features on the Threshold Token dApp. The job starts only if workflow gets 197 | # triggered by the `workflow_dispatch` event on the branch `dapp-development`. 198 | contracts-dapp-development-deployment-testnet: 199 | needs: [contracts-build-and-test] 200 | if: | 201 | github.event_name == 'workflow_dispatch' 202 | && github.ref == 'refs/heads/dapp-development' 203 | runs-on: ubuntu-latest 204 | steps: 205 | - uses: actions/checkout@v3 206 | 207 | - uses: actions/setup-node@v3 208 | with: 209 | # Using fixed version, because 18.16 was sometimes causing issues with 210 | # artifacts generation during `hardhat compile` - see 211 | # https://github.com/NomicFoundation/hardhat/issues/3877 212 | node-version: "18.15.0" 213 | cache: "yarn" 214 | registry-url: "https://registry.npmjs.org" 215 | 216 | - name: Install dependencies 217 | run: yarn install --frozen-lockfile 218 | 219 | - name: Deploy contracts 220 | env: 221 | CHAIN_API_URL: ${{ secrets.SEPOLIA_ETH_HOSTNAME_HTTP }} 222 | CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY: ${{ secrets.DAPP_DEV_TESTNET_ETH_CONTRACT_OWNER_PRIVATE_KEY }} 223 | KEEP_CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY: ${{ secrets.TESTNET_ETH_CONTRACT_OWNER_PRIVATE_KEY }} 224 | run: yarn deploy --network ${{ github.event.inputs.environment }} 225 | 226 | - name: Bump up package version 227 | id: npm-version-bump 228 | uses: keep-network/npm-version-bump@v2 229 | with: 230 | environment: dapp-dev-${{ github.event.inputs.environment }} 231 | branch: ${{ github.ref }} 232 | commit: ${{ github.sha }} 233 | 234 | - name: Publish to npm 235 | env: 236 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 237 | run: | 238 | npm publish --access=public \ 239 | --network=${{ github.event.inputs.environment }} \ 240 | --tag dapp-development-${{ github.event.inputs.environment }} 241 | 242 | - name: Notify CI about completion of the workflow 243 | uses: keep-network/ci/actions/notify-workflow-completed@v2 244 | env: 245 | GITHUB_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} 246 | with: 247 | module: "github.com/threshold-network/solidity-contracts" 248 | url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 249 | environment: ${{ github.event.inputs.environment }} 250 | upstream_builds: ${{ github.event.inputs.upstream_builds }} 251 | upstream_ref: dapp-development 252 | version: ${{ steps.npm-version-bump.outputs.version }} 253 | 254 | contracts-slither: 255 | runs-on: ubuntu-latest 256 | if: | 257 | github.event_name != 'workflow_dispatch' 258 | && github.event_name != 'schedule' 259 | steps: 260 | - uses: actions/checkout@v3 261 | 262 | - uses: actions/setup-node@v3 263 | with: 264 | # Using fixed version, because 18.16 was sometimes causing issues with 265 | # artifacts generation during `hardhat compile` - see 266 | # https://github.com/NomicFoundation/hardhat/issues/3877 267 | node-version: "18.15.0" 268 | cache: "yarn" 269 | 270 | - uses: actions/setup-python@v4 271 | with: 272 | python-version: 3.10.8 273 | 274 | - name: Install Solidity 275 | env: 276 | SOLC_VERSION: 0.8.9 # according to solidity.version in hardhat.config.ts 277 | run: | 278 | pip3 install solc-select 279 | solc-select install $SOLC_VERSION 280 | solc-select use $SOLC_VERSION 281 | 282 | - name: Install Slither 283 | env: 284 | SLITHER_VERSION: 0.8.0 285 | run: pip3 install slither-analyzer==$SLITHER_VERSION 286 | 287 | - name: Install dependencies 288 | run: yarn install 289 | 290 | - name: Run Slither 291 | run: slither . 292 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Code Format Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | code-lint-and-format: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: "18" 19 | cache: "yarn" 20 | 21 | - name: Install dependencies 22 | run: yarn install 23 | 24 | - name: Check formatting 25 | run: yarn format 26 | -------------------------------------------------------------------------------- /.github/workflows/npm.yml: -------------------------------------------------------------------------------- 1 | name: NPM 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "contracts/**" 9 | - "deploy/**" 10 | - "hardhat.config.ts" 11 | - "package.json" 12 | - "yarn.lock" 13 | workflow_dispatch: 14 | 15 | jobs: 16 | npm-compile-publish-contracts: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: actions/setup-node@v3 22 | with: 23 | # Using fixed version, because 18.16 may cause issues with the 24 | # artifacts generation during `hardhat compile` - see 25 | # https://github.com/NomicFoundation/hardhat/issues/3877. 26 | node-version: "18.15.0" 27 | registry-url: "https://registry.npmjs.org" 28 | cache: "yarn" 29 | 30 | - name: Install needed dependencies 31 | run: yarn install --frozen-lockfile 32 | 33 | # Deploy contracts to a local network to generate deployment artifacts that 34 | # are required by dashboard compilation. 35 | - name: Deploy contracts 36 | run: yarn deploy --network hardhat --write true 37 | 38 | - name: Bump up package version 39 | id: npm-version-bump 40 | uses: keep-network/npm-version-bump@v2 41 | with: 42 | environment: dev 43 | branch: ${{ github.ref }} 44 | commit: ${{ github.sha }} 45 | 46 | - name: Publish package 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | run: npm publish --access=public --network=hardhat --tag=development 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | artifacts/ 4 | build/ 5 | cache/ 6 | deployments/* 7 | !deployments/mainnet/ 8 | export/ 9 | export.json 10 | 11 | external/npm 12 | 13 | typechain/ 14 | 15 | yarn-error.log 16 | 17 | .vscode/ 18 | 19 | .hardhat/* 20 | !.hardhat/networks_TEMPLATE.ts 21 | 22 | # OpenZeppelin 23 | .openzeppelin/unknown-*.json 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/keep-network/pre-commit-hooks.git 3 | rev: v1.3.0 4 | hooks: 5 | - id: check-added-large-files 6 | - repo: local 7 | hooks: 8 | - id: lint-js 9 | name: "lint js" 10 | entry: /usr/bin/env bash -c "npx run lint:js" 11 | files: '\.js$' 12 | language: script 13 | description: "Checks JS code according to the package's linter configuration" 14 | - id: lint-sol 15 | name: "lint solidity" 16 | entry: /usr/bin/env bash -c "npx run lint:sol" 17 | files: '\.sol$' 18 | language: script 19 | description: "Checks Solidity code according to the package's linter configuration" 20 | - id: prettier 21 | name: "prettier" 22 | entry: /usr/bin/env bash -c "npx prettier --check ." 23 | language: script 24 | description: "Checks code according to the package's formatting configuration" 25 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .openzeppelin/ 2 | artifacts/ 3 | build/ 4 | cache/ 5 | deployments/ 6 | docgen-templates/ 7 | export/ 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("@keep-network/prettier-config-keep"), 3 | plugins: ["prettier-plugin-sh"], 4 | } 5 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "keep", 3 | "plugins": [], 4 | "rules": { 5 | "max-states-count": ["warn", 20], 6 | "func-visibility": ["error", { "ignoreConstructors": true }], 7 | "no-inline-assembly": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | /contracts/ @vzotova @cygnusv @pdyraga @lukasz-zimnoch 2 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | :toc: macro 2 | 3 | = Threshold network contracts 4 | 5 | This package contains Threshold network contracts. 6 | 7 | toc::[] 8 | 9 | == Build, test and deploy 10 | 11 | Threshold contracts use https://hardhat.org/[*Hardhat*] development environment. 12 | To build and deploy these contracts, please follow the instructions presented 13 | below. 14 | 15 | === Prerequisites 16 | 17 | Please make sure you have the following prerequisites installed on your machine: 18 | 19 | - https://nodejs.org[Node.js] >14.17.4 20 | - https://yarnpkg.com[Yarn] >1.22.10 21 | 22 | === Build contracts 23 | 24 | To build the smart contracts, install node packages first: 25 | ``` 26 | yarn install 27 | ``` 28 | Once packages are installed, you can build the smart contracts using: 29 | ``` 30 | yarn build 31 | ``` 32 | Compiled contracts will land in the `build` directory. 33 | 34 | === Test contracts 35 | 36 | There are multiple test scenarios living in the `test` directory. 37 | You can run them by doing: 38 | ``` 39 | yarn test 40 | ``` 41 | 42 | === Deploy contracts 43 | 44 | To deploy all contracts on the given network, please run: 45 | ``` 46 | yarn deploy --network 47 | ``` 48 | 49 | If contracts haven't been built yet or changes occurred, this task will build 50 | the contracts before running the deployment script. This command produces 51 | an `export.json` file containing contract deployment info. 52 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Bug Bounty Program 4 | 5 | Threshold Network has a [Bug Bounty program with Immunefi](https://immunefi.com/bounty/thresholdnetwork/). 6 | 7 | The details for the Bug Bounty are maintained and updated at the [Immunefi Threshold page](https://immunefi.com/bounty/thresholdnetwork/). There you can explore the assets in scope for the bounty and the different rewards by threat level. As a guide, the initial bounty program launched with the following rewards according to the severity of the threats found: 8 | 9 | Smart Contracts 10 | 11 | - Critical Level: USD $100,000 to USD $500,000 12 | - High Level: USD $10,000 to USD $50,000 13 | - Medium Level: USD $1,000 to USD $5,000 14 | - Low Level: USD $1,000 15 | 16 | Websites and Applications 17 | 18 | - Critical Level: USD $10,000 to USD $25,000 19 | - High Level: USD $1,000 to USD $10,000 20 | - Medium Level: USD $1,000 21 | 22 | A great place to begin your research is by working on our testnet. Please see our [documentation](https://docs.threshold.network) to get started. We ask that you please respect network machines and their owners. If you find a vulnerability that you suspect has given you access to a machine against the owner's permission, stop what you're doing and create a report using the Immunefi dashboard for researchers. 23 | 24 | Rewards are distributed according to the impact of the vulnerability based on the [Immunefi Vulnerability Severity Classification System V2.3](https://immunefi.com/immunefi-vulnerability-severity-classification-system-v2-3/). This is a simplified 4-level scale, with separate scales for websites/apps, smart contracts, and blockchains/DLTs, focusing on the impact of the vulnerability reported. 25 | 26 | ## Reporting a Vulnerability Not Covered by the Bug Bounty Program 27 | 28 | Please verify the list of assets in-scope and out-of-scope available as part of the [Threshold Bug Bounty details](https://immunefi.com/bounty/thresholdnetwork/). Additionally, security researchers are encouraged to submit issues outside of the outlined "Impacts" and "Assets in Scope". If you can demonstrate a critical impact on code in production for an asset not in scope, Threshold DAO encourages you to submit your bug report using the “primacy of impact exception” asset in Immunefi. 29 | 30 | Threshold DAO will try to make an initial assessment of a bug's relevance, severity, and exploitability, and communicate this back to the reporter. The Threshold DAO will compensate important findings on a case-by-case basis. We value security researchers and we encourage you to contact us to discuss your findings. 31 | 32 | We also ask all researchers to please submit their reports in English. 33 | -------------------------------------------------------------------------------- /config_tenderly.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | LOG_START='\n\e[1;36m' # new line + bold + color 6 | LOG_END='\n\e[0m' # new line + reset color 7 | 8 | printf "${LOG_START}Configuring Tenderly...${LOG_END}" 9 | 10 | mkdir $HOME/.tenderly && touch $HOME/.tenderly/config.yaml 11 | 12 | echo access_key: ${TENDERLY_TOKEN} > $HOME/.tenderly/config.yaml 13 | -------------------------------------------------------------------------------- /contracts/governance/BaseTokenholderGovernor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | import "./TokenholderGovernorVotes.sol"; 19 | import "../token/T.sol"; 20 | import "@openzeppelin/contracts/access/AccessControl.sol"; 21 | import "@openzeppelin/contracts/governance/Governor.sol"; 22 | import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; 23 | import "@openzeppelin/contracts/governance/extensions/GovernorPreventLateQuorum.sol"; 24 | import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; 25 | 26 | contract BaseTokenholderGovernor is 27 | AccessControl, 28 | GovernorCountingSimple, 29 | TokenholderGovernorVotes, 30 | GovernorPreventLateQuorum, 31 | GovernorTimelockControl 32 | { 33 | bytes32 public constant VETO_POWER = 34 | keccak256("Power to veto proposals in Threshold's Tokenholder DAO"); 35 | 36 | constructor( 37 | T _token, 38 | IVotesHistory _staking, 39 | TimelockController _timelock, 40 | address _vetoer, 41 | uint256 _quorumNumerator, 42 | uint256 _proposalThresholdNumerator, 43 | uint256 votingDelay, 44 | uint256 votingPeriod, 45 | uint64 votingExtension 46 | ) 47 | Governor("TokenholderGovernor") 48 | GovernorParameters( 49 | _quorumNumerator, 50 | _proposalThresholdNumerator, 51 | votingDelay, 52 | votingPeriod 53 | ) 54 | GovernorPreventLateQuorum(votingExtension) 55 | TokenholderGovernorVotes(_token, _staking) 56 | GovernorTimelockControl(_timelock) 57 | { 58 | _setupRole(VETO_POWER, _vetoer); 59 | _setupRole(DEFAULT_ADMIN_ROLE, address(_timelock)); 60 | } 61 | 62 | function cancel( 63 | address[] memory targets, 64 | uint256[] memory values, 65 | bytes[] memory calldatas, 66 | bytes32 descriptionHash 67 | ) external onlyRole(VETO_POWER) returns (uint256) { 68 | return _cancel(targets, values, calldatas, descriptionHash); 69 | } 70 | 71 | function propose( 72 | address[] memory targets, 73 | uint256[] memory values, 74 | bytes[] memory calldatas, 75 | string memory description 76 | ) public override(Governor, IGovernor) returns (uint256) { 77 | return super.propose(targets, values, calldatas, description); 78 | } 79 | 80 | function quorum(uint256 blockNumber) 81 | public 82 | view 83 | override(IGovernor, GovernorParameters) 84 | returns (uint256) 85 | { 86 | return super.quorum(blockNumber); 87 | } 88 | 89 | function proposalThreshold() 90 | public 91 | view 92 | override(Governor, GovernorParameters) 93 | returns (uint256) 94 | { 95 | return super.proposalThreshold(); 96 | } 97 | 98 | function getVotes(address account, uint256 blockNumber) 99 | public 100 | view 101 | override(IGovernor, TokenholderGovernorVotes) 102 | returns (uint256) 103 | { 104 | return super.getVotes(account, blockNumber); 105 | } 106 | 107 | function state(uint256 proposalId) 108 | public 109 | view 110 | override(Governor, GovernorTimelockControl) 111 | returns (ProposalState) 112 | { 113 | return super.state(proposalId); 114 | } 115 | 116 | function supportsInterface(bytes4 interfaceId) 117 | public 118 | view 119 | override(Governor, GovernorTimelockControl, AccessControl) 120 | returns (bool) 121 | { 122 | return super.supportsInterface(interfaceId); 123 | } 124 | 125 | function proposalDeadline(uint256 proposalId) 126 | public 127 | view 128 | virtual 129 | override(IGovernor, Governor, GovernorPreventLateQuorum) 130 | returns (uint256) 131 | { 132 | return super.proposalDeadline(proposalId); 133 | } 134 | 135 | function _execute( 136 | uint256 proposalId, 137 | address[] memory targets, 138 | uint256[] memory values, 139 | bytes[] memory calldatas, 140 | bytes32 descriptionHash 141 | ) internal override(Governor, GovernorTimelockControl) { 142 | super._execute(proposalId, targets, values, calldatas, descriptionHash); 143 | } 144 | 145 | function _cancel( 146 | address[] memory targets, 147 | uint256[] memory values, 148 | bytes[] memory calldatas, 149 | bytes32 descriptionHash 150 | ) internal override(Governor, GovernorTimelockControl) returns (uint256) { 151 | return super._cancel(targets, values, calldatas, descriptionHash); 152 | } 153 | 154 | function _castVote( 155 | uint256 proposalId, 156 | address account, 157 | uint8 support, 158 | string memory reason 159 | ) 160 | internal 161 | virtual 162 | override(Governor, GovernorPreventLateQuorum) 163 | returns (uint256) 164 | { 165 | return super._castVote(proposalId, account, support, reason); 166 | } 167 | 168 | function _executor() 169 | internal 170 | view 171 | override(Governor, GovernorTimelockControl) 172 | returns (address) 173 | { 174 | return super._executor(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /contracts/governance/Checkpoints.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | import "./IVotesHistory.sol"; 19 | import "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; 20 | import "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; 21 | 22 | /// @title Checkpoints 23 | /// @dev Abstract contract to support checkpoints for Compound-like voting and 24 | /// delegation. This implementation supports token supply up to 2^96 - 1. 25 | /// This contract keeps a history (checkpoints) of each account's vote 26 | /// power. Vote power can be delegated either by calling the {delegate} 27 | /// function directly, or by providing a signature to be used with 28 | /// {delegateBySig}. Voting power can be publicly queried through 29 | /// {getVotes} and {getPastVotes}. 30 | /// NOTE: Extracted from OpenZeppelin ERCVotes.sol. 31 | /// @dev This contract is upgrade-safe. 32 | abstract contract Checkpoints is IVotesHistory { 33 | struct Checkpoint { 34 | uint32 fromBlock; 35 | uint96 votes; 36 | } 37 | 38 | // slither-disable-next-line uninitialized-state 39 | mapping(address => address) internal _delegates; 40 | mapping(address => uint128[]) internal _checkpoints; 41 | uint128[] internal _totalSupplyCheckpoints; 42 | 43 | // Reserved storage space in case we need to add more variables, 44 | // since there are upgradeable contracts that inherit from this one. 45 | // See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 46 | // slither-disable-next-line unused-state 47 | uint256[47] private __gap; 48 | 49 | /// @notice Emitted when an account changes their delegate. 50 | event DelegateChanged( 51 | address indexed delegator, 52 | address indexed fromDelegate, 53 | address indexed toDelegate 54 | ); 55 | 56 | /// @notice Emitted when a balance or delegate change results in changes 57 | /// to an account's voting power. 58 | event DelegateVotesChanged( 59 | address indexed delegate, 60 | uint256 previousBalance, 61 | uint256 newBalance 62 | ); 63 | 64 | function checkpoints(address account, uint32 pos) 65 | public 66 | view 67 | virtual 68 | returns (Checkpoint memory checkpoint) 69 | { 70 | (uint32 fromBlock, uint96 votes) = decodeCheckpoint( 71 | _checkpoints[account][pos] 72 | ); 73 | checkpoint = Checkpoint(fromBlock, votes); 74 | } 75 | 76 | /// @notice Get number of checkpoints for `account`. 77 | function numCheckpoints(address account) 78 | public 79 | view 80 | virtual 81 | returns (uint32) 82 | { 83 | return SafeCastUpgradeable.toUint32(_checkpoints[account].length); 84 | } 85 | 86 | /// @notice Get the address `account` is currently delegating to. 87 | function delegates(address account) public view virtual returns (address) { 88 | return _delegates[account]; 89 | } 90 | 91 | /// @notice Gets the current votes balance for `account`. 92 | /// @param account The address to get votes balance 93 | /// @return The number of current votes for `account` 94 | function getVotes(address account) public view returns (uint96) { 95 | uint256 pos = _checkpoints[account].length; 96 | return pos == 0 ? 0 : decodeValue(_checkpoints[account][pos - 1]); 97 | } 98 | 99 | /// @notice Determine the prior number of votes for an account as of 100 | /// a block number. 101 | /// @dev Block number must be a finalized block or else this function will 102 | /// revert to prevent misinformation. 103 | /// @param account The address of the account to check 104 | /// @param blockNumber The block number to get the vote balance at 105 | /// @return The number of votes the account had as of the given block 106 | function getPastVotes(address account, uint256 blockNumber) 107 | public 108 | view 109 | returns (uint96) 110 | { 111 | return lookupCheckpoint(_checkpoints[account], blockNumber); 112 | } 113 | 114 | /// @notice Retrieve the `totalSupply` at the end of `blockNumber`. 115 | /// Note, this value is the sum of all balances, but it is NOT the 116 | /// sum of all the delegated votes! 117 | /// @param blockNumber The block number to get the total supply at 118 | /// @dev `blockNumber` must have been already mined 119 | function getPastTotalSupply(uint256 blockNumber) 120 | public 121 | view 122 | returns (uint96) 123 | { 124 | return lookupCheckpoint(_totalSupplyCheckpoints, blockNumber); 125 | } 126 | 127 | /// @notice Change delegation for `delegator` to `delegatee`. 128 | // slither-disable-next-line dead-code 129 | function delegate(address delegator, address delegatee) internal virtual; 130 | 131 | /// @notice Moves voting power from one delegate to another 132 | /// @param src Address of old delegate 133 | /// @param dst Address of new delegate 134 | /// @param amount Voting power amount to transfer between delegates 135 | function moveVotingPower( 136 | address src, 137 | address dst, 138 | uint256 amount 139 | ) internal { 140 | if (src != dst && amount > 0) { 141 | if (src != address(0)) { 142 | // https://github.com/crytic/slither/issues/960 143 | // slither-disable-next-line variable-scope 144 | (uint256 oldWeight, uint256 newWeight) = writeCheckpoint( 145 | _checkpoints[src], 146 | subtract, 147 | amount 148 | ); 149 | emit DelegateVotesChanged(src, oldWeight, newWeight); 150 | } 151 | 152 | if (dst != address(0)) { 153 | // https://github.com/crytic/slither/issues/959 154 | // slither-disable-next-line uninitialized-local 155 | (uint256 oldWeight, uint256 newWeight) = writeCheckpoint( 156 | _checkpoints[dst], 157 | add, 158 | amount 159 | ); 160 | emit DelegateVotesChanged(dst, oldWeight, newWeight); 161 | } 162 | } 163 | } 164 | 165 | /// @notice Writes a new checkpoint based on operating last stored value 166 | /// with a `delta`. Usually, said operation is the `add` or 167 | /// `subtract` functions from this contract, but more complex 168 | /// functions can be passed as parameters. 169 | /// @param ckpts The checkpoints array to use 170 | /// @param op The function to apply over the last value and the `delta` 171 | /// @param delta Variation with respect to last stored value to be used 172 | /// for new checkpoint 173 | function writeCheckpoint( 174 | uint128[] storage ckpts, 175 | function(uint256, uint256) view returns (uint256) op, 176 | uint256 delta 177 | ) internal returns (uint256 oldWeight, uint256 newWeight) { 178 | uint256 pos = ckpts.length; 179 | oldWeight = pos == 0 ? 0 : decodeValue(ckpts[pos - 1]); 180 | newWeight = op(oldWeight, delta); 181 | 182 | if (pos > 0) { 183 | uint32 fromBlock = decodeBlockNumber(ckpts[pos - 1]); 184 | // slither-disable-next-line incorrect-equality 185 | if (fromBlock == block.number) { 186 | ckpts[pos - 1] = encodeCheckpoint( 187 | fromBlock, 188 | SafeCastUpgradeable.toUint96(newWeight) 189 | ); 190 | return (oldWeight, newWeight); 191 | } 192 | } 193 | 194 | ckpts.push( 195 | encodeCheckpoint( 196 | SafeCastUpgradeable.toUint32(block.number), 197 | SafeCastUpgradeable.toUint96(newWeight) 198 | ) 199 | ); 200 | } 201 | 202 | /// @notice Lookup a value in a list of (sorted) checkpoints. 203 | /// @param ckpts The checkpoints array to use 204 | /// @param blockNumber Block number when we want to get the checkpoint at 205 | function lookupCheckpoint(uint128[] storage ckpts, uint256 blockNumber) 206 | internal 207 | view 208 | returns (uint96) 209 | { 210 | // We run a binary search to look for the earliest checkpoint taken 211 | // after `blockNumber`. During the loop, the index of the wanted 212 | // checkpoint remains in the range [low-1, high). With each iteration, 213 | // either `low` or `high` is moved towards the middle of the range to 214 | // maintain the invariant. 215 | // - If the middle checkpoint is after `blockNumber`, 216 | // we look in [low, mid) 217 | // - If the middle checkpoint is before or equal to `blockNumber`, 218 | // we look in [mid+1, high) 219 | // Once we reach a single value (when low == high), we've found the 220 | // right checkpoint at the index high-1, if not out of bounds (in that 221 | // case we're looking too far in the past and the result is 0). 222 | // Note that if the latest checkpoint available is exactly for 223 | // `blockNumber`, we end up with an index that is past the end of the 224 | // array, so we technically don't find a checkpoint after 225 | // `blockNumber`, but it works out the same. 226 | require(blockNumber < block.number, "Block not yet determined"); 227 | 228 | uint256 high = ckpts.length; 229 | uint256 low = 0; 230 | while (low < high) { 231 | uint256 mid = MathUpgradeable.average(low, high); 232 | uint32 midBlock = decodeBlockNumber(ckpts[mid]); 233 | if (midBlock > blockNumber) { 234 | high = mid; 235 | } else { 236 | low = mid + 1; 237 | } 238 | } 239 | 240 | return high == 0 ? 0 : decodeValue(ckpts[high - 1]); 241 | } 242 | 243 | /// @notice Maximum token supply. Defaults to `type(uint96).max` (2^96 - 1) 244 | // slither-disable-next-line dead-code 245 | function maxSupply() internal view virtual returns (uint96) { 246 | return type(uint96).max; 247 | } 248 | 249 | /// @notice Encodes a `blockNumber` and `value` into a single `uint128` 250 | /// checkpoint. 251 | /// @dev `blockNumber` is stored in the first 32 bits, while `value` in the 252 | /// remaining 96 bits. 253 | function encodeCheckpoint(uint32 blockNumber, uint96 value) 254 | internal 255 | pure 256 | returns (uint128) 257 | { 258 | return (uint128(blockNumber) << 96) | uint128(value); 259 | } 260 | 261 | /// @notice Decodes a block number from a `uint128` `checkpoint`. 262 | function decodeBlockNumber(uint128 checkpoint) 263 | internal 264 | pure 265 | returns (uint32) 266 | { 267 | return uint32(bytes4(bytes16(checkpoint))); 268 | } 269 | 270 | /// @notice Decodes a voting value from a `uint128` `checkpoint`. 271 | function decodeValue(uint128 checkpoint) internal pure returns (uint96) { 272 | return uint96(checkpoint); 273 | } 274 | 275 | /// @notice Decodes a block number and voting value from a `uint128` 276 | /// `checkpoint`. 277 | function decodeCheckpoint(uint128 checkpoint) 278 | internal 279 | pure 280 | returns (uint32 blockNumber, uint96 value) 281 | { 282 | blockNumber = decodeBlockNumber(checkpoint); 283 | value = decodeValue(checkpoint); 284 | } 285 | 286 | // slither-disable-next-line dead-code 287 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 288 | return a + b; 289 | } 290 | 291 | // slither-disable-next-line dead-code 292 | function subtract(uint256 a, uint256 b) internal pure returns (uint256) { 293 | return a - b; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /contracts/governance/GovernorParameters.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | import "@openzeppelin/contracts/governance/Governor.sol"; 19 | 20 | /// @title GovernorParameters 21 | /// @notice Abstract contract to handle governance parameters 22 | /// @dev Based on `GovernorVotesQuorumFraction`, but without being opinionated 23 | /// on what's the source of voting power, and extended to handle proposal 24 | /// thresholds too. See OpenZeppelin's GovernorVotesQuorumFraction, 25 | /// GovernorVotes and GovernorSettings for reference. 26 | abstract contract GovernorParameters is Governor { 27 | uint256 public constant FRACTION_DENOMINATOR = 10000; 28 | uint64 internal constant AVERAGE_BLOCK_TIME_IN_SECONDS = 13; 29 | 30 | uint256 public quorumNumerator; 31 | uint256 public proposalThresholdNumerator; 32 | 33 | uint256 private _votingDelay; 34 | uint256 private _votingPeriod; 35 | 36 | event QuorumNumeratorUpdated( 37 | uint256 oldQuorumNumerator, 38 | uint256 newQuorumNumerator 39 | ); 40 | 41 | event ProposalThresholdNumeratorUpdated( 42 | uint256 oldThresholdNumerator, 43 | uint256 newThresholdNumerator 44 | ); 45 | 46 | event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay); 47 | event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod); 48 | 49 | constructor( 50 | uint256 quorumNumeratorValue, 51 | uint256 proposalNumeratorValue, 52 | uint256 initialVotingDelay, 53 | uint256 initialVotingPeriod 54 | ) { 55 | _updateQuorumNumerator(quorumNumeratorValue); 56 | _updateProposalThresholdNumerator(proposalNumeratorValue); 57 | _setVotingDelay(initialVotingDelay); 58 | _setVotingPeriod(initialVotingPeriod); 59 | } 60 | 61 | function updateQuorumNumerator(uint256 newQuorumNumerator) 62 | external 63 | virtual 64 | onlyGovernance 65 | { 66 | _updateQuorumNumerator(newQuorumNumerator); 67 | } 68 | 69 | function updateProposalThresholdNumerator(uint256 newNumerator) 70 | external 71 | virtual 72 | onlyGovernance 73 | { 74 | _updateProposalThresholdNumerator(newNumerator); 75 | } 76 | 77 | /// @notice Update the voting delay. This operation can only be performed 78 | /// through a governance proposal. Emits a `VotingDelaySet` event. 79 | function setVotingDelay(uint256 newVotingDelay) 80 | external 81 | virtual 82 | onlyGovernance 83 | { 84 | _setVotingDelay(newVotingDelay); 85 | } 86 | 87 | /// @notice Update the voting period. This operation can only be performed 88 | /// through a governance proposal. Emits a `VotingPeriodSet` event. 89 | function setVotingPeriod(uint256 newVotingPeriod) 90 | external 91 | virtual 92 | onlyGovernance 93 | { 94 | _setVotingPeriod(newVotingPeriod); 95 | } 96 | 97 | /// @notice Compute the required amount of voting power to reach quorum 98 | /// @param blockNumber The block number to get the quorum at 99 | function quorum(uint256 blockNumber) 100 | public 101 | view 102 | virtual 103 | override 104 | returns (uint256) 105 | { 106 | return 107 | (_getPastTotalSupply(blockNumber) * quorumNumerator) / 108 | FRACTION_DENOMINATOR; 109 | } 110 | 111 | /// @notice Compute the required amount of voting power to create a proposal 112 | /// at the last block height 113 | /// @dev This function is implemented to comply with Governor API but we 114 | /// we will actually use `proposalThreshold(uint256 blockNumber)`, 115 | /// as in our DAOs the threshold amount changes according to supply. 116 | function proposalThreshold() 117 | public 118 | view 119 | virtual 120 | override 121 | returns (uint256) 122 | { 123 | return proposalThreshold(block.number - 1); 124 | } 125 | 126 | /// @notice Compute the required amount of voting power to create a proposal 127 | /// @param blockNumber The block number to get the proposal threshold at 128 | function proposalThreshold(uint256 blockNumber) 129 | public 130 | view 131 | returns (uint256) 132 | { 133 | return 134 | (_getPastTotalSupply(blockNumber) * proposalThresholdNumerator) / 135 | FRACTION_DENOMINATOR; 136 | } 137 | 138 | function votingDelay() public view virtual override returns (uint256) { 139 | return _votingDelay; 140 | } 141 | 142 | function votingPeriod() public view virtual override returns (uint256) { 143 | return _votingPeriod; 144 | } 145 | 146 | function _updateQuorumNumerator(uint256 newQuorumNumerator) 147 | internal 148 | virtual 149 | { 150 | require( 151 | newQuorumNumerator <= FRACTION_DENOMINATOR, 152 | "quorumNumerator > Denominator" 153 | ); 154 | 155 | uint256 oldQuorumNumerator = quorumNumerator; 156 | quorumNumerator = newQuorumNumerator; 157 | 158 | emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator); 159 | } 160 | 161 | function _updateProposalThresholdNumerator(uint256 proposalNumerator) 162 | internal 163 | virtual 164 | { 165 | require( 166 | proposalNumerator <= FRACTION_DENOMINATOR, 167 | "proposalNumerator > Denominator" 168 | ); 169 | 170 | uint256 oldNumerator = proposalThresholdNumerator; 171 | proposalThresholdNumerator = proposalNumerator; 172 | 173 | emit ProposalThresholdNumeratorUpdated(oldNumerator, proposalNumerator); 174 | } 175 | 176 | function _setVotingDelay(uint256 newVotingDelay) internal virtual { 177 | emit VotingDelaySet(_votingDelay, newVotingDelay); 178 | _votingDelay = newVotingDelay; 179 | } 180 | 181 | function _setVotingPeriod(uint256 newVotingPeriod) internal virtual { 182 | // voting period must be at least one block long 183 | require(newVotingPeriod > 0, "Voting period too low"); 184 | emit VotingPeriodSet(_votingPeriod, newVotingPeriod); 185 | _votingPeriod = newVotingPeriod; 186 | } 187 | 188 | /// @notice Compute the past total voting power at a particular block 189 | /// @param blockNumber The block number to get the vote power at 190 | // slither-disable-next-line dead-code 191 | function _getPastTotalSupply(uint256 blockNumber) 192 | internal 193 | view 194 | virtual 195 | returns (uint256); 196 | } 197 | -------------------------------------------------------------------------------- /contracts/governance/IVotesHistory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | interface IVotesHistory { 19 | function getPastVotes(address account, uint256 blockNumber) 20 | external 21 | view 22 | returns (uint96); 23 | 24 | function getPastTotalSupply(uint256 blockNumber) 25 | external 26 | view 27 | returns (uint96); 28 | } 29 | -------------------------------------------------------------------------------- /contracts/governance/ProxyAdminWithDeputy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity 0.8.9; 4 | 5 | import "./StakerGovernor.sol"; 6 | import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; 7 | 8 | /// @title ProxyAdminWithDeputy 9 | /// @notice Based on `ProxyAdmin`, an auxiliary contract in OpenZeppelin's 10 | /// upgradeability approach meant to act as the admin of a 11 | /// `TransparentUpgradeableProxy`. This variant allows an additional 12 | /// actor, the "deputy", to perform upgrades, which originally can only 13 | /// be performed by the ProxyAdmin's owner. See OpenZeppelin's 14 | /// documentation for `TransparentUpgradeableProxy` for more details on 15 | /// why a ProxyAdmin is recommended. 16 | contract ProxyAdminWithDeputy is ProxyAdmin { 17 | address public deputy; 18 | event DeputyUpdated( 19 | address indexed previousDeputy, 20 | address indexed newDeputy 21 | ); 22 | 23 | modifier onlyOwnerOrDeputy() { 24 | _checkCallerIsOwnerOrDeputy(); 25 | _; 26 | } 27 | 28 | constructor(StakerGovernor dao, address _deputy) { 29 | address timelock = dao.timelock(); 30 | require(timelock != address(0), "DAO doesn't have a Timelock"); 31 | _setDeputy(_deputy); 32 | _transferOwnership(timelock); 33 | } 34 | 35 | function setDeputy(address newDeputy) external onlyOwner { 36 | _setDeputy(newDeputy); 37 | } 38 | 39 | /// @notice Upgrades `proxy` to `implementation`. This contract must be the 40 | /// admin of `proxy`, and the caller must be this contract's owner 41 | /// or the deputy. 42 | function upgrade(TransparentUpgradeableProxy proxy, address implementation) 43 | public 44 | virtual 45 | override 46 | onlyOwnerOrDeputy 47 | { 48 | proxy.upgradeTo(implementation); 49 | } 50 | 51 | /// @notice Upgrades `proxy` to `implementation` and calls a function on the 52 | /// new implementation. This contract must be the admin of `proxy`, 53 | /// and the caller must be this contract's owner or the deputy. 54 | function upgradeAndCall( 55 | TransparentUpgradeableProxy proxy, 56 | address implementation, 57 | bytes memory data 58 | ) public payable virtual override onlyOwnerOrDeputy { 59 | proxy.upgradeToAndCall{value: msg.value}(implementation, data); 60 | } 61 | 62 | function _setDeputy(address newDeputy) internal { 63 | address oldDeputy = deputy; 64 | deputy = newDeputy; 65 | emit DeputyUpdated(oldDeputy, newDeputy); 66 | } 67 | 68 | function _checkCallerIsOwnerOrDeputy() internal view { 69 | address caller = _msgSender(); 70 | require( 71 | owner() == caller || deputy == caller, 72 | "Caller is neither the owner nor the deputy" 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /contracts/governance/StakerGovernor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | import "./StakerGovernorVotes.sol"; 19 | import "./TokenholderGovernor.sol"; 20 | import "../token/T.sol"; 21 | import "@openzeppelin/contracts/access/AccessControl.sol"; 22 | import "@openzeppelin/contracts/governance/Governor.sol"; 23 | import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; 24 | import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; 25 | import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; 26 | 27 | contract StakerGovernor is 28 | AccessControl, 29 | GovernorCountingSimple, 30 | StakerGovernorVotes, 31 | GovernorTimelockControl 32 | { 33 | uint256 private constant INITIAL_QUORUM_NUMERATOR = 150; // Defined in basis points, i.e., 1.5% 34 | uint256 private constant INITIAL_PROPOSAL_THRESHOLD_NUMERATOR = 25; // Defined in basis points, i.e., 0.25% 35 | uint256 private constant INITIAL_VOTING_DELAY = 36 | 2 days / AVERAGE_BLOCK_TIME_IN_SECONDS; 37 | uint256 private constant INITIAL_VOTING_PERIOD = 38 | 10 days / AVERAGE_BLOCK_TIME_IN_SECONDS; 39 | 40 | bytes32 public constant VETO_POWER = 41 | keccak256("Power to veto proposals in Threshold's Staker DAO"); 42 | 43 | address internal immutable manager; 44 | 45 | constructor( 46 | IVotesHistory _staking, 47 | TimelockController _timelock, 48 | TokenholderGovernor tokenholderGovernor, 49 | address vetoer 50 | ) 51 | Governor("StakerGovernor") 52 | GovernorParameters( 53 | INITIAL_QUORUM_NUMERATOR, 54 | INITIAL_PROPOSAL_THRESHOLD_NUMERATOR, 55 | INITIAL_VOTING_DELAY, 56 | INITIAL_VOTING_PERIOD 57 | ) 58 | StakerGovernorVotes(_staking) 59 | GovernorTimelockControl(_timelock) 60 | { 61 | require( 62 | keccak256(bytes(tokenholderGovernor.name())) == 63 | keccak256(bytes("TokenholderGovernor")), 64 | "Incorrect TokenholderGovernor" 65 | ); 66 | manager = tokenholderGovernor.timelock(); 67 | require(manager != address(0), "No timelock founds"); 68 | _setupRole(VETO_POWER, vetoer); 69 | _setupRole(DEFAULT_ADMIN_ROLE, manager); 70 | } 71 | 72 | function cancel( 73 | address[] memory targets, 74 | uint256[] memory values, 75 | bytes[] memory calldatas, 76 | bytes32 descriptionHash 77 | ) external onlyRole(VETO_POWER) returns (uint256) { 78 | return _cancel(targets, values, calldatas, descriptionHash); 79 | } 80 | 81 | function propose( 82 | address[] memory targets, 83 | uint256[] memory values, 84 | bytes[] memory calldatas, 85 | string memory description 86 | ) public override(Governor, IGovernor) returns (uint256) { 87 | uint256 atLastBlock = block.number - 1; 88 | require( 89 | getVotes(msg.sender, atLastBlock) >= proposalThreshold(atLastBlock), 90 | "Proposal below threshold" 91 | ); 92 | return super.propose(targets, values, calldatas, description); 93 | } 94 | 95 | function quorum(uint256 blockNumber) 96 | public 97 | view 98 | override(IGovernor, GovernorParameters) 99 | returns (uint256) 100 | { 101 | return super.quorum(blockNumber); 102 | } 103 | 104 | function proposalThreshold() 105 | public 106 | view 107 | override(Governor, GovernorParameters) 108 | returns (uint256) 109 | { 110 | return super.proposalThreshold(); 111 | } 112 | 113 | function getVotes(address account, uint256 blockNumber) 114 | public 115 | view 116 | override(IGovernor, StakerGovernorVotes) 117 | returns (uint256) 118 | { 119 | return super.getVotes(account, blockNumber); 120 | } 121 | 122 | function state(uint256 proposalId) 123 | public 124 | view 125 | override(Governor, GovernorTimelockControl) 126 | returns (ProposalState) 127 | { 128 | return super.state(proposalId); 129 | } 130 | 131 | function supportsInterface(bytes4 interfaceId) 132 | public 133 | view 134 | override(Governor, GovernorTimelockControl, AccessControl) 135 | returns (bool) 136 | { 137 | return super.supportsInterface(interfaceId); 138 | } 139 | 140 | function _execute( 141 | uint256 proposalId, 142 | address[] memory targets, 143 | uint256[] memory values, 144 | bytes[] memory calldatas, 145 | bytes32 descriptionHash 146 | ) internal override(Governor, GovernorTimelockControl) { 147 | super._execute(proposalId, targets, values, calldatas, descriptionHash); 148 | } 149 | 150 | function _cancel( 151 | address[] memory targets, 152 | uint256[] memory values, 153 | bytes[] memory calldatas, 154 | bytes32 descriptionHash 155 | ) internal override(Governor, GovernorTimelockControl) returns (uint256) { 156 | return super._cancel(targets, values, calldatas, descriptionHash); 157 | } 158 | 159 | /// @notice Returns the address of the entity that acts as governance for 160 | /// this contract. 161 | /// @dev By default, Governor assumes this is either the Governor contract 162 | /// itself, or a timelock if there's one configured. We override this 163 | /// here for the StakerGovernor contract so it's the Tokenholder DAO's 164 | /// Timelock, which we obtain at constructor time. 165 | function _executor() 166 | internal 167 | view 168 | override(Governor, GovernorTimelockControl) 169 | returns (address) 170 | { 171 | return manager; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /contracts/governance/StakerGovernorVotes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | import "./GovernorParameters.sol"; 19 | import "./IVotesHistory.sol"; 20 | 21 | /// @title StakerGovernorVotes 22 | /// @notice Staker DAO voting power extraction from staked T positions. 23 | abstract contract StakerGovernorVotes is GovernorParameters { 24 | IVotesHistory public immutable staking; 25 | 26 | constructor(IVotesHistory tStakingAddress) { 27 | staking = tStakingAddress; 28 | } 29 | 30 | /// @notice Read the voting weight from the snapshot mechanism in the T 31 | /// staking contracts. 32 | /// @param account Delegate account with T staking voting power 33 | /// @param blockNumber The block number to get the vote balance at 34 | /// @dev See {IGovernor-getVotes} 35 | function getVotes(address account, uint256 blockNumber) 36 | public 37 | view 38 | virtual 39 | override 40 | returns (uint256) 41 | { 42 | return staking.getPastVotes(account, blockNumber); 43 | } 44 | 45 | /// @notice Compute the total voting power for the Staker DAO. 46 | /// @param blockNumber The block number to get the voting power at 47 | function _getPastTotalSupply(uint256 blockNumber) 48 | internal 49 | view 50 | virtual 51 | override 52 | returns (uint256) 53 | { 54 | return staking.getPastTotalSupply(blockNumber); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /contracts/governance/TokenholderGovernor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | import "./BaseTokenholderGovernor.sol"; 19 | 20 | contract TokenholderGovernor is BaseTokenholderGovernor { 21 | uint256 private constant INITIAL_QUORUM_NUMERATOR = 150; // Defined in basis points, i.e., 1.5% 22 | uint256 private constant INITIAL_PROPOSAL_THRESHOLD_NUMERATOR = 25; // Defined in basis points, i.e., 0.25% 23 | uint256 private constant INITIAL_VOTING_DELAY = 24 | 2 days / AVERAGE_BLOCK_TIME_IN_SECONDS; 25 | uint256 private constant INITIAL_VOTING_PERIOD = 26 | 10 days / AVERAGE_BLOCK_TIME_IN_SECONDS; 27 | uint64 private constant INITIAL_VOTING_EXTENSION = 28 | uint64(2 days) / AVERAGE_BLOCK_TIME_IN_SECONDS; 29 | 30 | constructor( 31 | T _token, 32 | IVotesHistory _staking, 33 | TimelockController _timelock, 34 | address vetoer 35 | ) 36 | BaseTokenholderGovernor( 37 | _token, 38 | _staking, 39 | _timelock, 40 | vetoer, 41 | INITIAL_QUORUM_NUMERATOR, 42 | INITIAL_PROPOSAL_THRESHOLD_NUMERATOR, 43 | INITIAL_VOTING_DELAY, 44 | INITIAL_VOTING_PERIOD, 45 | INITIAL_VOTING_EXTENSION 46 | ) 47 | {} 48 | } 49 | -------------------------------------------------------------------------------- /contracts/governance/TokenholderGovernorVotes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | import "./GovernorParameters.sol"; 19 | import "./IVotesHistory.sol"; 20 | 21 | /// @title TokenholderGovernorVotes 22 | /// @notice Tokenholder DAO voting power extraction from both liquid and staked 23 | /// T token positions. 24 | abstract contract TokenholderGovernorVotes is GovernorParameters { 25 | IVotesHistory public immutable token; 26 | IVotesHistory public immutable staking; 27 | 28 | constructor(IVotesHistory tokenAddress, IVotesHistory tStakingAddress) { 29 | token = tokenAddress; 30 | staking = tStakingAddress; 31 | } 32 | 33 | /// @notice Read the voting weight from the snapshot mechanism in the token 34 | /// and staking contracts. For Tokenholder DAO, there are currently 35 | /// two voting power sources: 36 | /// - Liquid T, tracked by the T token contract 37 | /// - Stakes in the T network, tracked by the T staking contract. 38 | /// @param account Tokenholder account in the T network 39 | /// @param blockNumber The block number to get the vote balance at 40 | /// @dev See {IGovernor-getVotes} 41 | function getVotes(address account, uint256 blockNumber) 42 | public 43 | view 44 | virtual 45 | override 46 | returns (uint256) 47 | { 48 | uint256 liquidVotes = token.getPastVotes(account, blockNumber); 49 | uint256 stakedVotes = staking.getPastVotes(account, blockNumber); 50 | return liquidVotes + stakedVotes; 51 | } 52 | 53 | /// @notice Compute the total voting power for Tokenholder DAO. Note how it 54 | /// only uses the token total supply as source, as native T tokens 55 | /// that are staked continue existing, but as deposits in the 56 | /// staking contract. 57 | /// @param blockNumber The block number to get the vote power at 58 | function _getPastTotalSupply(uint256 blockNumber) 59 | internal 60 | view 61 | virtual 62 | override 63 | returns (uint256) 64 | { 65 | return token.getPastTotalSupply(blockNumber); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/staking/IApplication.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity ^0.8.9; 17 | 18 | /// @title Application interface for Threshold Network applications 19 | /// @notice Generic interface for an application. Application is an external 20 | /// smart contract or a set of smart contracts utilizing functionalities 21 | /// offered by Threshold Network. Applications authorized for the given 22 | /// staking provider are eligible to slash the stake delegated to that 23 | /// staking provider. 24 | interface IApplication { 25 | /// @dev Event emitted by `withdrawRewards` function. 26 | event RewardsWithdrawn(address indexed stakingProvider, uint96 amount); 27 | 28 | /// @notice Withdraws application rewards for the given staking provider. 29 | /// Rewards are withdrawn to the staking provider's beneficiary 30 | /// address set in the staking contract. 31 | /// @dev Emits `RewardsWithdrawn` event. 32 | function withdrawRewards(address stakingProvider) external; 33 | 34 | /// @notice Used by T staking contract to inform the application that the 35 | /// authorized amount for the given staking provider increased. 36 | /// The application may do any necessary housekeeping. The 37 | /// application must revert the transaction in case the 38 | /// authorization is below the minimum required. 39 | function authorizationIncreased( 40 | address stakingProvider, 41 | uint96 fromAmount, 42 | uint96 toAmount 43 | ) external; 44 | 45 | /// @notice Used by T staking contract to inform the application that the 46 | /// authorization decrease for the given staking provider has been 47 | /// requested. The application should mark the authorization as 48 | /// pending decrease and respond to the staking contract with 49 | /// `approveAuthorizationDecrease` at its discretion. It may 50 | /// happen right away but it also may happen several months later. 51 | /// If there is already a pending authorization decrease request 52 | /// for the application, and the application does not agree for 53 | /// overwriting it, the function should revert. 54 | function authorizationDecreaseRequested( 55 | address stakingProvider, 56 | uint96 fromAmount, 57 | uint96 toAmount 58 | ) external; 59 | 60 | /// @notice Used by T staking contract to inform the application the 61 | /// authorization has been decreased for the given staking provider 62 | /// involuntarily, as a result of slashing. Lets the application to 63 | /// do any housekeeping neccessary. Called with 250k gas limit and 64 | /// does not revert the transaction if 65 | /// `involuntaryAuthorizationDecrease` call failed. 66 | function involuntaryAuthorizationDecrease( 67 | address stakingProvider, 68 | uint96 fromAmount, 69 | uint96 toAmount 70 | ) external; 71 | 72 | /// @notice Returns the amount of application rewards available for 73 | /// withdrawal for the given staking provider. 74 | function availableRewards(address stakingProvider) 75 | external 76 | view 77 | returns (uint96); 78 | 79 | /// @notice The minimum authorization amount required for the staking 80 | /// provider so that they can participate in the application. 81 | function minimumAuthorization() external view returns (uint96); 82 | } 83 | -------------------------------------------------------------------------------- /contracts/staking/IApplicationWithDecreaseDelay.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity ^0.8.9; 17 | 18 | import "./IApplication.sol"; 19 | 20 | /// @title Interface for Threshold Network applications with delay after decrease request 21 | interface IApplicationWithDecreaseDelay is IApplication { 22 | /// @notice Approves the previously registered authorization decrease 23 | /// request. Reverts if authorization decrease delay has not passed 24 | /// yet or if the authorization decrease was not requested for the 25 | /// given staking provider. 26 | function approveAuthorizationDecrease(address stakingProvider) external; 27 | 28 | /// @notice Returns authorization-related parameters of the application. 29 | /// @dev The minimum authorization is also returned by `minimumAuthorization()` 30 | /// function, as a requirement of `IApplication` interface. 31 | /// @return _minimumAuthorization The minimum authorization amount required 32 | /// so that operator can participate in the application. 33 | /// @return authorizationDecreaseDelay Delay in seconds that needs to pass 34 | /// between the time authorization decrease is requested and the 35 | /// time that request gets approved. Protects against participants 36 | /// earning rewards and not being active in the network. 37 | /// @return authorizationDecreaseChangePeriod Authorization decrease change 38 | /// period in seconds. It is the time window, before authorization decrease 39 | /// delay ends, during which the pending authorization decrease 40 | /// request can be overwritten. 41 | /// If set to 0, pending authorization decrease request can not be 42 | /// overwritten until the entire `authorizationDecreaseDelay` ends. 43 | /// If set to a value equal to `authorizationDecreaseDelay`, request can 44 | /// always be overwritten. 45 | function authorizationParameters() 46 | external 47 | view 48 | returns ( 49 | uint96 _minimumAuthorization, 50 | uint64 authorizationDecreaseDelay, 51 | uint64 authorizationDecreaseChangePeriod 52 | ); 53 | 54 | /// @notice Returns the amount of stake that is pending authorization 55 | /// decrease for the given staking provider. If no authorization 56 | /// decrease has been requested, returns zero. 57 | function pendingAuthorizationDecrease(address _stakingProvider) 58 | external 59 | view 60 | returns (uint96); 61 | 62 | /// @notice Returns the remaining time in seconds that needs to pass before 63 | /// the requested authorization decrease can be approved. 64 | function remainingAuthorizationDecreaseDelay(address stakingProvider) 65 | external 66 | view 67 | returns (uint64); 68 | } 69 | -------------------------------------------------------------------------------- /contracts/staking/IApplicationWithOperator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity ^0.8.9; 17 | 18 | import "./IApplication.sol"; 19 | 20 | /// @title Interface for Threshold Network applications with operator role 21 | interface IApplicationWithOperator is IApplication { 22 | /// @notice Used by staking provider to set operator address that will 23 | /// operate a node. The operator address must be unique. 24 | /// Reverts if the operator is already set for the staking provider 25 | /// or if the operator address is already in use. 26 | /// @dev Depending on application the given staking provider can set operator 27 | /// address only once or multiple times. Besides that, application can decide 28 | /// if function reverts if there is a pending authorization decrease for 29 | /// the staking provider. 30 | function registerOperator(address operator) external; 31 | 32 | /// @notice Returns operator registered for the given staking provider. 33 | function stakingProviderToOperator(address stakingProvider) 34 | external 35 | view 36 | returns (address); 37 | 38 | /// @notice Returns staking provider of the given operator. 39 | function operatorToStakingProvider(address operator) 40 | external 41 | view 42 | returns (address); 43 | } 44 | -------------------------------------------------------------------------------- /contracts/staking/IStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity ^0.8.9; 17 | 18 | /// @title Interface of Threshold Network staking contract 19 | /// @notice The staking contract enables T owners to have their wallets offline 20 | /// and their stake managed by staking providers on their behalf. 21 | /// The staking contract does not define operator role. The operator 22 | /// responsible for running off-chain client software is appointed by 23 | /// the staking provider in the particular application utilizing the 24 | /// staking contract. All off-chain client software should be able 25 | /// to run without exposing operator's or staking provider’s private 26 | /// key and should not require any owner’s keys at all. The stake 27 | /// delegation optimizes the network throughput without compromising the 28 | /// security of the owners’ stake. 29 | interface IStaking { 30 | // 31 | // 32 | // Delegating a stake 33 | // 34 | // 35 | 36 | /// @notice Allows the Governance to set the minimum required stake amount. 37 | /// This amount is required to protect against griefing the staking 38 | /// contract and individual applications are allowed to require 39 | /// higher minimum stakes if necessary. 40 | function setMinimumStakeAmount(uint96 amount) external; 41 | 42 | // 43 | // 44 | // Authorizing an application 45 | // 46 | // 47 | 48 | /// @notice Requests decrease of the authorization for the given staking 49 | /// provider on the given application by the provided amount. 50 | /// It may not change the authorized amount immediatelly. When 51 | /// it happens depends on the application. Can only be called by the 52 | /// given staking provider’s authorizer. Overwrites pending 53 | /// authorization decrease for the given staking provider and 54 | /// application if the application agrees for that. If the 55 | /// application does not agree for overwriting, the function 56 | /// reverts. 57 | /// @dev Calls `authorizationDecreaseRequested(address stakingProvider, uint256 amount)` 58 | /// on the given application. See `IApplication`. 59 | function requestAuthorizationDecrease( 60 | address stakingProvider, 61 | address application, 62 | uint96 amount 63 | ) external; 64 | 65 | /// @notice Called by the application at its discretion to approve the 66 | /// previously requested authorization decrease request. Can only be 67 | /// called by the application that was previously requested to 68 | /// decrease the authorization for that staking provider. 69 | /// Returns resulting authorized amount for the application. 70 | function approveAuthorizationDecrease(address stakingProvider) 71 | external 72 | returns (uint96); 73 | 74 | /// @notice Decreases the authorization for the given `stakingProvider` on 75 | /// the given disabled `application`, for all authorized amount. 76 | /// Can be called by anyone. 77 | function forceDecreaseAuthorization( 78 | address stakingProvider, 79 | address application 80 | ) external; 81 | 82 | /// @notice Pauses the given application’s eligibility to slash stakes. 83 | /// Besides that stakers can't change authorization to the application. 84 | /// Can be called only by the Panic Button of the particular 85 | /// application. The paused application can not slash stakes until 86 | /// it is approved again by the Governance using `approveApplication` 87 | /// function. Should be used only in case of an emergency. 88 | function pauseApplication(address application) external; 89 | 90 | /// @notice Disables the given application. The disabled application can't 91 | /// slash stakers. Also stakers can't increase authorization to that 92 | /// application but can decrease without waiting by calling 93 | /// `requestAuthorizationDecrease` at any moment. Can be called only 94 | /// by the governance. The disabled application can't be approved 95 | /// again. Should be used only in case of an emergency. 96 | function disableApplication(address application) external; 97 | 98 | /// @notice Sets the Panic Button role for the given application to the 99 | /// provided address. Can only be called by the Governance. If the 100 | /// Panic Button for the given application should be disabled, the 101 | /// role address should be set to 0x0 address. 102 | function setPanicButton(address application, address panicButton) external; 103 | 104 | /// @notice Sets the maximum number of applications one staking provider can 105 | /// have authorized. Used to protect against DoSing slashing queue. 106 | /// Can only be called by the Governance. 107 | function setAuthorizationCeiling(uint256 ceiling) external; 108 | 109 | // 110 | // 111 | // Undelegating a stake (unstaking) 112 | // 113 | // 114 | 115 | /// @notice Reduces the T stake amount by the provided amount and 116 | /// withdraws T to the owner. Reverts if there is at least one 117 | /// authorization higher than the remaining T stake or 118 | /// if the unstake amount is higher than the T stake amount. 119 | /// Can be called only by the delegation owner or the staking 120 | /// provider. 121 | function unstakeT(address stakingProvider, uint96 amount) external; 122 | 123 | // 124 | // 125 | // Keeping information in sync 126 | // 127 | // 128 | 129 | /// @notice Withdraw some amount of T tokens from notifiers treasury. 130 | /// Can only be called by the governance. 131 | function withdrawNotificationReward(address recipient, uint96 amount) 132 | external; 133 | 134 | /// @notice Adds staking providers to the slashing queue along with the 135 | /// amount that should be slashed from each one of them. Can only be 136 | /// called by application authorized for all staking providers in 137 | /// the array. 138 | function slash(uint96 amount, address[] memory stakingProviders) external; 139 | 140 | /// @notice Adds staking providers to the slashing queue along with the 141 | /// amount. The notifier will receive reward per each staking 142 | /// provider from notifiers treasury. Can only be called by 143 | /// application authorized for all staking providers in the array. 144 | function seize( 145 | uint96 amount, 146 | uint256 rewardMultipier, 147 | address notifier, 148 | address[] memory stakingProviders 149 | ) external; 150 | 151 | // 152 | // 153 | // Auxiliary functions 154 | // 155 | // 156 | 157 | /// @notice Returns the authorized stake amount of the staking provider for 158 | /// the application. 159 | function authorizedStake(address stakingProvider, address application) 160 | external 161 | view 162 | returns (uint96); 163 | 164 | /// @notice Returns staked amount of T for the specified staking provider. 165 | function stakeAmount(address stakingProvider) 166 | external 167 | view 168 | returns (uint96); 169 | 170 | /// @notice Returns start staking timestamp. 171 | /// @dev This value is set at most once. 172 | function getStartStakingTimestamp(address stakingProvider) 173 | external 174 | view 175 | returns (uint256); 176 | 177 | /// @notice Gets the stake owner, the beneficiary and the authorizer 178 | /// for the specified staking provider address. 179 | /// @return owner Stake owner address. 180 | /// @return beneficiary Beneficiary address. 181 | /// @return authorizer Authorizer address. 182 | function rolesOf(address stakingProvider) 183 | external 184 | view 185 | returns ( 186 | address owner, 187 | address payable beneficiary, 188 | address authorizer 189 | ); 190 | 191 | /// @notice Returns length of application array 192 | function getApplicationsLength() external view returns (uint256); 193 | 194 | /// @notice Returns the maximum application authorization 195 | function getMaxAuthorization(address stakingProvider) 196 | external 197 | view 198 | returns (uint96); 199 | 200 | /// @notice Returns available amount to authorize for the specified application 201 | function getAvailableToAuthorize( 202 | address stakingProvider, 203 | address application 204 | ) external view returns (uint96); 205 | } 206 | -------------------------------------------------------------------------------- /contracts/test/SimplePREApplicationStub.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-or-later 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract SimplePREApplicationStub { 6 | struct StakingProviderInfo { 7 | address operator; 8 | bool operatorConfirmed; 9 | uint256 operatorStartTimestamp; 10 | } 11 | 12 | mapping(address => StakingProviderInfo) public stakingProviderInfo; 13 | 14 | event OperatorBonded( 15 | address indexed stakingProvider, 16 | address indexed operator, 17 | uint256 startTimestamp 18 | ); 19 | 20 | event OperatorConfirmed( 21 | address indexed stakingProvider, 22 | address indexed operator 23 | ); 24 | 25 | function bondOperator(address _stakingProvider, address _operator) 26 | external 27 | { 28 | StakingProviderInfo storage info = stakingProviderInfo[ 29 | _stakingProvider 30 | ]; 31 | require( 32 | _operator != info.operator, 33 | "Specified operator is already bonded with this provider" 34 | ); 35 | // Bond new operator (or unbond if _operator == address(0)) 36 | info.operator = _operator; 37 | /* solhint-disable-next-line not-rely-on-time */ 38 | info.operatorStartTimestamp = block.timestamp; 39 | info.operatorConfirmed = false; 40 | /* solhint-disable-next-line not-rely-on-time */ 41 | emit OperatorBonded(_stakingProvider, _operator, block.timestamp); 42 | } 43 | 44 | function confirmOperatorAddress(address stakingProvider) external { 45 | StakingProviderInfo storage info = stakingProviderInfo[stakingProvider]; 46 | require( 47 | !info.operatorConfirmed, 48 | "Operator address is already confirmed" 49 | ); 50 | info.operatorConfirmed = true; 51 | emit OperatorConfirmed(stakingProvider, msg.sender); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /contracts/test/TestGovernorTestSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity 0.8.9; 4 | 5 | import "../governance/GovernorParameters.sol"; 6 | import "../governance/StakerGovernor.sol"; 7 | import "../governance/TokenholderGovernor.sol"; 8 | import "../token/T.sol"; 9 | 10 | contract TestTokenholderGovernorStub { 11 | string public name = "TokenholderGovernor"; 12 | address public timelock = address(0x42); 13 | } 14 | 15 | contract TestTokenholderGovernorStubV2 { 16 | string public name = "TokenholderGovernor"; 17 | address public timelock; 18 | 19 | constructor(address _timelock) { 20 | timelock = _timelock; 21 | } 22 | } 23 | 24 | contract TestStakerGovernor is StakerGovernor { 25 | constructor( 26 | IVotesHistory tStaking, 27 | TokenholderGovernor tokenholderGov, 28 | address vetoer 29 | ) 30 | StakerGovernor( 31 | tStaking, 32 | TimelockController(payable(0)), 33 | tokenholderGov, 34 | vetoer 35 | ) 36 | {} 37 | 38 | function executor() external view returns (address) { 39 | return _executor(); 40 | } 41 | } 42 | 43 | contract TestTokenholderGovernor is BaseTokenholderGovernor { 44 | uint256 private constant INITIAL_QUORUM_NUMERATOR = 150; // Defined in basis points, i.e., 1.5% 45 | uint256 private constant INITIAL_PROPOSAL_THRESHOLD_NUMERATOR = 25; // Defined in basis points, i.e., 0.25% 46 | uint256 private constant INITIAL_VOTING_DELAY = 2; 47 | uint256 private constant INITIAL_VOTING_PERIOD = 8; 48 | uint64 private constant INITIAL_VOTING_EXTENSION = 4; 49 | 50 | constructor( 51 | T _tToken, 52 | IVotesHistory _tStaking, 53 | TimelockController _timelock, 54 | address _vetoer 55 | ) 56 | BaseTokenholderGovernor( 57 | _tToken, 58 | _tStaking, 59 | _timelock, 60 | _vetoer, 61 | INITIAL_QUORUM_NUMERATOR, 62 | INITIAL_PROPOSAL_THRESHOLD_NUMERATOR, 63 | INITIAL_VOTING_DELAY, 64 | INITIAL_VOTING_PERIOD, 65 | INITIAL_VOTING_EXTENSION 66 | ) 67 | {} 68 | } 69 | 70 | contract TestGovernorParameters is GovernorParameters { 71 | address internal executor; 72 | 73 | constructor(address executorAddress) 74 | Governor("TestGovernorParameters") 75 | GovernorParameters(10, 20, 30, 40) 76 | { 77 | executor = executorAddress; 78 | } 79 | 80 | function getVotes(address account, uint256 blockNumber) 81 | public 82 | view 83 | virtual 84 | override 85 | returns (uint256) 86 | {} 87 | 88 | function getPastTotalSupply(uint256 blockNumber) 89 | public 90 | view 91 | returns (uint256) 92 | {} 93 | 94 | function hasVoted(uint256 proposalId, address account) 95 | public 96 | view 97 | virtual 98 | override 99 | returns (bool) 100 | {} 101 | 102 | // solhint-disable-next-line func-name-mixedcase 103 | function COUNTING_MODE() 104 | public 105 | pure 106 | virtual 107 | override 108 | returns (string memory) 109 | {} 110 | 111 | function _countVote( 112 | uint256 proposalId, 113 | address account, 114 | uint8 support, 115 | uint256 weight 116 | ) internal virtual override {} 117 | 118 | function _quorumReached(uint256 proposalId) 119 | internal 120 | view 121 | virtual 122 | override 123 | returns (bool) 124 | {} 125 | 126 | function _voteSucceeded(uint256 proposalId) 127 | internal 128 | view 129 | virtual 130 | override 131 | returns (bool) 132 | {} 133 | 134 | function _getPastTotalSupply(uint256 blockNumber) 135 | internal 136 | view 137 | virtual 138 | override 139 | returns (uint256) 140 | {} 141 | 142 | function _executor() internal view virtual override returns (address) { 143 | return executor; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /contracts/test/TestStakerGovernorVotes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity 0.8.9; 4 | 5 | import "../governance/StakerGovernorVotes.sol"; 6 | 7 | contract TestStakerGovernorVotes is StakerGovernorVotes { 8 | constructor(IVotesHistory _tStaking) 9 | Governor("TestStakerGovernor") 10 | GovernorParameters(125, 75, 12, 34) 11 | StakerGovernorVotes(_tStaking) 12 | {} 13 | 14 | function getPastTotalSupply(uint256 blockNumber) 15 | public 16 | view 17 | returns (uint256) 18 | { 19 | return _getPastTotalSupply(blockNumber); 20 | } 21 | 22 | function hasVoted(uint256 proposalId, address account) 23 | public 24 | view 25 | virtual 26 | override 27 | returns (bool) 28 | {} 29 | 30 | // solhint-disable-next-line func-name-mixedcase 31 | function COUNTING_MODE() 32 | public 33 | pure 34 | virtual 35 | override 36 | returns (string memory) 37 | {} 38 | 39 | function _countVote( 40 | uint256 proposalId, 41 | address account, 42 | uint8 support, 43 | uint256 weight 44 | ) internal virtual override {} 45 | 46 | function _quorumReached(uint256 proposalId) 47 | internal 48 | view 49 | virtual 50 | override 51 | returns (bool) 52 | {} 53 | 54 | function _voteSucceeded(uint256 proposalId) 55 | internal 56 | view 57 | virtual 58 | override 59 | returns (bool) 60 | {} 61 | } 62 | -------------------------------------------------------------------------------- /contracts/test/TestStakingCheckpoints.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity 0.8.9; 4 | 5 | import "../governance/Checkpoints.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | contract TestStakingCheckpoints is Checkpoints { 9 | mapping(address => uint256) public stake; 10 | 11 | /// @notice T token contract. 12 | IERC20 public immutable tToken; 13 | 14 | constructor(IERC20 _tToken) { 15 | tToken = _tToken; 16 | } 17 | 18 | function deposit(uint256 amount) public { 19 | stake[msg.sender] += amount; 20 | writeCheckpoint(_checkpoints[msg.sender], add, amount); 21 | writeCheckpoint(_totalSupplyCheckpoints, add, amount); 22 | 23 | tToken.transferFrom(msg.sender, address(this), amount); 24 | } 25 | 26 | function withdraw(uint256 amount) public { 27 | require(stake[msg.sender] >= amount, "Not enough stake to withdraw"); 28 | stake[msg.sender] -= amount; 29 | writeCheckpoint(_checkpoints[msg.sender], subtract, amount); 30 | writeCheckpoint(_totalSupplyCheckpoints, subtract, amount); 31 | 32 | tToken.transfer(msg.sender, amount); 33 | } 34 | 35 | function delegate(address delegator, address delegatee) 36 | internal 37 | virtual 38 | override 39 | {} 40 | } 41 | -------------------------------------------------------------------------------- /contracts/test/TestToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity 0.8.9; 4 | 5 | import "@thesis/solidity-contracts/contracts/token/ERC20WithPermit.sol"; 6 | 7 | contract TestToken is ERC20WithPermit { 8 | constructor() ERC20WithPermit("Test Token", "TEST") {} 9 | } 10 | -------------------------------------------------------------------------------- /contracts/test/TestTokenholderGovernorVotes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity 0.8.9; 4 | 5 | import "../governance/TokenholderGovernorVotes.sol"; 6 | import "../token/T.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; 8 | 9 | contract TestTokenholderGovernorVotes is TokenholderGovernorVotes { 10 | constructor(T _tToken, IVotesHistory _tStaking) 11 | Governor("TestTokenholderGovernor") 12 | GovernorParameters(125, 75, 12, 34) 13 | TokenholderGovernorVotes(_tToken, _tStaking) 14 | {} 15 | 16 | function getPastTotalSupply(uint256 blockNumber) 17 | public 18 | view 19 | returns (uint256) 20 | { 21 | return _getPastTotalSupply(blockNumber); 22 | } 23 | 24 | function hasVoted(uint256 proposalId, address account) 25 | public 26 | view 27 | virtual 28 | override 29 | returns (bool) 30 | {} 31 | 32 | // solhint-disable-next-line func-name-mixedcase 33 | function COUNTING_MODE() 34 | public 35 | pure 36 | virtual 37 | override 38 | returns (string memory) 39 | {} 40 | 41 | function _countVote( 42 | uint256 proposalId, 43 | address account, 44 | uint8 support, 45 | uint256 weight 46 | ) internal virtual override {} 47 | 48 | function _quorumReached(uint256 proposalId) 49 | internal 50 | view 51 | virtual 52 | override 53 | returns (bool) 54 | {} 55 | 56 | function _voteSucceeded(uint256 proposalId) 57 | internal 58 | view 59 | virtual 60 | override 61 | returns (bool) 62 | {} 63 | } 64 | -------------------------------------------------------------------------------- /contracts/test/TokenStakingTestSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity 0.8.9; 4 | 5 | import "../staking/IApplication.sol"; 6 | import "../staking/TokenStaking.sol"; 7 | 8 | contract VendingMachineMock { 9 | uint256 public constant FLOATING_POINT_DIVISOR = 10**15; 10 | 11 | uint256 public immutable ratio; 12 | 13 | constructor(uint96 _wrappedTokenAllocation, uint96 _tTokenAllocation) { 14 | ratio = 15 | (FLOATING_POINT_DIVISOR * _tTokenAllocation) / 16 | _wrappedTokenAllocation; 17 | } 18 | } 19 | 20 | contract ApplicationMock is IApplication { 21 | struct StakingProviderStruct { 22 | uint96 authorized; 23 | uint96 deauthorizingTo; 24 | } 25 | 26 | TokenStaking internal immutable tokenStaking; 27 | mapping(address => StakingProviderStruct) public stakingProviders; 28 | 29 | constructor(TokenStaking _tokenStaking) { 30 | tokenStaking = _tokenStaking; 31 | } 32 | 33 | function withdrawRewards(address) external { 34 | // does nothing 35 | } 36 | 37 | function authorizationIncreased( 38 | address stakingProvider, 39 | uint96, 40 | uint96 toAmount 41 | ) external override { 42 | stakingProviders[stakingProvider].authorized = toAmount; 43 | } 44 | 45 | function authorizationDecreaseRequested( 46 | address stakingProvider, 47 | uint96, 48 | uint96 toAmount 49 | ) external override { 50 | stakingProviders[stakingProvider].deauthorizingTo = toAmount; 51 | } 52 | 53 | function approveAuthorizationDecrease(address stakingProvider) external { 54 | StakingProviderStruct storage stakingProviderStruct = stakingProviders[ 55 | stakingProvider 56 | ]; 57 | stakingProviderStruct.authorized = tokenStaking 58 | .approveAuthorizationDecrease(stakingProvider); 59 | } 60 | 61 | function availableRewards(address) external pure returns (uint96) { 62 | return 0; 63 | } 64 | 65 | function minimumAuthorization() external pure returns (uint96) { 66 | return 0; 67 | } 68 | 69 | function involuntaryAuthorizationDecrease( 70 | address stakingProvider, 71 | uint96, 72 | uint96 toAmount 73 | ) public virtual override { 74 | StakingProviderStruct storage stakingProviderStruct = stakingProviders[ 75 | stakingProvider 76 | ]; 77 | require( 78 | toAmount != stakingProviderStruct.authorized, 79 | "Nothing to decrease" 80 | ); 81 | stakingProviderStruct.authorized = toAmount; 82 | if (stakingProviderStruct.deauthorizingTo > toAmount) { 83 | stakingProviderStruct.deauthorizingTo = toAmount; 84 | } 85 | } 86 | } 87 | 88 | contract BrokenApplicationMock is ApplicationMock { 89 | constructor(TokenStaking _tokenStaking) ApplicationMock(_tokenStaking) {} 90 | 91 | function involuntaryAuthorizationDecrease( 92 | address, 93 | uint96, 94 | uint96 95 | ) public pure override { 96 | revert("Broken application"); 97 | } 98 | } 99 | 100 | contract ExpensiveApplicationMock is ApplicationMock { 101 | uint256[] private dummy; 102 | 103 | constructor(TokenStaking _tokenStaking) ApplicationMock(_tokenStaking) {} 104 | 105 | function involuntaryAuthorizationDecrease( 106 | address stakingProvider, 107 | uint96 fromAmount, 108 | uint96 toAmount 109 | ) public override { 110 | super.involuntaryAuthorizationDecrease( 111 | stakingProvider, 112 | fromAmount, 113 | toAmount 114 | ); 115 | for (uint256 i = 0; i < 12; i++) { 116 | dummy.push(i); 117 | } 118 | } 119 | } 120 | 121 | contract ManagedGrantMock { 122 | address public grantee; 123 | 124 | //slither-disable-next-line missing-zero-check 125 | function setGrantee(address _grantee) external { 126 | grantee = _grantee; 127 | } 128 | } 129 | 130 | contract ExtendedTokenStaking is TokenStaking { 131 | using SafeTUpgradeable for T; 132 | 133 | /// @custom:oz-upgrades-unsafe-allow constructor 134 | constructor(T _token) TokenStaking(_token) {} 135 | 136 | function cleanAuthorizedApplications( 137 | address stakingProvider, 138 | uint256 numberToDelete 139 | ) external { 140 | StakingProviderInfo storage stakingProviderStruct = stakingProviders[ 141 | stakingProvider 142 | ]; 143 | cleanAuthorizedApplications(stakingProviderStruct, numberToDelete); 144 | } 145 | 146 | function setAuthorization( 147 | address stakingProvider, 148 | address application, 149 | uint96 amount 150 | ) external { 151 | stakingProviders[stakingProvider] 152 | .authorizations[application] 153 | .authorized = amount; 154 | } 155 | 156 | function setAuthorizedApplications( 157 | address stakingProvider, 158 | address[] memory _applications 159 | ) external { 160 | stakingProviders[stakingProvider] 161 | .authorizedApplications = _applications; 162 | } 163 | 164 | /// @notice Creates a delegation with `msg.sender` owner with the given 165 | /// staking provider, beneficiary, and authorizer. Transfers the 166 | /// given amount of T to the staking contract. 167 | /// @dev The owner of the delegation needs to have the amount approved to 168 | /// transfer to the staking contract. 169 | function stake( 170 | address stakingProvider, 171 | address payable beneficiary, 172 | address authorizer, 173 | uint96 amount 174 | ) external { 175 | require( 176 | stakingProvider != address(0) && 177 | beneficiary != address(0) && 178 | authorizer != address(0), 179 | "Parameters must be specified" 180 | ); 181 | StakingProviderInfo storage stakingProviderStruct = stakingProviders[ 182 | stakingProvider 183 | ]; 184 | require( 185 | stakingProviderStruct.owner == address(0), 186 | "Provider is already in use" 187 | ); 188 | require( 189 | amount > 0 && amount >= minTStakeAmount, 190 | "Amount is less than minimum" 191 | ); 192 | stakingProviderStruct.owner = msg.sender; 193 | stakingProviderStruct.authorizer = authorizer; 194 | stakingProviderStruct.beneficiary = beneficiary; 195 | 196 | stakingProviderStruct.tStake = amount; 197 | /* solhint-disable-next-line not-rely-on-time */ 198 | stakingProviderStruct.startStakingTimestamp = block.timestamp; 199 | 200 | increaseStakeCheckpoint(stakingProvider, amount); 201 | 202 | token.safeTransferFrom(msg.sender, address(this), amount); 203 | } 204 | 205 | /// @notice Increases the authorization of the given staking provider for 206 | /// the given application by the given amount. Can only be called by 207 | /// the given staking provider’s authorizer. 208 | /// @dev Calls `authorizationIncreased` callback on the given application to 209 | /// notify the application about authorization change. 210 | /// See `IApplication`. 211 | function increaseAuthorization( 212 | address stakingProvider, 213 | address application, 214 | uint96 amount 215 | ) external onlyAuthorizerOf(stakingProvider) { 216 | require(amount > 0, "Parameters must be specified"); 217 | ApplicationInfo storage applicationStruct = applicationInfo[ 218 | application 219 | ]; 220 | require( 221 | applicationStruct.status == ApplicationStatus.APPROVED, 222 | "Application is not approved" 223 | ); 224 | 225 | StakingProviderInfo storage stakingProviderStruct = stakingProviders[ 226 | stakingProvider 227 | ]; 228 | AppAuthorization storage authorization = stakingProviderStruct 229 | .authorizations[application]; 230 | uint96 fromAmount = authorization.authorized; 231 | if (fromAmount == 0) { 232 | require( 233 | authorizationCeiling == 0 || 234 | stakingProviderStruct.authorizedApplications.length < 235 | authorizationCeiling, 236 | "Too many applications" 237 | ); 238 | stakingProviderStruct.authorizedApplications.push(application); 239 | } 240 | 241 | uint96 availableTValue = getAvailableToAuthorize( 242 | stakingProvider, 243 | application 244 | ); 245 | require(availableTValue >= amount, "Not enough stake to authorize"); 246 | authorization.authorized += amount; 247 | IApplication(application).authorizationIncreased( 248 | stakingProvider, 249 | fromAmount, 250 | authorization.authorized 251 | ); 252 | } 253 | 254 | /// @notice Transfer some amount of T tokens as reward for notifications 255 | /// of misbehaviour 256 | function pushNotificationReward(uint96 reward) external { 257 | require(reward > 0, "Parameters must be specified"); 258 | notifiersTreasury += reward; 259 | token.safeTransferFrom(msg.sender, address(this), reward); 260 | } 261 | 262 | /// @notice Allows the Governance to approve the particular application 263 | /// before individual stake authorizers are able to authorize it. 264 | function approveApplication(address application) external { 265 | require(application != address(0), "Parameters must be specified"); 266 | ApplicationInfo storage info = applicationInfo[application]; 267 | require( 268 | info.status == ApplicationStatus.NOT_APPROVED || 269 | info.status == ApplicationStatus.PAUSED, 270 | "Can't approve application" 271 | ); 272 | 273 | if (info.status == ApplicationStatus.NOT_APPROVED) { 274 | applications.push(application); 275 | } 276 | info.status = ApplicationStatus.APPROVED; 277 | emit ApplicationStatusChanged(application, ApplicationStatus.APPROVED); 278 | } 279 | 280 | function legacyRequestAuthorizationDecrease(address stakingProvider) 281 | external 282 | { 283 | StakingProviderInfo storage stakingProviderStruct = stakingProviders[ 284 | stakingProvider 285 | ]; 286 | uint96 deauthorizing = 0; 287 | for ( 288 | uint256 i = 0; 289 | i < stakingProviderStruct.authorizedApplications.length; 290 | i++ 291 | ) { 292 | address application = stakingProviderStruct.authorizedApplications[ 293 | i 294 | ]; 295 | uint96 authorized = stakingProviderStruct 296 | .authorizations[application] 297 | .authorized; 298 | if (authorized > 0) { 299 | legacyRequestAuthorizationDecrease( 300 | stakingProvider, 301 | application, 302 | authorized 303 | ); 304 | deauthorizing += authorized; 305 | } 306 | } 307 | 308 | require(deauthorizing > 0, "Nothing was authorized"); 309 | } 310 | 311 | function getAuthorizedApplications(address stakingProvider) 312 | external 313 | view 314 | returns (address[] memory) 315 | { 316 | return stakingProviders[stakingProvider].authorizedApplications; 317 | } 318 | 319 | function getDeauthorizingAmount( 320 | address stakingProvider, 321 | address application 322 | ) external view returns (uint96) { 323 | return 324 | stakingProviders[stakingProvider] 325 | .authorizations[application] 326 | .deauthorizing; 327 | } 328 | 329 | function legacyRequestAuthorizationDecrease( 330 | address stakingProvider, 331 | address application, 332 | uint96 amount 333 | ) public { 334 | ApplicationInfo storage applicationStruct = applicationInfo[ 335 | application 336 | ]; 337 | require( 338 | applicationStruct.status == ApplicationStatus.APPROVED, 339 | "Application is not approved" 340 | ); 341 | 342 | require(amount > 0, "Parameters must be specified"); 343 | 344 | AppAuthorization storage authorization = stakingProviders[ 345 | stakingProvider 346 | ].authorizations[application]; 347 | require( 348 | authorization.authorized >= amount, 349 | "Amount exceeds authorized" 350 | ); 351 | 352 | authorization.deauthorizing = amount; 353 | uint96 deauthorizingTo = authorization.authorized - amount; 354 | emit AuthorizationDecreaseRequested( 355 | stakingProvider, 356 | application, 357 | authorization.authorized, 358 | deauthorizingTo 359 | ); 360 | IApplication(application).authorizationDecreaseRequested( 361 | stakingProvider, 362 | authorization.authorized, 363 | deauthorizingTo 364 | ); 365 | } 366 | 367 | /// @notice Creates new checkpoints due to an increment of a stakers' stake 368 | /// @param _delegator Address of the staking provider acting as delegator 369 | /// @param _amount Amount of T to increment 370 | function increaseStakeCheckpoint(address _delegator, uint96 _amount) 371 | internal 372 | { 373 | newStakeCheckpoint(_delegator, _amount, true); 374 | } 375 | 376 | function skipApplication(address) internal pure override returns (bool) { 377 | return false; 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /contracts/test/UpgradesTestSet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | pragma solidity 0.8.9; 4 | 5 | contract SimpleStorage { 6 | /// @custom:oz-upgrades-unsafe-allow state-variable-immutable 7 | uint256 public immutable implementationVersion; 8 | uint256 public storedValue; 9 | 10 | /// @custom:oz-upgrades-unsafe-allow constructor 11 | constructor(uint256 _version) { 12 | implementationVersion = _version; 13 | } 14 | 15 | function initialize(uint256 _value) external { 16 | storedValue = _value; 17 | } 18 | 19 | function setValue(uint256 _value) external { 20 | storedValue = _value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contracts/token/T.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | import "../governance/Checkpoints.sol"; 19 | import "@openzeppelin/contracts/utils/math/SafeCast.sol"; 20 | import "@thesis/solidity-contracts/contracts/token/ERC20WithPermit.sol"; 21 | import "@thesis/solidity-contracts/contracts/token/MisfundRecovery.sol"; 22 | 23 | /// @title T token 24 | /// @notice Threshold Network T token 25 | /// @dev By default, token balance does not account for voting power. 26 | /// This makes transfers cheaper. The downside is that it requires users 27 | /// to delegate to themselves to activate checkpoints and have their 28 | /// voting power tracked. 29 | contract T is ERC20WithPermit, MisfundRecovery, Checkpoints { 30 | /// @notice The EIP-712 typehash for the delegation struct used by 31 | /// `delegateBySig`. 32 | bytes32 public constant DELEGATION_TYPEHASH = 33 | keccak256( 34 | "Delegation(address delegatee,uint256 nonce,uint256 deadline)" 35 | ); 36 | 37 | constructor() ERC20WithPermit("Threshold Network Token", "T") {} 38 | 39 | /// @notice Delegates votes from signatory to `delegatee` 40 | /// @param delegatee The address to delegate votes to 41 | /// @param deadline The time at which to expire the signature 42 | /// @param v The recovery byte of the signature 43 | /// @param r Half of the ECDSA signature pair 44 | /// @param s Half of the ECDSA signature pair 45 | function delegateBySig( 46 | address signatory, 47 | address delegatee, 48 | uint256 deadline, 49 | uint8 v, 50 | bytes32 r, 51 | bytes32 s 52 | ) external { 53 | /* solhint-disable-next-line not-rely-on-time */ 54 | require(deadline >= block.timestamp, "Delegation expired"); 55 | 56 | // Validate `s` and `v` values for a malleability concern described in EIP2. 57 | // Only signatures with `s` value in the lower half of the secp256k1 58 | // curve's order and `v` value of 27 or 28 are considered valid. 59 | require( 60 | uint256(s) <= 61 | 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, 62 | "Invalid signature 's' value" 63 | ); 64 | require(v == 27 || v == 28, "Invalid signature 'v' value"); 65 | 66 | bytes32 digest = keccak256( 67 | abi.encodePacked( 68 | "\x19\x01", 69 | DOMAIN_SEPARATOR(), 70 | keccak256( 71 | abi.encode( 72 | DELEGATION_TYPEHASH, 73 | delegatee, 74 | nonce[signatory]++, 75 | deadline 76 | ) 77 | ) 78 | ) 79 | ); 80 | 81 | address recoveredAddress = ecrecover(digest, v, r, s); 82 | require( 83 | recoveredAddress != address(0) && recoveredAddress == signatory, 84 | "Invalid signature" 85 | ); 86 | 87 | return delegate(signatory, delegatee); 88 | } 89 | 90 | /// @notice Delegate votes from `msg.sender` to `delegatee`. 91 | /// @param delegatee The address to delegate votes to 92 | function delegate(address delegatee) public virtual { 93 | return delegate(msg.sender, delegatee); 94 | } 95 | 96 | // slither-disable-next-line dead-code 97 | function beforeTokenTransfer( 98 | address from, 99 | address to, 100 | uint256 amount 101 | ) internal override { 102 | uint96 safeAmount = SafeCast.toUint96(amount); 103 | 104 | // When minting: 105 | if (from == address(0)) { 106 | // Does not allow to mint more than uint96 can fit. Otherwise, the 107 | // Checkpoint might not fit the balance. 108 | require( 109 | totalSupply + amount <= maxSupply(), 110 | "Maximum total supply exceeded" 111 | ); 112 | writeCheckpoint(_totalSupplyCheckpoints, add, safeAmount); 113 | } 114 | 115 | // When burning: 116 | if (to == address(0)) { 117 | writeCheckpoint(_totalSupplyCheckpoints, subtract, safeAmount); 118 | } 119 | 120 | moveVotingPower(delegates(from), delegates(to), safeAmount); 121 | } 122 | 123 | function delegate(address delegator, address delegatee) 124 | internal 125 | virtual 126 | override 127 | { 128 | address currentDelegate = delegates(delegator); 129 | uint96 delegatorBalance = SafeCast.toUint96(balanceOf[delegator]); 130 | _delegates[delegator] = delegatee; 131 | 132 | emit DelegateChanged(delegator, currentDelegate, delegatee); 133 | 134 | moveVotingPower(currentDelegate, delegatee, delegatorBalance); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /contracts/utils/SafeTUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | import "../token/T.sol"; 19 | import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 20 | import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 21 | 22 | /// @notice A wrapper around OpenZeppelin's `SafeERC20Upgradeable` but specific 23 | /// to the T token. Use this library in upgradeable contracts. If your 24 | /// contract is non-upgradeable, then the traditional `SafeERC20` works. 25 | /// The motivation is to prevent upgradeable contracts that use T from 26 | /// depending on the `Address` library, which can be problematic since 27 | /// it uses `delegatecall`, which is discouraged by OpenZeppelin for use 28 | /// in upgradeable contracts. 29 | /// @dev This implementation force-casts T to `IERC20Upgradeable` to make it 30 | /// work with `SafeERC20Upgradeable`. 31 | library SafeTUpgradeable { 32 | function safeTransfer( 33 | T token, 34 | address to, 35 | uint256 value 36 | ) internal { 37 | SafeERC20Upgradeable.safeTransfer( 38 | IERC20Upgradeable(address(token)), 39 | to, 40 | value 41 | ); 42 | } 43 | 44 | function safeTransferFrom( 45 | T token, 46 | address from, 47 | address to, 48 | uint256 value 49 | ) internal { 50 | SafeERC20Upgradeable.safeTransferFrom( 51 | IERC20Upgradeable(address(token)), 52 | from, 53 | to, 54 | value 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /contracts/vending/VendingMachine.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | // ██████████████ ▐████▌ ██████████████ 4 | // ██████████████ ▐████▌ ██████████████ 5 | // ▐████▌ ▐████▌ 6 | // ▐████▌ ▐████▌ 7 | // ██████████████ ▐████▌ ██████████████ 8 | // ██████████████ ▐████▌ ██████████████ 9 | // ▐████▌ ▐████▌ 10 | // ▐████▌ ▐████▌ 11 | // ▐████▌ ▐████▌ 12 | // ▐████▌ ▐████▌ 13 | // ▐████▌ ▐████▌ 14 | // ▐████▌ ▐████▌ 15 | 16 | pragma solidity 0.8.9; 17 | 18 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 19 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 20 | 21 | import "@thesis/solidity-contracts/contracts/token/IReceiveApproval.sol"; 22 | import "../token/T.sol"; 23 | 24 | /// @title T token vending machine 25 | /// @notice Contract implements a special update protocol to enable KEEP/NU 26 | /// token holders to wrap their tokens and obtain T tokens according 27 | /// to a fixed ratio. This will go on indefinitely and enable NU and 28 | /// KEEP token holders to join T network without needing to buy or 29 | /// sell any assets. Logistically, anyone holding NU or KEEP can wrap 30 | /// those assets in order to upgrade to T. They can also unwrap T in 31 | /// order to downgrade back to the underlying asset. There is a separate 32 | /// instance of this contract deployed for KEEP holders and a separate 33 | /// instance of this contract deployed for NU holders. 34 | contract VendingMachine is IReceiveApproval { 35 | using SafeERC20 for IERC20; 36 | using SafeERC20 for T; 37 | 38 | /// @notice Number of decimal places of precision in conversion to/from 39 | /// wrapped tokens (assuming typical ERC20 token with 18 decimals). 40 | /// This implies that amounts of wrapped tokens below this precision 41 | /// won't take part in the conversion. E.g., for a value of 3, then 42 | /// for a conversion of 1.123456789 wrapped tokens, only 1.123 is 43 | /// convertible (i.e., 3 decimal places), and 0.000456789 is left. 44 | uint256 public constant WRAPPED_TOKEN_CONVERSION_PRECISION = 3; 45 | 46 | /// @notice Divisor for precision purposes, used to represent fractions. 47 | uint256 public constant FLOATING_POINT_DIVISOR = 48 | 10**(18 - WRAPPED_TOKEN_CONVERSION_PRECISION); 49 | 50 | /// @notice The token being wrapped to T (KEEP/NU). 51 | IERC20 public immutable wrappedToken; 52 | 53 | /// @notice T token contract. 54 | T public immutable tToken; 55 | 56 | /// @notice The ratio with which T token is converted based on the provided 57 | /// token being wrapped (KEEP/NU), expressed in 1e18 precision. 58 | /// 59 | /// When wrapping: 60 | /// x [T] = amount [KEEP/NU] * ratio / FLOATING_POINT_DIVISOR 61 | /// 62 | /// When unwrapping: 63 | /// x [KEEP/NU] = amount [T] * FLOATING_POINT_DIVISOR / ratio 64 | uint256 public immutable ratio; 65 | 66 | /// @notice The total balance of wrapped tokens for the given holder 67 | /// account. Only holders that have previously wrapped KEEP/NU to T 68 | /// can unwrap, up to the amount previously wrapped. 69 | mapping(address => uint256) public wrappedBalance; 70 | 71 | event Wrapped( 72 | address indexed recipient, 73 | uint256 wrappedTokenAmount, 74 | uint256 tTokenAmount 75 | ); 76 | event Unwrapped( 77 | address indexed recipient, 78 | uint256 tTokenAmount, 79 | uint256 wrappedTokenAmount 80 | ); 81 | 82 | /// @notice Sets the reference to `wrappedToken` and `tToken`. Initializes 83 | /// conversion `ratio` between wrapped token and T based on the 84 | /// provided `_tTokenAllocation` and `_wrappedTokenAllocation`. 85 | /// @param _wrappedToken Address to ERC20 token that will be wrapped to T 86 | /// @param _tToken Address of T token 87 | /// @param _wrappedTokenAllocation The total supply of the token that will be 88 | /// wrapped to T 89 | /// @param _tTokenAllocation The allocation of T this instance of Vending 90 | /// Machine will receive 91 | /// @dev Multiplications in this contract can't overflow uint256 as we 92 | /// restrict `_wrappedTokenAllocation` and `_tTokenAllocation` to 93 | /// 96 bits and FLOATING_POINT_DIVISOR fits in less than 60 bits. 94 | constructor( 95 | IERC20 _wrappedToken, 96 | T _tToken, 97 | uint96 _wrappedTokenAllocation, 98 | uint96 _tTokenAllocation 99 | ) { 100 | wrappedToken = _wrappedToken; 101 | tToken = _tToken; 102 | ratio = 103 | (FLOATING_POINT_DIVISOR * _tTokenAllocation) / 104 | _wrappedTokenAllocation; 105 | } 106 | 107 | /// @notice Wraps up to the the given `amount` of the token (KEEP/NU) and 108 | /// releases T token proportionally to the amount being wrapped with 109 | /// respect to the wrap ratio. The token holder needs to have at 110 | /// least the given amount of the wrapped token (KEEP/NU) approved 111 | /// to transfer to the Vending Machine before calling this function. 112 | /// @param amount The amount of KEEP/NU to be wrapped 113 | function wrap(uint256 amount) external { 114 | _wrap(msg.sender, amount); 115 | } 116 | 117 | /// @notice Wraps up to the given amount of the token (KEEP/NU) and releases 118 | /// T token proportionally to the amount being wrapped with respect 119 | /// to the wrap ratio. This is a shortcut to `wrap` function that 120 | /// avoids a separate approval transaction. Only KEEP/NU token 121 | /// is allowed as a caller, so please call this function via 122 | /// token's `approveAndCall`. 123 | /// @param from Caller's address, must be the same as `wrappedToken` field 124 | /// @param amount The amount of KEEP/NU to be wrapped 125 | /// @param token Token's address, must be the same as `wrappedToken` field 126 | function receiveApproval( 127 | address from, 128 | uint256 amount, 129 | address token, 130 | bytes calldata 131 | ) external override { 132 | require( 133 | token == address(wrappedToken), 134 | "Token is not the wrapped token" 135 | ); 136 | require( 137 | msg.sender == address(wrappedToken), 138 | "Only wrapped token caller allowed" 139 | ); 140 | _wrap(from, amount); 141 | } 142 | 143 | /// @notice Unwraps up to the given `amount` of T back to the legacy token 144 | /// (KEEP/NU) according to the wrap ratio. It can only be called by 145 | /// a token holder who previously wrapped their tokens in this 146 | /// vending machine contract. The token holder can't unwrap more 147 | /// tokens than they originally wrapped. The token holder needs to 148 | /// have at least the given amount of T tokens approved to transfer 149 | /// to the Vending Machine before calling this function. 150 | /// @param amount The amount of T to unwrap back to the collateral (KEEP/NU) 151 | function unwrap(uint256 amount) external { 152 | _unwrap(msg.sender, amount); 153 | } 154 | 155 | /// @notice Returns the T token amount that's obtained from `amount` wrapped 156 | /// tokens (KEEP/NU), and the remainder that can't be upgraded. 157 | function conversionToT(uint256 amount) 158 | public 159 | view 160 | returns (uint256 tAmount, uint256 wrappedRemainder) 161 | { 162 | wrappedRemainder = amount % FLOATING_POINT_DIVISOR; 163 | uint256 convertibleAmount = amount - wrappedRemainder; 164 | tAmount = (convertibleAmount * ratio) / FLOATING_POINT_DIVISOR; 165 | } 166 | 167 | /// @notice The amount of wrapped tokens (KEEP/NU) that's obtained from 168 | /// `amount` T tokens, and the remainder that can't be downgraded. 169 | function conversionFromT(uint256 amount) 170 | public 171 | view 172 | returns (uint256 wrappedAmount, uint256 tRemainder) 173 | { 174 | tRemainder = amount % ratio; 175 | uint256 convertibleAmount = amount - tRemainder; 176 | wrappedAmount = (convertibleAmount * FLOATING_POINT_DIVISOR) / ratio; 177 | } 178 | 179 | function _wrap(address tokenHolder, uint256 wrappedTokenAmount) internal { 180 | (uint256 tTokenAmount, uint256 remainder) = conversionToT( 181 | wrappedTokenAmount 182 | ); 183 | wrappedTokenAmount -= remainder; 184 | require(wrappedTokenAmount > 0, "Disallow conversions of zero value"); 185 | emit Wrapped(tokenHolder, wrappedTokenAmount, tTokenAmount); 186 | 187 | wrappedBalance[tokenHolder] += wrappedTokenAmount; 188 | wrappedToken.safeTransferFrom( 189 | tokenHolder, 190 | address(this), 191 | wrappedTokenAmount 192 | ); 193 | tToken.safeTransfer(tokenHolder, tTokenAmount); 194 | } 195 | 196 | function _unwrap(address tokenHolder, uint256 tTokenAmount) internal { 197 | (uint256 wrappedTokenAmount, uint256 remainder) = conversionFromT( 198 | tTokenAmount 199 | ); 200 | tTokenAmount -= remainder; 201 | require(tTokenAmount > 0, "Disallow conversions of zero value"); 202 | require( 203 | wrappedBalance[tokenHolder] >= wrappedTokenAmount, 204 | "Can not unwrap more than previously wrapped" 205 | ); 206 | 207 | emit Unwrapped(tokenHolder, tTokenAmount, wrappedTokenAmount); 208 | wrappedBalance[tokenHolder] -= wrappedTokenAmount; 209 | tToken.safeTransferFrom(tokenHolder, address(this), tTokenAmount); 210 | wrappedToken.safeTransfer(tokenHolder, wrappedTokenAmount); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /deploy/00_resolve_nucypher_pre_application.ts: -------------------------------------------------------------------------------- 1 | import type { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import type { DeployFunction } from "hardhat-deploy/types" 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const { getNamedAccounts, deployments } = hre 6 | const { log } = deployments 7 | const { deployer } = await getNamedAccounts() 8 | 9 | log(`deploying SimplePREApplication stub`) 10 | 11 | await deployments.deploy("SimplePREApplication", { 12 | contract: "SimplePREApplicationStub", 13 | from: deployer, 14 | log: true, 15 | }) 16 | } 17 | 18 | export default func 19 | 20 | func.tags = ["NuCypherSimplePREApplication"] 21 | func.skip = async function (hre: HardhatRuntimeEnvironment): Promise { 22 | return hre.network.name !== "development" 23 | } 24 | -------------------------------------------------------------------------------- /deploy/00_resolve_nucypher_token.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | HardhatRuntimeEnvironment, 3 | HardhatNetworkConfig, 4 | } from "hardhat/types" 5 | import type { DeployFunction } from "hardhat-deploy/types" 6 | 7 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 8 | const { getNamedAccounts, deployments, helpers } = hre 9 | const { log } = deployments 10 | const { deployer } = await getNamedAccounts() 11 | const { execute, read } = deployments 12 | const { to1e18, from1e18 } = helpers.number 13 | 14 | const NuCypherToken = await deployments.getOrNull("NuCypherToken") 15 | 16 | if (NuCypherToken && helpers.address.isValid(NuCypherToken.address)) { 17 | log(`using existing NuCypherToken at ${NuCypherToken.address}`) 18 | 19 | // Save deployment artifact of external contract to include it in the package. 20 | await deployments.save("NuCypherToken", NuCypherToken) 21 | } else if ( 22 | // TODO: For testnets currently we deploy a stub contract. We should consider 23 | // switching to an actual contract. 24 | hre.network.name !== "sepolia" && 25 | (!hre.network.tags.allowStubs || 26 | (hre.network.config as HardhatNetworkConfig)?.forking?.enabled) 27 | ) { 28 | throw new Error("deployed NuCypherToken contract not found") 29 | } else { 30 | log(`deploying NuCypherToken stub`) 31 | 32 | // To make a testnet deployment that is closer to the real setting we want 33 | // to mint 1.34B NU tokens. The NuCyppher team deployed the token with a 34 | // 3.8B supply, but most of it is on escrow in the `StakingEscrow` contract. 35 | // The actual issued supply to date is around 1.34B, and it will still be 36 | // around that number when the merge happens. 37 | const NU_SUPPLY = to1e18("1340000000") // 1.34B NU 38 | 39 | await deployments.deploy("NuCypherToken", { 40 | contract: "TestToken", 41 | from: deployer, 42 | log: true, 43 | }) 44 | 45 | await execute( 46 | "NuCypherToken", 47 | { from: deployer }, 48 | "mint", 49 | deployer, 50 | NU_SUPPLY 51 | ) 52 | 53 | log(`minted ${from1e18(await read("NuCypherToken", "totalSupply"))} NU`) 54 | } 55 | } 56 | 57 | export default func 58 | 59 | func.tags = ["NuCypherToken"] 60 | -------------------------------------------------------------------------------- /deploy/01_deploy_t.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const { getNamedAccounts, deployments } = hre 6 | const { deployer } = await getNamedAccounts() 7 | 8 | const T = await deployments.deploy("T", { 9 | from: deployer, 10 | log: true, 11 | }) 12 | 13 | if (hre.network.tags.tenderly) { 14 | await hre.tenderly.verify({ 15 | name: "T", 16 | address: T.address, 17 | }) 18 | } 19 | } 20 | 21 | export default func 22 | 23 | func.tags = ["T"] 24 | -------------------------------------------------------------------------------- /deploy/02_mint_t.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | import type { BigNumber } from "@ethersproject/bignumber" 4 | 5 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 6 | const { getNamedAccounts, deployments, helpers } = hre 7 | const { deployer } = await getNamedAccounts() 8 | const { execute, read, log } = deployments 9 | const { to1e18, from1e18 } = helpers.number 10 | 11 | const T = await deployments.get("T") 12 | 13 | const totalSupply: BigNumber = await read("T", "totalSupply") 14 | 15 | if (!totalSupply.isZero()) { 16 | log( 17 | `T tokens were already minted, T total supply: ${from1e18(totalSupply)}` 18 | ) 19 | } else { 20 | // We're minting 10B T, which is the value of the T supply on the production 21 | // environment. 22 | const T_SUPPLY = to1e18("10000000000") // 10B T 23 | 24 | await execute("T", { from: deployer }, "mint", deployer, T_SUPPLY) 25 | 26 | log(`minted ${from1e18(await read("T", "totalSupply"))} T`) 27 | } 28 | } 29 | 30 | export default func 31 | 32 | func.tags = ["MintT"] 33 | func.dependencies = ["T"] 34 | -------------------------------------------------------------------------------- /deploy/04_deploy_vending_machine_nu.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const { getNamedAccounts, deployments, helpers } = hre 6 | const { deployer } = await getNamedAccounts() 7 | const { to1e18 } = helpers.number 8 | 9 | const NuCypherToken = await deployments.get("NuCypherToken") 10 | const T = await deployments.get("T") 11 | 12 | const NU_TOKEN_ALLOCATION = "1380688920644254727736959922" 13 | 14 | const T_ALLOCATION_NU = to1e18("4500000000") 15 | 16 | const vendingMachine = await deployments.deploy("VendingMachineNuCypher", { 17 | contract: "VendingMachine", 18 | from: deployer, 19 | args: [ 20 | NuCypherToken.address, 21 | T.address, 22 | NU_TOKEN_ALLOCATION, 23 | T_ALLOCATION_NU, 24 | ], 25 | log: true, 26 | }) 27 | 28 | if (hre.network.tags.tenderly) { 29 | await hre.tenderly.verify({ 30 | name: "VendingMachineNuCypher", 31 | address: vendingMachine.address, 32 | }) 33 | } 34 | } 35 | 36 | export default func 37 | 38 | func.tags = ["VendingMachineNuCypher"] 39 | func.dependencies = ["T", "NuCypherToken", "NuCypherStakingEscrow"] 40 | -------------------------------------------------------------------------------- /deploy/05_transfer_t.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const { getNamedAccounts, deployments, helpers } = hre 6 | const { deployer } = await getNamedAccounts() 7 | const { execute } = deployments 8 | const { to1e18, from1e18 } = helpers.number 9 | 10 | const VendingMachineNuCypher = await deployments.get("VendingMachineNuCypher") 11 | 12 | const vendingMachines = [ 13 | { 14 | tokenSymbol: "NU", 15 | vendingMachineAddress: VendingMachineNuCypher.address, 16 | }, 17 | ] 18 | 19 | // There will be 10B T minted on the production environment. The 45% of this 20 | // amount will go to the KEEP holders, 45% will go to NU holders and 10% will 21 | // be sent to the DAO treasury. 22 | const T_TO_TRANSFER = to1e18("4500000000") // 4.5B T 23 | 24 | for (const { tokenSymbol, vendingMachineAddress } of vendingMachines) { 25 | await execute( 26 | "T", 27 | { from: deployer }, 28 | "transfer", 29 | vendingMachineAddress, 30 | T_TO_TRANSFER 31 | ) 32 | 33 | console.log( 34 | `transferred ${from1e18( 35 | T_TO_TRANSFER 36 | )} T to the VendingMachine for ${tokenSymbol}` 37 | ) 38 | } 39 | } 40 | 41 | export default func 42 | 43 | func.tags = ["TransferT"] 44 | func.dependencies = ["MintT", "VendingMachineNuCypher"] 45 | -------------------------------------------------------------------------------- /deploy/07_deploy_token_staking.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | 4 | import { ethers, upgrades } from "hardhat" 5 | 6 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { getNamedAccounts, deployments } = hre 8 | const { execute, log } = deployments 9 | const { deployer } = await getNamedAccounts() 10 | 11 | const T = await deployments.get("T") 12 | 13 | const tokenStakingConstructorArgs = [T.address] 14 | const tokenStakingInitializerArgs = [] 15 | 16 | // TODO: Consider upgradable deployment also for sepolia. 17 | let tokenStakingAddress 18 | if (hre.network.name == "mainnet") { 19 | const TokenStaking = await ethers.getContractFactory("TokenStaking") 20 | 21 | const tokenStaking = await upgrades.deployProxy( 22 | TokenStaking, 23 | tokenStakingInitializerArgs, 24 | { 25 | constructorArgs: tokenStakingConstructorArgs, 26 | } 27 | ) 28 | tokenStakingAddress = tokenStaking.address 29 | log(`Deployed TokenStaking with TransparentProxy at ${tokenStakingAddress}`) 30 | 31 | const implementationInterface = tokenStaking.interface 32 | let jsonAbi = implementationInterface.format(ethers.utils.FormatTypes.json) 33 | 34 | const tokenStakingDeployment = { 35 | address: tokenStakingAddress, 36 | abi: JSON.parse(jsonAbi as string), 37 | } 38 | const fs = require("fs") 39 | fs.writeFileSync( 40 | "TokenStaking.json", 41 | JSON.stringify(tokenStakingDeployment, null, 2), 42 | "utf8", 43 | function (err) { 44 | if (err) { 45 | console.log(err) 46 | } 47 | } 48 | ) 49 | log(`Saved TokenStaking address and ABI in TokenStaking.json`) 50 | } else { 51 | const TokenStaking = await deployments.deploy("TokenStaking", { 52 | from: deployer, 53 | args: tokenStakingConstructorArgs, 54 | log: true, 55 | }) 56 | tokenStakingAddress = TokenStaking.address 57 | 58 | await execute("TokenStaking", { from: deployer }, "initialize") 59 | log("Initialized TokenStaking.") 60 | } 61 | 62 | if (hre.network.tags.tenderly) { 63 | await hre.tenderly.verify({ 64 | name: "TokenStaking", 65 | address: tokenStakingAddress, 66 | }) 67 | } 68 | } 69 | 70 | export default func 71 | 72 | func.tags = ["TokenStaking"] 73 | func.dependencies = ["T", "VendingMachineNuCypher", "MintT"] 74 | -------------------------------------------------------------------------------- /deploy/30_deploy_tokenholder_timelock.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | 4 | // FIXME: As a workaround for a bug in hardhat-gas-reporter #86 we import 5 | // ethers here instead of using the one defined in `hre`. 6 | // #86: https://github.com/cgewecke/hardhat-gas-reporter/issues/86 7 | import { ethers } from "ethers" 8 | 9 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 10 | const { getNamedAccounts, deployments } = hre 11 | const { deployer } = await getNamedAccounts() 12 | 13 | const proposers = [] 14 | const executors = [ethers.constants.AddressZero] 15 | const minDelay = 172800 // 2 days in seconds (2 * 24 * 60 * 60) 16 | 17 | const timelock = await deployments.deploy("TokenholderTimelock", { 18 | contract: "TimelockController", 19 | from: deployer, 20 | args: [minDelay, proposers, executors], 21 | log: true, 22 | }) 23 | 24 | if (hre.network.tags.tenderly) { 25 | await hre.tenderly.verify({ 26 | name: "TokenholderTimelock", 27 | address: timelock.address, 28 | }) 29 | } 30 | } 31 | 32 | export default func 33 | 34 | func.tags = ["TokenholderTimelock"] 35 | -------------------------------------------------------------------------------- /deploy/31_deploy_tokenholder_governor.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const { getNamedAccounts, deployments } = hre 6 | const { deployer, thresholdCouncil } = await getNamedAccounts() 7 | 8 | // TODO: fail if thresholdCouncil is undefined on mainnet 9 | let vetoer = thresholdCouncil ?? deployer 10 | 11 | const T = await deployments.get("T") 12 | const TokenStaking = await deployments.get("TokenStaking") 13 | const TokenholderTimelock = await deployments.get("TokenholderTimelock") 14 | 15 | const timelock = await deployments.deploy("TokenholderGovernor", { 16 | from: deployer, 17 | args: [ 18 | T.address, 19 | TokenStaking.address, 20 | TokenholderTimelock.address, 21 | vetoer, 22 | ], 23 | log: true, 24 | }) 25 | 26 | if (hre.network.tags.tenderly) { 27 | await hre.tenderly.verify({ 28 | name: "TokenholderGovernor", 29 | address: timelock.address, 30 | }) 31 | } 32 | } 33 | 34 | export default func 35 | 36 | func.tags = ["TokenholderGovernor"] 37 | func.dependencies = ["TokenStaking"] 38 | -------------------------------------------------------------------------------- /deploy/32_configure_tokenholder_timelock.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const { getNamedAccounts, deployments } = hre 6 | const { deployer } = await getNamedAccounts() 7 | const { execute, read, log } = deployments 8 | 9 | const TokenholderGovernor = await deployments.get("TokenholderGovernor") 10 | // const TokenholderTimelock = await deployments.get("TokenholderTimelock") 11 | 12 | const PROPOSER_ROLE = await read("TokenholderTimelock", "PROPOSER_ROLE") 13 | const TIMELOCK_ADMIN_ROLE = await read( 14 | "TokenholderTimelock", 15 | "TIMELOCK_ADMIN_ROLE" 16 | ) 17 | 18 | await execute( 19 | "TokenholderTimelock", 20 | { from: deployer }, 21 | "grantRole", 22 | PROPOSER_ROLE, 23 | TokenholderGovernor.address 24 | ) 25 | log(`Granted PROPOSER_ROLE to ${TokenholderGovernor.address}`) 26 | 27 | await execute( 28 | "TokenholderTimelock", 29 | { from: deployer }, 30 | "renounceRole", 31 | TIMELOCK_ADMIN_ROLE, 32 | deployer 33 | ) 34 | log(`Address ${deployer} renounced to TIMELOCK_ADMIN_ROLE`) 35 | } 36 | 37 | export default func 38 | 39 | func.tags = ["ConfigTokenholderTimelock"] 40 | func.dependencies = ["TokenholderGovernor"] 41 | -------------------------------------------------------------------------------- /deploy/51_transfer_ownership_t.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const { getNamedAccounts, helpers } = hre 6 | const { deployer, thresholdCouncil } = await getNamedAccounts() 7 | 8 | await helpers.ownable.transferOwnership("T", thresholdCouncil, deployer) 9 | } 10 | 11 | export default func 12 | 13 | func.tags = ["TransferOwnershipT"] 14 | func.dependencies = ["T"] 15 | func.runAtTheEnd = true 16 | func.skip = async function (hre: HardhatRuntimeEnvironment): Promise { 17 | return hre.network.name !== "mainnet" 18 | } 19 | -------------------------------------------------------------------------------- /deploy/52_transfer_ownership_token_staking.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const { getNamedAccounts, deployments } = hre 6 | const { deployer, thresholdCouncil } = await getNamedAccounts() 7 | const { execute } = deployments 8 | 9 | await execute( 10 | "TokenStaking", 11 | { from: deployer }, 12 | "transferGovernance", 13 | thresholdCouncil 14 | ) 15 | } 16 | 17 | export default func 18 | 19 | func.tags = ["TransferOwnershipTokenStaking"] 20 | func.runAtTheEnd = true 21 | func.dependencies = ["TokenStaking"] 22 | func.skip = async function (hre: HardhatRuntimeEnvironment): Promise { 23 | return hre.network.name !== "mainnet" 24 | } 25 | -------------------------------------------------------------------------------- /deploy/53_transfer_upgradeability_token_staking.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from "hardhat/types" 2 | import { DeployFunction } from "hardhat-deploy/types" 3 | import { upgrades } from "hardhat" 4 | 5 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 6 | const { getNamedAccounts } = hre 7 | const { thresholdCouncil } = await getNamedAccounts() 8 | 9 | await upgrades.admin.transferProxyAdminOwnership(thresholdCouncil) 10 | } 11 | 12 | export default func 13 | 14 | func.tags = ["TransferUpgradeabilityTokenStaking"] 15 | func.runAtTheEnd = true 16 | func.dependencies = ["TokenStaking"] 17 | func.skip = async function (hre: HardhatRuntimeEnvironment): Promise { 18 | return hre.network.name !== "mainnet" 19 | } 20 | -------------------------------------------------------------------------------- /deployments/mainnet/.chainId: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /deployments/mainnet/NuCypherToken.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x4fE83213D56308330EC302a8BD641f1d0113A4Cc", 3 | "abi": [ 4 | { 5 | "inputs": [ 6 | { 7 | "internalType": "uint256", 8 | "name": "_totalSupplyOfTokens", 9 | "type": "uint256" 10 | } 11 | ], 12 | "stateMutability": "nonpayable", 13 | "type": "constructor" 14 | }, 15 | { 16 | "anonymous": false, 17 | "inputs": [ 18 | { 19 | "indexed": true, 20 | "internalType": "address", 21 | "name": "owner", 22 | "type": "address" 23 | }, 24 | { 25 | "indexed": true, 26 | "internalType": "address", 27 | "name": "spender", 28 | "type": "address" 29 | }, 30 | { 31 | "indexed": false, 32 | "internalType": "uint256", 33 | "name": "value", 34 | "type": "uint256" 35 | } 36 | ], 37 | "name": "Approval", 38 | "type": "event" 39 | }, 40 | { 41 | "anonymous": false, 42 | "inputs": [ 43 | { 44 | "indexed": true, 45 | "internalType": "address", 46 | "name": "from", 47 | "type": "address" 48 | }, 49 | { 50 | "indexed": true, 51 | "internalType": "address", 52 | "name": "to", 53 | "type": "address" 54 | }, 55 | { 56 | "indexed": false, 57 | "internalType": "uint256", 58 | "name": "value", 59 | "type": "uint256" 60 | } 61 | ], 62 | "name": "Transfer", 63 | "type": "event" 64 | }, 65 | { 66 | "inputs": [ 67 | { 68 | "internalType": "address", 69 | "name": "owner", 70 | "type": "address" 71 | }, 72 | { 73 | "internalType": "address", 74 | "name": "spender", 75 | "type": "address" 76 | } 77 | ], 78 | "name": "allowance", 79 | "outputs": [ 80 | { 81 | "internalType": "uint256", 82 | "name": "", 83 | "type": "uint256" 84 | } 85 | ], 86 | "stateMutability": "view", 87 | "type": "function" 88 | }, 89 | { 90 | "inputs": [ 91 | { 92 | "internalType": "address", 93 | "name": "spender", 94 | "type": "address" 95 | }, 96 | { 97 | "internalType": "uint256", 98 | "name": "value", 99 | "type": "uint256" 100 | } 101 | ], 102 | "name": "approve", 103 | "outputs": [ 104 | { 105 | "internalType": "bool", 106 | "name": "", 107 | "type": "bool" 108 | } 109 | ], 110 | "stateMutability": "nonpayable", 111 | "type": "function" 112 | }, 113 | { 114 | "inputs": [ 115 | { 116 | "internalType": "address", 117 | "name": "_spender", 118 | "type": "address" 119 | }, 120 | { 121 | "internalType": "uint256", 122 | "name": "_value", 123 | "type": "uint256" 124 | }, 125 | { 126 | "internalType": "bytes", 127 | "name": "_extraData", 128 | "type": "bytes" 129 | } 130 | ], 131 | "name": "approveAndCall", 132 | "outputs": [ 133 | { 134 | "internalType": "bool", 135 | "name": "success", 136 | "type": "bool" 137 | } 138 | ], 139 | "stateMutability": "nonpayable", 140 | "type": "function" 141 | }, 142 | { 143 | "inputs": [ 144 | { 145 | "internalType": "address", 146 | "name": "owner", 147 | "type": "address" 148 | } 149 | ], 150 | "name": "balanceOf", 151 | "outputs": [ 152 | { 153 | "internalType": "uint256", 154 | "name": "", 155 | "type": "uint256" 156 | } 157 | ], 158 | "stateMutability": "view", 159 | "type": "function" 160 | }, 161 | { 162 | "inputs": [], 163 | "name": "decimals", 164 | "outputs": [ 165 | { 166 | "internalType": "uint8", 167 | "name": "", 168 | "type": "uint8" 169 | } 170 | ], 171 | "stateMutability": "view", 172 | "type": "function" 173 | }, 174 | { 175 | "inputs": [ 176 | { 177 | "internalType": "address", 178 | "name": "spender", 179 | "type": "address" 180 | }, 181 | { 182 | "internalType": "uint256", 183 | "name": "subtractedValue", 184 | "type": "uint256" 185 | } 186 | ], 187 | "name": "decreaseAllowance", 188 | "outputs": [ 189 | { 190 | "internalType": "bool", 191 | "name": "", 192 | "type": "bool" 193 | } 194 | ], 195 | "stateMutability": "nonpayable", 196 | "type": "function" 197 | }, 198 | { 199 | "inputs": [ 200 | { 201 | "internalType": "address", 202 | "name": "spender", 203 | "type": "address" 204 | }, 205 | { 206 | "internalType": "uint256", 207 | "name": "addedValue", 208 | "type": "uint256" 209 | } 210 | ], 211 | "name": "increaseAllowance", 212 | "outputs": [ 213 | { 214 | "internalType": "bool", 215 | "name": "", 216 | "type": "bool" 217 | } 218 | ], 219 | "stateMutability": "nonpayable", 220 | "type": "function" 221 | }, 222 | { 223 | "inputs": [], 224 | "name": "name", 225 | "outputs": [ 226 | { 227 | "internalType": "string", 228 | "name": "", 229 | "type": "string" 230 | } 231 | ], 232 | "stateMutability": "view", 233 | "type": "function" 234 | }, 235 | { 236 | "inputs": [], 237 | "name": "symbol", 238 | "outputs": [ 239 | { 240 | "internalType": "string", 241 | "name": "", 242 | "type": "string" 243 | } 244 | ], 245 | "stateMutability": "view", 246 | "type": "function" 247 | }, 248 | { 249 | "inputs": [], 250 | "name": "totalSupply", 251 | "outputs": [ 252 | { 253 | "internalType": "uint256", 254 | "name": "", 255 | "type": "uint256" 256 | } 257 | ], 258 | "stateMutability": "view", 259 | "type": "function" 260 | }, 261 | { 262 | "inputs": [ 263 | { 264 | "internalType": "address", 265 | "name": "to", 266 | "type": "address" 267 | }, 268 | { 269 | "internalType": "uint256", 270 | "name": "value", 271 | "type": "uint256" 272 | } 273 | ], 274 | "name": "transfer", 275 | "outputs": [ 276 | { 277 | "internalType": "bool", 278 | "name": "", 279 | "type": "bool" 280 | } 281 | ], 282 | "stateMutability": "nonpayable", 283 | "type": "function" 284 | }, 285 | { 286 | "inputs": [ 287 | { 288 | "internalType": "address", 289 | "name": "from", 290 | "type": "address" 291 | }, 292 | { 293 | "internalType": "address", 294 | "name": "to", 295 | "type": "address" 296 | }, 297 | { 298 | "internalType": "uint256", 299 | "name": "value", 300 | "type": "uint256" 301 | } 302 | ], 303 | "name": "transferFrom", 304 | "outputs": [ 305 | { 306 | "internalType": "bool", 307 | "name": "", 308 | "type": "bool" 309 | } 310 | ], 311 | "stateMutability": "nonpayable", 312 | "type": "function" 313 | } 314 | ], 315 | "numDeployments": 2 316 | } -------------------------------------------------------------------------------- /docgen-templates/common.hbs: -------------------------------------------------------------------------------- 1 | {{h}} {{name}} 2 | 3 | {{#if signature}} 4 | ```solidity 5 | {{{signature}}} 6 | ``` 7 | {{/if}} 8 | 9 | {{{natspec.notice}}} 10 | 11 | {{#if natspec.dev}} 12 | {{{natspec.dev}}} 13 | {{/if}} 14 | 15 | {{#if natspec.params}} 16 | {{h 2}} Parameters 17 | 18 | | Name | Type | Description | 19 | | ---- | ---- | ----------- | 20 | {{#each params}} 21 | | {{name}} | {{type}} | {{{joinLines natspec}}} | 22 | {{/each}} 23 | {{/if}} 24 | 25 | {{#if natspec.returns}} 26 | {{h 2}} Return Values 27 | 28 | | Name | Type | Description | 29 | | ---- | ---- | ----------- | 30 | {{#each returns}} 31 | | {{#if name}}{{name}}{{else}}[{{@index}}]{{/if}} | {{type}} | {{{joinLines natspec}}} | 32 | {{/each}} 33 | {{/if}} -------------------------------------------------------------------------------- /docs/rfc-1-staking-contract.adoc: -------------------------------------------------------------------------------- 1 | :toc: macro 2 | 3 | = RFC 1: T Staking Contract 4 | 5 | :icons: font 6 | :numbered: 7 | toc::[] 8 | 9 | == Proposal 10 | 11 | === Goal 12 | 13 | The goal of this proposal is to specify a simple and secure stake delegation 14 | mechanism. It should enable T owners to have their wallets offline and their 15 | stake managed by staking providers on their behalf. All off-chain client software 16 | should be able to run without exposing staking provider’s private key and should 17 | not require any owner’s keys at all. The stake delegation should also optimize the 18 | network throughput without compromising the security of the owners’ stake. 19 | 20 | This proposal aims at implementing a minimum viable staking contract version 21 | allowing to support native T delegations in all applications developed against 22 | this staking contract version. The functionality of the staking contract can be 23 | further extended by the upgradeability of the contract code. 24 | 25 | === Terminology 26 | 27 | Owner:: An address owning T tokens. An owner is the ultimate holder of the tokens. 28 | Before stake delegation, the owner has full control over the tokens, and the 29 | tokens are returned to the owner after stake delegation has finished. 30 | Owner’s participation is not required in the day-to-day operations on the 31 | stake, so cold storage can be accommodated to the maximum extent. 32 | 33 | Staking Provider:: An address of a party authorized to operate in the network on 34 | behalf of a given owner. The staking provider handles the everyday operations on 35 | the delegated stake without actually owning the staked tokens. A staking provider 36 | can not simply transfer away delegated tokens, however, it should be noted that 37 | the staking provider’s misbehavior may result in slashing tokens and thus the 38 | entire staked amount is indeed at stake. 39 | 40 | Beneficiary:: An address where the rewards for participation are sent, earned by 41 | the staking provider, on behalf of the owner. A beneficiary doesn’t sign or 42 | publish any protocol-relevant transactions, but any currency or tokens earned by 43 | the staking provider will be transferred to the beneficiary. 44 | 45 | Authorizer:: An address appointed by an owner to authorize applications on 46 | behalf of the owner. An application must be approved by the authorizer before the 47 | staking provider is eligible to participate. 48 | 49 | Delegated stake:: An owner’s staked tokens, delegated to the staking provider by 50 | the owner. Delegation enables token owners to have their wallets offline and their 51 | stake operated by staking providers on their behalf. 52 | 53 | Application:: An external smart contract or a set of smart contracts utilizing 54 | functionalities offered by Threshold Network. Example applications are: random 55 | beacon, proxy re-encryption, and tBTC. Applications authorized for the given 56 | staking provider are eligible to slash the stake delegated to that staking 57 | provider. 58 | 59 | == Specification 60 | 61 | === Functionality 62 | 63 | ==== Delegating a stake 64 | 65 | Tokens are delegated by the owner. During the delegation, the owner needs to 66 | appoint a staking provider, beneficiary, and authorizer. The owner may decide to 67 | delegate just a portion of their tokens. The owner may delegate multiple times 68 | to different staking providers. For simplicity, the staking provider address can 69 | not be reused between delegations even if all tokens have been unstaked. To 70 | protect against griefing by frontrunning stake delegation transactions and 71 | reserving staking provider addresses, staking contract should require a 72 | governable minimum stake amount for each delegation and there should be a 73 | minimum undelegation delay of 24 hours. 74 | 75 | Staking contract binds the owner with the staking provider, beneficiary, and 76 | authorizer for the given stake delegation and records the stake delegation amount. 77 | 78 | The staking contract does not define operator role. The operator responsible for 79 | running off-chain client software is appointed by the staking provider in the 80 | particular application contract utilizing the staking contract. All off-chain 81 | client software should be able to run without exposing operator's or staking 82 | provider’s private key. 83 | 84 | ==== Authorizing an application 85 | 86 | Before the staking provider is eligible to participate in the given application, 87 | the authorizer appointed during the stake delegation needs to review the 88 | application and approve it to use the stake. From the moment of approval, the 89 | application is authorized to slash or seize the stake. 90 | 91 | The authorizer decides up to which amount the given application is eligible to 92 | slash. This allows managing the risk profile of the staker individually for each 93 | application. The authorized stake amount can be increased and decreased. 94 | 95 | The amount authorized is set individually for each application and should be 96 | used as a staker's weight in the application: for work selection and rewards. 97 | The amount authorized for the application can be decreased as a result of 98 | the application approving authorization decrease request. 99 | The amount authorized for the application can be also decreased as a result of 100 | slashing, including slashing executed in the context of another application. 101 | 102 | The application being authorized is notified via a callback every single time 103 | new authorization happens and the application may decide to revert the 104 | transaction if individual requirements of the application are not met. 105 | For example, application may require a certain minimum authorized amount. 106 | The application is notified every single time the authorization increases or 107 | decreases. The application can revert this transaction in case some rules 108 | specific to that application are not met. Inside the callback, the application 109 | can also update its internal state, such as, for example, the weight of the 110 | staking provider in the sortition pool. 111 | 112 | Authorization decrease is a two-step process. First, the authorizer requests to 113 | decrease the authorization, and the application is notified about it by 114 | a callback. In the second step, the application approves the decrease at its own 115 | discretion. It is up to every individual application to decide if and when the 116 | authorization decrease gets approved. New authorization decrease request 117 | overwrites the pending one. 118 | 119 | All applications first need to be authorized in a registry of T applications 120 | controlled by the governance before any authorizer can use them. 121 | To protect against the attack of blocking the slashing queue by exceeding the 122 | block gas limit for slashing the particular staking provider, there should be a 123 | governable limit of applications that can be authorized per staking provider. 124 | 125 | ==== Stake top-up 126 | 127 | Top-up increases the amount of tokens locked in a delegation and allows to later 128 | increase the authorization for applications. This increases the probability of being 129 | chosen for work in the application but is only effective for future checks of the 130 | authorized amount. 131 | 132 | Stakes can only be topped-up with liquid T. 133 | 134 | Anyone can execute a stake top-up for a staking provider using liquid T. 135 | Stake top-up does not automatically increase authorization levels for applications. 136 | Stake top-up is a one-step process and does not require any delay. 137 | 138 | ==== Undelegating a stake (unstaking) 139 | 140 | The owner or staking provider may decide to unstake some amount of tokens if the 141 | amount left on the stake after this operation will be higher or equal to the 142 | highest authorization amongst all applications. Even if all tokens have been 143 | unstaked, relationship between owner, staking provider, beneficiary, and 144 | authorizer is retained in the staking contract in case some applications still 145 | have some rewards waiting for withdrawal. 146 | 147 | If the owner or staking provider attempts to unstake tokens before 24 hours 148 | passed since the delegation so that the amount left in the contract would be 149 | below the minimum stake, the transaction reverts. 150 | 151 | ==== Slashing a stake 152 | 153 | Authorized applications can slash or seize a stake. Slash operation decreases 154 | the stake of a staking provider and burns slashed tokens. Seize decreases the 155 | stake, burns 95% of the stake, and awards up to 5% to the notifier of misbehavior. 156 | 157 | To keep stakes synchronized between applications when staking providers are 158 | slashed, without the risk of running out of gas, the staking contract queues up 159 | slashings and let users process the transactions. 160 | 161 | When an application slashes one or more staking providers, it adds them to the 162 | slashing queue on the staking contract. A queue entry contains the stakign 163 | provider's address and the amount they're due to be slashed. 164 | 165 | When there is at least one staking provider in the slashing queue, any account 166 | can submit a transaction processing one or more staking providers' slashings, 167 | and collecting a reward for doing so. A queued slashing is processed by updating 168 | the staking provider's stake to the post-slashing amount, updating authorized 169 | amount for each affected application, and notifying all affected applications 170 | that the staking provider's authorized stake has been reduced due to slashing. 171 | The application must then do the necessary adjustments, such as removing the 172 | provider from the sortition pool or reducing its weight, changing the provider's 173 | eligibility for rewards, and so forth. 174 | 175 | Every application callback executed as a result of a slash should have a 250k gas 176 | limit. Slashing are processed in a FIFO basis, and there is just one function 177 | exposed by the staking contract allowing to slash one or more staking providers 178 | from the head of the queue. Callback failure does not revert the transaction. In 179 | case the callback failed, the slashing request is removed from the queue and 180 | never retried so it is in the application's best interest to ensure it can always 181 | execute the callback. The same happens if the slash operation fails because 182 | the given staking provider has insufficient stake to slash. 183 | 184 | It is important to note slashing executed in the context of one application may 185 | lead to involuntarily decreasing the authorization for other applications in 186 | case the amount of stake available after the slashing is lower than these 187 | authorizations. 188 | 189 | === Upgradeability 190 | 191 | The staking contract will be upgradeable. The exact upgradeability mechanism is 192 | out of the scope of this document. 193 | 194 | == Public API 195 | 196 | === Delegating a stake 197 | 198 | ==== `stake(address stakingProvider, address beneficiary, address authorizer, uint96 amount) external` 199 | 200 | Creates a delegation with `msg.sender` owner with the given staking provider, 201 | beneficiary, and authorizer. Transfers the given amount of T to the staking 202 | contract. The owner of the delegation needs to have the amount approved to 203 | transfer to the staking contract. 204 | 205 | ==== `setMinimumStakeAmount(uint96 amount) external onlyGovernance` 206 | 207 | Allows the governance to set the minimum required stake amount. This amount is 208 | required to protect against griefing the staking contract and individual 209 | applications are allowed to require higher minimum stakes if necessary. 210 | 211 | === Authorizing an application 212 | 213 | ==== `approveApplication(address application) external onlyGovernance` 214 | 215 | Allows the governance to approve the particular application before individual 216 | stake authorizers are able to authorize it. 217 | 218 | ==== `increaseAuthorization(address stakingProvider, address application, uint96 amount) external onlyAuthorizerOf(stakingProvider)` 219 | 220 | Increases the authorization of the given staking provider for the given application by 221 | the given amount. Calls `authorizationIncreased(address stakingProvider, uint96 amount)` 222 | callback on the given application to notify the application. Can only be called 223 | by the given provider's authorizer. 224 | 225 | ==== `requestAuthorizationDecrease(address stakingProvider, address application, uint96 amount) external onlyAuthorizerOf(stakingProvider)` 226 | 227 | Requests decrease of the authorization for the given staking provider on the given 228 | application by the provided amount. Calls `authorizationDecreaseRequested(address stakingProvider, uint96 amount)` 229 | on the application. It does not change the authorized amount. Can only be called 230 | by the given provider's authorizer. Overwrites pending authorization decrease 231 | for the given provider and application. 232 | 233 | ==== `requestAuthorizationDecrease(address stakingProvider) external onlyAuthorizerOf(stakingProvider)` 234 | 235 | Requests decrease of all authorizations for the given provider on all 236 | applications by all authorized amount. Calls `authorizationDecreaseRequested(address stakingProvider, uint256 amount)` 237 | for each authorized application. It may not change the authorized amount 238 | immediatelly. When it happens depends on the application. Can only be called by 239 | the given staking provider’s authorizer. Overwrites pending authorization 240 | decrease for the given staking provider and application. 241 | 242 | ==== `approveAuthorizationDecrease(address stakingProvider) external onlyRequestedApplication returns (uint96)` 243 | 244 | Called by the application at its discretion to approve the previously requested 245 | authorization decrease request. Can only be called by the application that 246 | was previously requested to decrease the authorization for that staking provider. 247 | Returns resulting authorized amount for the application. 248 | 249 | ==== `forceDecreaseAuthorization(address stakingProvider, address application) external` 250 | 251 | Decreases the authorization for the given `stakingProvider` on the given disabled 252 | `application`, for all authorized amount. Can be called by anyone. 253 | 254 | ==== `pauseApplication(address application) external onlyPanicButtonOf(application)` 255 | 256 | Pauses the given application's eligibility to slash stakes. Besides that stakers can't 257 | change authorization to the application. Can be called only by a panic button of the 258 | particular application. The paused application can not slash stakes until it is 259 | approved again by the governance using `approveApplication` function. Should be 260 | used only in case of an emergency. 261 | 262 | ==== `disableApplication(address application) external onlyGovernance` 263 | 264 | Disables the given application. The disabled application can't slash stakers. Also 265 | stakers can't increase authorization to that application but can decrease without 266 | waiting by calling `forceDecreaseAuthorization` at any moment. Can be called only 267 | by the governance. The disabled application can't be approved again. Should be used 268 | only in case of an emergency. 269 | 270 | ==== `setPanicButton(address application, address panicButton) external onlyGovernance` 271 | 272 | Sets the panic button role for the given application to the provided address. 273 | Can only be called by the governance. If the panic button for the given 274 | application should be disabled, the role address should be set to 0x0 address. 275 | 276 | ==== `setAuthorizationCeiling(uint256 ceiling) external onlyGovernance` 277 | 278 | Sets the maximum number of applications one staking provider can authorize. Used to 279 | protect against DoSing slashing queue. Can only be called by the governance. 280 | 281 | === Stake top-up 282 | 283 | ==== `topUp(address stakingProvider, uint96 amount) external` 284 | 285 | Increases the amount of the stake for the given staking provider. If `autoIncrease` 286 | flag is true then the amount will be added for all authorized applications. 287 | The sender of this transaction needs to have the amount approved to transfer 288 | to the staking contract. 289 | 290 | ==== `toggleAutoAuthorizationIncrease(address stakingProvider) external` 291 | 292 | Toggle auto authorization increase flag. If true then all amount in top-up 293 | will be added to already authorized applications. 294 | 295 | === Undelegating a stake (unstaking) 296 | 297 | ==== `unstakeT(address stakingProvider, uint96 amount) external onlyOwnerOrStakingProvider(stakingProvider)` 298 | 299 | Reduces the T stake amount by `amount` and withdraws `amount` of T 300 | to the owner. Reverts if there is at least one authorization higher than the 301 | remaining T stake or if the `amount` is higher than the T stake amount. 302 | Can be called only by the owner or the staking provider. 303 | 304 | === Keeping information in sync 305 | 306 | ==== `setNotificationReward(uint96 reward) external onlyGovernance` 307 | 308 | Sets reward in T tokens for notification of misbehaviour of one staking provider. 309 | Can only be called by the governance. 310 | 311 | ==== `pushNotificationReward(uint96 reward) external` 312 | 313 | Transfer some amount of T tokens as reward for notifications of misbehaviour. 314 | 315 | ==== `withdrawNotificationReward(address recipient, uint96 amount) external onlyGovernance` 316 | 317 | Withdraw some amount of T tokens from notifiers treasury. Can only be called by 318 | the governance. 319 | 320 | === Slashing a stake 321 | 322 | ==== `slash(uint96 amount, address[] memory stakingProviders) external onlyAuthorizedApplication` 323 | 324 | Adds staking providers to the slashing queue along with the amount that should be 325 | slashed from each one of them. Can only be called by an authorized application. 326 | 327 | ==== `seize(uint96 amount, uint256 rewardMultipier, address notifier, address[] memory stakingProviders) external onlyAuthorizedApplication` 328 | 329 | Adds staking providers to the slashing queue along with the amount. The notifier will 330 | receive reward per each staking provider from notifiers treasury. Can only be called by 331 | application authorized for all staking providers in the array. 332 | 333 | ==== `processSlashing(uint256 count)` 334 | 335 | Takes the `count` of queued slashing operations and processes them. Receives 5% 336 | of the slashed amount. Executes `involuntaryAuthorizationDecrease` function on 337 | each affected application. 338 | 339 | === Auxiliary functions 340 | 341 | ==== `authorizedStake(address stakingProvider, address application) external view returns (uint96)` 342 | 343 | Returns the authorized stake amount of the staking provider for the application. 344 | 345 | ==== `stakes(address stakingProvider) external view returns (uint96 tStake, uint96 keepInTStake, uint96 nuInTStake)` 346 | 347 | Returns staked amount of T for the specified staking provider. 348 | Method is deprecated. Use `stakeAmount` instead. 349 | 350 | ==== `stakeAmount(address stakingProvider) external view returns (uint96)` 351 | 352 | Returns staked amount of T for the specified staking provider. 353 | 354 | ==== `getStartStakingTimestamp(address stakingProvider) external view returns (uint256)` 355 | 356 | Returns start staking timestamp for T stake. This value is set at most once, 357 | and only when a stake is created with. 358 | 359 | 360 | ==== `getAutoIncreaseFlag(address stakingProvider) external view returns (bool)` 361 | 362 | Returns auto-increase flag. If flag is true then any topped up amount will be added to 363 | existing authorizations. 364 | 365 | ==== `rolesOf(address stakingProvider) external view returns (address owner, address payable beneficiary, address authorizer)` 366 | 367 | Gets the stake owner, the beneficiary and the authorizer for the specified 368 | staking provider address. 369 | 370 | ==== `getApplicationsLength() external view returns (uint256)` 371 | 372 | Returns length of application array 373 | 374 | ==== `getSlashingQueueLength() external view returns (uint256)` 375 | 376 | Returns length of slashing queue 377 | 378 | ==== `getMaxAuthorization(address stakingProvider) external view returns (uint96)` 379 | 380 | Returns the maximum application authorization. 381 | 382 | ==== `getAvailableToAuthorize(address stakingProvider, address application) external view returns (uint96)` 383 | 384 | Returns available amount to authorize for the specified application 385 | 386 | === Application callbacks 387 | 388 | ==== `authorizationIncreased(address stakingProvider, uint96 amount)` 389 | 390 | Used by T staking contract to inform the application the the authorized amount 391 | for the given staking provider increased to the given amount. The application 392 | may do any housekeeping necessary. 393 | 394 | ==== `authorizationDecreaseRequested(address stakingProvider, uint96 amount)` 395 | 396 | Used by T staking contract to inform the application that the given staking 397 | provider requested to decrease the authorization to the given amount. The 398 | application should mark the authorization as pending decrease and respond to the 399 | staking contract with `approveAuthorizationDecrease` at its discretion. It may 400 | happen right away but it also may happen several months later. 401 | 402 | ==== `involuntaryAuthorizationDecrease(address stakingProvider, uint96 amount)` 403 | 404 | Used by T staking contract to inform the application the authorization has 405 | been decreased for the given staking provider to the given amount involuntarily, 406 | as a result of slashing. Lets the application to do any housekeeping neccessary. 407 | Called with 250k gas limit and does not revert the transaction if 408 | `involuntaryAuthorizationDecrease` call failed. 409 | -------------------------------------------------------------------------------- /docs/rfc-2-vending-machine.adoc: -------------------------------------------------------------------------------- 1 | :toc: macro 2 | 3 | = RFC 2: Vending Machine contracts 4 | 5 | :icons: font 6 | :numbered: 7 | toc::[] 8 | 9 | == Introduction 10 | 11 | The Vending Machine contracts implement a special update protocol to enable 12 | legacy token holders (NU/KEEP) to wrap their tokens and obtain T tokens 13 | according to a fixed, pre-defined ratio. The contracts are completely autonomous 14 | and will work indefinitely, enabling NU and KEEP token holders to join the 15 | T network without needing to buy or sell any assets. Logistically, anyone 16 | holding NU or KEEP can wrap those assets in order to upgrade to T. They can also 17 | unwrap T in order to downgrade back to the underlying asset. 18 | 19 | There is a separate instance of this contract deployed for KEEP holders and a 20 | separate instance of this contract deployed for NU holders. 21 | 22 | == Public API 23 | 24 | === Wrapping and unwrapping 25 | 26 | === `wrap(uint256 amount) external` 27 | 28 | Wraps up to the the given `amount` of a legacy token (KEEP/NU) and releases T 29 | tokens proportionally to the amount being wrapped, according to the wrap ratio. 30 | The token holder needs to have at least the given amount of the wrapped token 31 | (KEEP/NU) previously approved to the Vending Machine before calling this 32 | function. Note that, since the Vending Machine guarantees exact conversion, 33 | there may be a small unspent remainder on the token holder balance. 34 | 35 | Emits a `Wrapped(address indexed recipient, uint256 wrappedTokenAmount, 36 | uint256 tTokenAmount)' event. 37 | 38 | === `unwrap(uint256 amount) external` 39 | 40 | Unwraps up to the given `amount` of T back to the Vending Machine's legacy token 41 | (KEEP/NU), according to the wrap ratio. It can only be called by a token holder 42 | who previously wrapped their tokens in this vending machine contract. The token 43 | holder can't unwrap more tokens than they originally wrapped. The token holder 44 | needs to have at least the given amount of T tokens approved to transfer to the 45 | Vending Machine before calling this function. Note that, since the Vending 46 | Machine guarantees exact conversion, there may be a small unspent remainder on 47 | the token holder balance. 48 | 49 | Emits an `Unwrapped(address indexed recipient, uint256 tTokenAmount, 50 | uint256 wrappedTokenAmount)` event. 51 | 52 | === Conversion rate utilities 53 | 54 | ==== `conversionToT(uint256 amount) public view returns (uint256 tAmount, uint256 wrappedRemainder)` 55 | 56 | Returns the T token amount that's obtained from `amount` wrapped tokens 57 | (KEEP/NU), and the `wrappedRemainder` that can't be upgraded. 58 | 59 | ==== `conversionFromT(uint256 amount) public view returns (uint256 wrappedAmount, uint256 tRemainder)` 60 | 61 | Returns the amount of wrapped tokens (KEEP/NU) that's obtained from unwrapping 62 | `amount` T tokens, and the `tRemainder` that can't be downgraded. 63 | 64 | === Vending Machine parameters 65 | 66 | TODO 67 | 68 | == Security considerations 69 | 70 | === Token total supplies 71 | 72 | The logic of the vending machine contracts assumes that the total supply of the 73 | underlying legacy tokens is fixed 74 | 75 | === Contract ownership 76 | 77 | The vending machine contracts are not ownable. 78 | 79 | === Upgradeability 80 | 81 | The vending machine contracts are not upgradeable. 82 | 83 | === Audits 84 | 85 | * CertiK (October 2021): https://www.certik.com/projects/threshold-network 86 | * ChainSecurity (November 2021): https://chainsecurity.com/security-audit/threshold-network/ 87 | 88 | 89 | == Deployments and contract addresses 90 | 91 | Information about the latest deployments and addresses of the contracts can be 92 | found link:https://docs.threshold.network/extras/contract-addresses[here]. 93 | -------------------------------------------------------------------------------- /external/mainnet/KeepToken.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x85Eee30c52B0b379b046Fb0F85F4f3Dc3009aFEC", 3 | "abi": [ 4 | { 5 | "inputs": [], 6 | "payable": false, 7 | "stateMutability": "nonpayable", 8 | "type": "constructor" 9 | }, 10 | { 11 | "anonymous": false, 12 | "inputs": [ 13 | { 14 | "indexed": true, 15 | "internalType": "address", 16 | "name": "owner", 17 | "type": "address" 18 | }, 19 | { 20 | "indexed": true, 21 | "internalType": "address", 22 | "name": "spender", 23 | "type": "address" 24 | }, 25 | { 26 | "indexed": false, 27 | "internalType": "uint256", 28 | "name": "value", 29 | "type": "uint256" 30 | } 31 | ], 32 | "name": "Approval", 33 | "type": "event" 34 | }, 35 | { 36 | "anonymous": false, 37 | "inputs": [ 38 | { 39 | "indexed": true, 40 | "internalType": "address", 41 | "name": "from", 42 | "type": "address" 43 | }, 44 | { 45 | "indexed": true, 46 | "internalType": "address", 47 | "name": "to", 48 | "type": "address" 49 | }, 50 | { 51 | "indexed": false, 52 | "internalType": "uint256", 53 | "name": "value", 54 | "type": "uint256" 55 | } 56 | ], 57 | "name": "Transfer", 58 | "type": "event" 59 | }, 60 | { 61 | "constant": true, 62 | "inputs": [], 63 | "name": "DECIMALS", 64 | "outputs": [ 65 | { 66 | "internalType": "uint8", 67 | "name": "", 68 | "type": "uint8" 69 | } 70 | ], 71 | "payable": false, 72 | "stateMutability": "view", 73 | "type": "function" 74 | }, 75 | { 76 | "constant": true, 77 | "inputs": [], 78 | "name": "INITIAL_SUPPLY", 79 | "outputs": [ 80 | { 81 | "internalType": "uint256", 82 | "name": "", 83 | "type": "uint256" 84 | } 85 | ], 86 | "payable": false, 87 | "stateMutability": "view", 88 | "type": "function" 89 | }, 90 | { 91 | "constant": true, 92 | "inputs": [], 93 | "name": "NAME", 94 | "outputs": [ 95 | { 96 | "internalType": "string", 97 | "name": "", 98 | "type": "string" 99 | } 100 | ], 101 | "payable": false, 102 | "stateMutability": "view", 103 | "type": "function" 104 | }, 105 | { 106 | "constant": true, 107 | "inputs": [], 108 | "name": "SYMBOL", 109 | "outputs": [ 110 | { 111 | "internalType": "string", 112 | "name": "", 113 | "type": "string" 114 | } 115 | ], 116 | "payable": false, 117 | "stateMutability": "view", 118 | "type": "function" 119 | }, 120 | { 121 | "constant": true, 122 | "inputs": [ 123 | { 124 | "internalType": "address", 125 | "name": "owner", 126 | "type": "address" 127 | }, 128 | { 129 | "internalType": "address", 130 | "name": "spender", 131 | "type": "address" 132 | } 133 | ], 134 | "name": "allowance", 135 | "outputs": [ 136 | { 137 | "internalType": "uint256", 138 | "name": "", 139 | "type": "uint256" 140 | } 141 | ], 142 | "payable": false, 143 | "stateMutability": "view", 144 | "type": "function" 145 | }, 146 | { 147 | "constant": false, 148 | "inputs": [ 149 | { 150 | "internalType": "address", 151 | "name": "spender", 152 | "type": "address" 153 | }, 154 | { 155 | "internalType": "uint256", 156 | "name": "amount", 157 | "type": "uint256" 158 | } 159 | ], 160 | "name": "approve", 161 | "outputs": [ 162 | { 163 | "internalType": "bool", 164 | "name": "", 165 | "type": "bool" 166 | } 167 | ], 168 | "payable": false, 169 | "stateMutability": "nonpayable", 170 | "type": "function" 171 | }, 172 | { 173 | "constant": false, 174 | "inputs": [ 175 | { 176 | "internalType": "address", 177 | "name": "_spender", 178 | "type": "address" 179 | }, 180 | { 181 | "internalType": "uint256", 182 | "name": "_value", 183 | "type": "uint256" 184 | }, 185 | { 186 | "internalType": "bytes", 187 | "name": "_extraData", 188 | "type": "bytes" 189 | } 190 | ], 191 | "name": "approveAndCall", 192 | "outputs": [ 193 | { 194 | "internalType": "bool", 195 | "name": "success", 196 | "type": "bool" 197 | } 198 | ], 199 | "payable": false, 200 | "stateMutability": "nonpayable", 201 | "type": "function" 202 | }, 203 | { 204 | "constant": true, 205 | "inputs": [ 206 | { 207 | "internalType": "address", 208 | "name": "account", 209 | "type": "address" 210 | } 211 | ], 212 | "name": "balanceOf", 213 | "outputs": [ 214 | { 215 | "internalType": "uint256", 216 | "name": "", 217 | "type": "uint256" 218 | } 219 | ], 220 | "payable": false, 221 | "stateMutability": "view", 222 | "type": "function" 223 | }, 224 | { 225 | "constant": false, 226 | "inputs": [ 227 | { 228 | "internalType": "uint256", 229 | "name": "amount", 230 | "type": "uint256" 231 | } 232 | ], 233 | "name": "burn", 234 | "outputs": [], 235 | "payable": false, 236 | "stateMutability": "nonpayable", 237 | "type": "function" 238 | }, 239 | { 240 | "constant": false, 241 | "inputs": [ 242 | { 243 | "internalType": "address", 244 | "name": "account", 245 | "type": "address" 246 | }, 247 | { 248 | "internalType": "uint256", 249 | "name": "amount", 250 | "type": "uint256" 251 | } 252 | ], 253 | "name": "burnFrom", 254 | "outputs": [], 255 | "payable": false, 256 | "stateMutability": "nonpayable", 257 | "type": "function" 258 | }, 259 | { 260 | "constant": true, 261 | "inputs": [], 262 | "name": "decimals", 263 | "outputs": [ 264 | { 265 | "internalType": "uint8", 266 | "name": "", 267 | "type": "uint8" 268 | } 269 | ], 270 | "payable": false, 271 | "stateMutability": "view", 272 | "type": "function" 273 | }, 274 | { 275 | "constant": false, 276 | "inputs": [ 277 | { 278 | "internalType": "address", 279 | "name": "spender", 280 | "type": "address" 281 | }, 282 | { 283 | "internalType": "uint256", 284 | "name": "subtractedValue", 285 | "type": "uint256" 286 | } 287 | ], 288 | "name": "decreaseAllowance", 289 | "outputs": [ 290 | { 291 | "internalType": "bool", 292 | "name": "", 293 | "type": "bool" 294 | } 295 | ], 296 | "payable": false, 297 | "stateMutability": "nonpayable", 298 | "type": "function" 299 | }, 300 | { 301 | "constant": false, 302 | "inputs": [ 303 | { 304 | "internalType": "address", 305 | "name": "spender", 306 | "type": "address" 307 | }, 308 | { 309 | "internalType": "uint256", 310 | "name": "addedValue", 311 | "type": "uint256" 312 | } 313 | ], 314 | "name": "increaseAllowance", 315 | "outputs": [ 316 | { 317 | "internalType": "bool", 318 | "name": "", 319 | "type": "bool" 320 | } 321 | ], 322 | "payable": false, 323 | "stateMutability": "nonpayable", 324 | "type": "function" 325 | }, 326 | { 327 | "constant": true, 328 | "inputs": [], 329 | "name": "name", 330 | "outputs": [ 331 | { 332 | "internalType": "string", 333 | "name": "", 334 | "type": "string" 335 | } 336 | ], 337 | "payable": false, 338 | "stateMutability": "view", 339 | "type": "function" 340 | }, 341 | { 342 | "constant": true, 343 | "inputs": [], 344 | "name": "symbol", 345 | "outputs": [ 346 | { 347 | "internalType": "string", 348 | "name": "", 349 | "type": "string" 350 | } 351 | ], 352 | "payable": false, 353 | "stateMutability": "view", 354 | "type": "function" 355 | }, 356 | { 357 | "constant": true, 358 | "inputs": [], 359 | "name": "totalSupply", 360 | "outputs": [ 361 | { 362 | "internalType": "uint256", 363 | "name": "", 364 | "type": "uint256" 365 | } 366 | ], 367 | "payable": false, 368 | "stateMutability": "view", 369 | "type": "function" 370 | }, 371 | { 372 | "constant": false, 373 | "inputs": [ 374 | { 375 | "internalType": "address", 376 | "name": "recipient", 377 | "type": "address" 378 | }, 379 | { 380 | "internalType": "uint256", 381 | "name": "amount", 382 | "type": "uint256" 383 | } 384 | ], 385 | "name": "transfer", 386 | "outputs": [ 387 | { 388 | "internalType": "bool", 389 | "name": "", 390 | "type": "bool" 391 | } 392 | ], 393 | "payable": false, 394 | "stateMutability": "nonpayable", 395 | "type": "function" 396 | }, 397 | { 398 | "constant": false, 399 | "inputs": [ 400 | { 401 | "internalType": "address", 402 | "name": "sender", 403 | "type": "address" 404 | }, 405 | { 406 | "internalType": "address", 407 | "name": "recipient", 408 | "type": "address" 409 | }, 410 | { 411 | "internalType": "uint256", 412 | "name": "amount", 413 | "type": "uint256" 414 | } 415 | ], 416 | "name": "transferFrom", 417 | "outputs": [ 418 | { 419 | "internalType": "bool", 420 | "name": "", 421 | "type": "bool" 422 | } 423 | ], 424 | "payable": false, 425 | "stateMutability": "nonpayable", 426 | "type": "function" 427 | } 428 | ] 429 | } 430 | -------------------------------------------------------------------------------- /external/mainnet/NuCypherToken.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "0x4fE83213D56308330EC302a8BD641f1d0113A4Cc", 3 | "abi": [ 4 | { 5 | "inputs": [ 6 | { 7 | "internalType": "uint256", 8 | "name": "_totalSupplyOfTokens", 9 | "type": "uint256" 10 | } 11 | ], 12 | "stateMutability": "nonpayable", 13 | "type": "constructor" 14 | }, 15 | { 16 | "anonymous": false, 17 | "inputs": [ 18 | { 19 | "indexed": true, 20 | "internalType": "address", 21 | "name": "owner", 22 | "type": "address" 23 | }, 24 | { 25 | "indexed": true, 26 | "internalType": "address", 27 | "name": "spender", 28 | "type": "address" 29 | }, 30 | { 31 | "indexed": false, 32 | "internalType": "uint256", 33 | "name": "value", 34 | "type": "uint256" 35 | } 36 | ], 37 | "name": "Approval", 38 | "type": "event" 39 | }, 40 | { 41 | "anonymous": false, 42 | "inputs": [ 43 | { 44 | "indexed": true, 45 | "internalType": "address", 46 | "name": "from", 47 | "type": "address" 48 | }, 49 | { 50 | "indexed": true, 51 | "internalType": "address", 52 | "name": "to", 53 | "type": "address" 54 | }, 55 | { 56 | "indexed": false, 57 | "internalType": "uint256", 58 | "name": "value", 59 | "type": "uint256" 60 | } 61 | ], 62 | "name": "Transfer", 63 | "type": "event" 64 | }, 65 | { 66 | "inputs": [ 67 | { "internalType": "address", "name": "owner", "type": "address" }, 68 | { "internalType": "address", "name": "spender", "type": "address" } 69 | ], 70 | "name": "allowance", 71 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 72 | "stateMutability": "view", 73 | "type": "function" 74 | }, 75 | { 76 | "inputs": [ 77 | { "internalType": "address", "name": "spender", "type": "address" }, 78 | { "internalType": "uint256", "name": "value", "type": "uint256" } 79 | ], 80 | "name": "approve", 81 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 82 | "stateMutability": "nonpayable", 83 | "type": "function" 84 | }, 85 | { 86 | "inputs": [ 87 | { "internalType": "address", "name": "_spender", "type": "address" }, 88 | { "internalType": "uint256", "name": "_value", "type": "uint256" }, 89 | { "internalType": "bytes", "name": "_extraData", "type": "bytes" } 90 | ], 91 | "name": "approveAndCall", 92 | "outputs": [ 93 | { "internalType": "bool", "name": "success", "type": "bool" } 94 | ], 95 | "stateMutability": "nonpayable", 96 | "type": "function" 97 | }, 98 | { 99 | "inputs": [ 100 | { "internalType": "address", "name": "owner", "type": "address" } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 104 | "stateMutability": "view", 105 | "type": "function" 106 | }, 107 | { 108 | "inputs": [], 109 | "name": "decimals", 110 | "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], 111 | "stateMutability": "view", 112 | "type": "function" 113 | }, 114 | { 115 | "inputs": [ 116 | { "internalType": "address", "name": "spender", "type": "address" }, 117 | { 118 | "internalType": "uint256", 119 | "name": "subtractedValue", 120 | "type": "uint256" 121 | } 122 | ], 123 | "name": "decreaseAllowance", 124 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 125 | "stateMutability": "nonpayable", 126 | "type": "function" 127 | }, 128 | { 129 | "inputs": [ 130 | { "internalType": "address", "name": "spender", "type": "address" }, 131 | { "internalType": "uint256", "name": "addedValue", "type": "uint256" } 132 | ], 133 | "name": "increaseAllowance", 134 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 135 | "stateMutability": "nonpayable", 136 | "type": "function" 137 | }, 138 | { 139 | "inputs": [], 140 | "name": "name", 141 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }], 142 | "stateMutability": "view", 143 | "type": "function" 144 | }, 145 | { 146 | "inputs": [], 147 | "name": "symbol", 148 | "outputs": [{ "internalType": "string", "name": "", "type": "string" }], 149 | "stateMutability": "view", 150 | "type": "function" 151 | }, 152 | { 153 | "inputs": [], 154 | "name": "totalSupply", 155 | "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], 156 | "stateMutability": "view", 157 | "type": "function" 158 | }, 159 | { 160 | "inputs": [ 161 | { "internalType": "address", "name": "to", "type": "address" }, 162 | { "internalType": "uint256", "name": "value", "type": "uint256" } 163 | ], 164 | "name": "transfer", 165 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 166 | "stateMutability": "nonpayable", 167 | "type": "function" 168 | }, 169 | { 170 | "inputs": [ 171 | { "internalType": "address", "name": "from", "type": "address" }, 172 | { "internalType": "address", "name": "to", "type": "address" }, 173 | { "internalType": "uint256", "name": "value", "type": "uint256" } 174 | ], 175 | "name": "transferFrom", 176 | "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], 177 | "stateMutability": "nonpayable", 178 | "type": "function" 179 | } 180 | ] 181 | } 182 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config" 2 | 3 | import "@keep-network/hardhat-helpers" 4 | import "@nomiclabs/hardhat-waffle" 5 | import "@openzeppelin/hardhat-upgrades" 6 | import "@tenderly/hardhat-tenderly" 7 | 8 | import "hardhat-contract-sizer" 9 | import "hardhat-deploy" 10 | import "hardhat-gas-reporter" 11 | import "solidity-docgen" 12 | 13 | const config: HardhatUserConfig = { 14 | solidity: { 15 | compilers: [ 16 | { 17 | version: "0.8.9", 18 | settings: { 19 | optimizer: { 20 | enabled: true, 21 | runs: 10, 22 | }, 23 | }, 24 | }, 25 | ], 26 | }, 27 | paths: { 28 | artifacts: "./build", 29 | }, 30 | networks: { 31 | hardhat: { 32 | forking: { 33 | // forking is enabled only if FORKING_URL env is provided 34 | enabled: !!process.env.FORKING_URL, 35 | // URL should point to a node with archival data (Alchemy recommended) 36 | url: process.env.FORKING_URL || "", 37 | // latest block is taken if FORKING_BLOCK env is not provided 38 | blockNumber: process.env.FORKING_BLOCK 39 | ? parseInt(process.env.FORKING_BLOCK) 40 | : undefined, 41 | }, 42 | tags: ["allowStubs"], 43 | }, 44 | development: { 45 | url: "http://localhost:8545", 46 | chainId: 1101, 47 | tags: ["allowStubs"], 48 | }, 49 | sepolia: { 50 | url: process.env.CHAIN_API_URL || "", 51 | chainId: 11155111, 52 | accounts: process.env.CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY 53 | ? [ 54 | process.env.CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY, 55 | process.env.KEEP_CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY, // TODO: verify if we have different owner here or can we remove this 56 | ] 57 | : undefined, 58 | tags: ["tenderly"], 59 | }, 60 | mainnet: { 61 | url: process.env.CHAIN_API_URL || "", 62 | chainId: 1, 63 | accounts: process.env.CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY 64 | ? [process.env.CONTRACT_OWNER_ACCOUNT_PRIVATE_KEY] 65 | : undefined, 66 | tags: ["tenderly"], 67 | }, 68 | }, 69 | tenderly: { 70 | username: "thesis", 71 | project: "thesis/threshold-network", 72 | }, 73 | verify: { 74 | etherscan: { 75 | apiKey: process.env.ETHERSCAN_API_KEY, 76 | }, 77 | }, 78 | external: { 79 | deployments: { 80 | // For hardhat environment we can fork the mainnet, so we need to point it 81 | // to the contract artifacts. 82 | hardhat: process.env.FORKING_URL ? ["./external/mainnet"] : [], 83 | mainnet: ["./external/mainnet"], 84 | }, 85 | }, 86 | namedAccounts: { 87 | deployer: { 88 | default: 1, // take the first account as deployer 89 | sepolia: 0, 90 | // mainnet: "0x123694886DBf5Ac94DDA07135349534536D14cAf", 91 | }, 92 | thresholdCouncil: { 93 | mainnet: "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f", 94 | }, 95 | }, 96 | mocha: { 97 | timeout: 60000, 98 | }, 99 | docgen: { 100 | outputDir: "generated-docs", 101 | templates: "docgen-templates", 102 | pages: "files", // `single`, `items` or `files` 103 | exclude: ["./test"], 104 | }, 105 | } 106 | 107 | export default config 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@threshold-network/solidity-contracts", 3 | "version": "1.3.0-dev", 4 | "license": "GPL-3.0-or-later", 5 | "files": [ 6 | "artifacts/", 7 | "build/contracts/", 8 | "contracts/", 9 | "deploy/", 10 | "export/", 11 | "scripts/", 12 | ".openzeppelin/mainnet.json", 13 | "export.json" 14 | ], 15 | "scripts": { 16 | "clean": "hardhat clean && rm -rf cache/ deployments/ export/ external/npm export.json", 17 | "build": "hardhat compile", 18 | "deploy": "hardhat deploy --export export.json", 19 | "format": "npm run lint && prettier --check .", 20 | "format:fix": "npm run lint:fix && prettier --write .", 21 | "lint": "npm run lint:js && npm run lint:sol", 22 | "lint:fix": "npm run lint:fix:js && npm run lint:fix:sol", 23 | "lint:fix:js": "eslint . --fix", 24 | "lint:fix:sol": "solhint 'contracts/**/*.sol' --fix", 25 | "lint:js": "eslint . ", 26 | "lint:sol": "solhint 'contracts/**/*.sol'", 27 | "test": "hardhat test", 28 | "test:system": "NODE_ENV=system-test hardhat test ./test/system/*.test.js", 29 | "prepack": "tsc -p tsconfig.export.json && hardhat export-artifacts export/artifacts", 30 | "prepublishOnly": "hardhat prepare-artifacts --network $npm_config_network" 31 | }, 32 | "devDependencies": { 33 | "@keep-network/hardhat-helpers": "^0.6.0-pre.8", 34 | "@keep-network/prettier-config-keep": "github:keep-network/prettier-config-keep#d6ec02e", 35 | "@nomiclabs/hardhat-ethers": "^2.0.2", 36 | "@nomiclabs/hardhat-waffle": "^2.0.1", 37 | "@openzeppelin/hardhat-upgrades": "^1.12.0", 38 | "@tenderly/hardhat-tenderly": ">=1.0.12 <1.1.0", 39 | "@types/chai": "^4.2.22", 40 | "@types/mocha": "^9.0.0", 41 | "@types/node": "^16.11.6", 42 | "chai": "^4.3.4", 43 | "eslint": "^7.27.0", 44 | "eslint-config-keep": "github:keep-network/eslint-config-keep#0c27ade", 45 | "ethereum-waffle": "^3.4.0", 46 | "ethers": "^5.5.3", 47 | "hardhat": "^2.8.3", 48 | "hardhat-contract-sizer": "^2.5.0", 49 | "hardhat-deploy": "^0.11.37", 50 | "hardhat-gas-reporter": "^1.0.6", 51 | "prettier": "^2.3.2", 52 | "prettier-plugin-sh": "^0.7.1", 53 | "prettier-plugin-solidity": "^1.0.0-beta.14 ", 54 | "solhint": "^3.3.6", 55 | "solhint-config-keep": "github:keep-network/solhint-config-keep", 56 | "solidity-docgen": "^0.6.0-beta.35", 57 | "ts-node": "^10.4.0", 58 | "typescript": "^4.4.4" 59 | }, 60 | "dependencies": { 61 | "@openzeppelin/contracts": "~4.5.0", 62 | "@openzeppelin/contracts-upgradeable": "~4.5.2", 63 | "@thesis/solidity-contracts": "github:thesis/solidity-contracts#4985bcf" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "detectors_to_exclude": "assembly,naming-convention,solc-version,timestamp,too-many-digits,reentrancy-no-eth,reentrancy-events,similar-names", 3 | "filter_paths": "contracts/test|node_modules", 4 | "hardhat_artifacts_directory": "./build" 5 | } 6 | -------------------------------------------------------------------------------- /test/governance/GovernorParameters.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai") 2 | 3 | describe("ParametersGovernor", () => { 4 | let executor 5 | let other 6 | 7 | beforeEach(async () => { 8 | ;[executor, other] = await ethers.getSigners() 9 | 10 | const TestGovernorParameters = await ethers.getContractFactory( 11 | "TestGovernorParameters" 12 | ) 13 | tGov = await TestGovernorParameters.deploy(executor.address) 14 | await tGov.deployed() 15 | }) 16 | 17 | describe("initial parameters", () => { 18 | it("quorum denominator is 10000", async () => { 19 | expect(await tGov.FRACTION_DENOMINATOR()).to.equal(10000) 20 | }) 21 | 22 | it("quorum numerator is 10", async () => { 23 | expect(await tGov.quorumNumerator()).to.equal(10) 24 | }) 25 | 26 | it("proposal threshold numerator is 20", async () => { 27 | expect(await tGov.proposalThresholdNumerator()).to.equal(20) 28 | }) 29 | 30 | it("voting delay is 30", async () => { 31 | expect(await tGov.votingDelay()).to.equal(30) 32 | }) 33 | 34 | it("voting period is 40", async () => { 35 | expect(await tGov.votingPeriod()).to.equal(40) 36 | }) 37 | }) 38 | 39 | describe("parameters are updated", () => { 40 | let tx1 41 | let tx2 42 | let tx3 43 | let tx4 44 | beforeEach(async () => { 45 | tx1 = await tGov.connect(executor).updateQuorumNumerator(100) 46 | tx2 = await tGov.connect(executor).updateProposalThresholdNumerator(200) 47 | tx3 = await tGov.connect(executor).setVotingDelay(300) 48 | tx4 = await tGov.connect(executor).setVotingPeriod(400) 49 | }) 50 | 51 | it("quorum numerator is now 100", async () => { 52 | expect(await tGov.quorumNumerator()).to.equal(100) 53 | }) 54 | 55 | it("should emit QuorumNumeratorUpdated event", async () => { 56 | await expect(tx1) 57 | .to.emit(tGov, "QuorumNumeratorUpdated") 58 | .withArgs(10, 100) 59 | }) 60 | 61 | it("proposal threshold numerator is now 200", async () => { 62 | expect(await tGov.proposalThresholdNumerator()).to.equal(200) 63 | }) 64 | 65 | it("should emit ProposalThresholdNumeratorUpdated event", async () => { 66 | await expect(tx2) 67 | .to.emit(tGov, "ProposalThresholdNumeratorUpdated") 68 | .withArgs(20, 200) 69 | }) 70 | 71 | it("voting delay is now 300", async () => { 72 | expect(await tGov.votingDelay()).to.equal(300) 73 | }) 74 | 75 | it("should emit VotingDelaySet event", async () => { 76 | await expect(tx3).to.emit(tGov, "VotingDelaySet").withArgs(30, 300) 77 | }) 78 | 79 | it("voting period is now 400", async () => { 80 | expect(await tGov.votingPeriod()).to.equal(400) 81 | }) 82 | 83 | it("should emit VotingPeriodSet event", async () => { 84 | await expect(tx4).to.emit(tGov, "VotingPeriodSet").withArgs(40, 400) 85 | }) 86 | }) 87 | 88 | describe("when trying to update parameters by non-executor", () => { 89 | it("should revert", async () => { 90 | await expect( 91 | tGov.connect(other).updateQuorumNumerator(1234) 92 | ).to.be.revertedWith("Governor: onlyGovernance") 93 | await expect( 94 | tGov.connect(other).updateProposalThresholdNumerator(1234) 95 | ).to.be.revertedWith("Governor: onlyGovernance") 96 | await expect(tGov.connect(other).setVotingDelay(1234)).to.be.revertedWith( 97 | "Governor: onlyGovernance" 98 | ) 99 | await expect( 100 | tGov.connect(other).setVotingPeriod(1234) 101 | ).to.be.revertedWith("Governor: onlyGovernance") 102 | }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /test/governance/ProxyAdminWithDeputy.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai") 2 | 3 | const { upgrades } = require("hardhat") 4 | 5 | describe("ProxyAdminWithDeputy", () => { 6 | let deployer 7 | let deputy 8 | let timelock 9 | let adminWithDeputy 10 | 11 | let SimpleStorage 12 | const initialState = 42 13 | 14 | beforeEach(async () => { 15 | ;[deployer, deputy, timelock] = await ethers.getSigners() 16 | SimpleStorage = await ethers.getContractFactory("SimpleStorage") 17 | const initializerArgs = [initialState] // stored value in proxy state 18 | storage = await upgrades.deployProxy(SimpleStorage, initializerArgs, { 19 | kind: "transparent", 20 | constructorArgs: [1], // implementation version 1 21 | }) 22 | await storage.deployed() 23 | 24 | const GovernorStub = await ethers.getContractFactory( 25 | "TestTokenholderGovernorStubV2" 26 | ) 27 | tGov = await GovernorStub.deploy(timelock.address) 28 | await tGov.deployed() 29 | 30 | const ProxyAdminWithDeputy = await ethers.getContractFactory( 31 | "ProxyAdminWithDeputy" 32 | ) 33 | adminWithDeputy = await ProxyAdminWithDeputy.deploy( 34 | tGov.address, 35 | deputy.address 36 | ) 37 | await adminWithDeputy.deployed() 38 | }) 39 | 40 | describe("Plain Upgrades deployment - No ProxyAdminWithDeputy", () => { 41 | let newImplementationAddress 42 | let adminInstance 43 | 44 | beforeEach(async () => { 45 | newImplementationAddress = await upgrades.prepareUpgrade( 46 | storage.address, 47 | SimpleStorage, 48 | { 49 | constructorArgs: [2], 50 | } 51 | ) 52 | adminInstance = await upgrades.admin.getInstance() 53 | }) 54 | 55 | it("ProxyAdmin is the admin for the UpgradeableProxy", async () => { 56 | const adminAddress = await adminInstance.getProxyAdmin(storage.address) 57 | expect(adminInstance.address).to.equal(adminAddress) 58 | }) 59 | 60 | it("before upgrade, implementation version is 1", async () => { 61 | expect(await storage.implementationVersion()).to.equal(1) 62 | }) 63 | 64 | it("before upgrade, state is as expected", async () => { 65 | expect(await storage.storedValue()).to.equal(initialState) 66 | }) 67 | 68 | it("ProxyAdmin can upgrade, version is now 2 and state is correct", async () => { 69 | await adminInstance 70 | .connect(deployer) 71 | .upgrade(storage.address, newImplementationAddress) 72 | expect(await storage.storedValue()).to.equal(initialState) 73 | expect(await storage.implementationVersion()).to.equal(2) 74 | }) 75 | 76 | it("Deputy can't upgrade", async () => { 77 | await expect( 78 | adminInstance 79 | .connect(deputy) 80 | .upgrade(storage.address, newImplementationAddress) 81 | ).to.be.revertedWith("Ownable: caller is not the owner") 82 | }) 83 | }) 84 | 85 | describe("Patched Upgrades deployment - Using our ProxyAdminWithDeputy", () => { 86 | beforeEach(async () => { 87 | // Change admin of TransparentUpgradeableProxy to the 88 | // ProxyAdminWithDeputy contract 89 | await upgrades.admin.changeProxyAdmin( 90 | storage.address, 91 | adminWithDeputy.address 92 | ) 93 | }) 94 | 95 | it("ProxyAdminWithDeputy is the admin for the UpgradeableProxy", async () => { 96 | const adminAddress = await adminWithDeputy.getProxyAdmin(storage.address) 97 | expect(adminWithDeputy.address).to.equal(adminAddress) 98 | }) 99 | 100 | describe("Upgrades procedure with ProxyAdminWithDeputy", () => { 101 | let newImplementationAddress 102 | 103 | beforeEach(async () => { 104 | newImplementationAddress = await upgrades.prepareUpgrade( 105 | storage.address, 106 | SimpleStorage, 107 | { 108 | constructorArgs: [2], 109 | } 110 | ) 111 | }) 112 | 113 | it("before upgrade, implementation version is 1", async () => { 114 | expect(await storage.implementationVersion()).to.equal(1) 115 | }) 116 | 117 | it("before upgrade, state is as expected", async () => { 118 | expect(await storage.storedValue()).to.equal(initialState) 119 | }) 120 | 121 | it("Deputy can upgrade, version is now 2 and state is correct", async () => { 122 | await adminWithDeputy 123 | .connect(deputy) 124 | .upgrade(storage.address, newImplementationAddress) 125 | expect(await storage.storedValue()).to.equal(initialState) 126 | expect(await storage.implementationVersion()).to.equal(2) 127 | }) 128 | 129 | it("ProxyAdminWithDeputy's owner (the Timelock) can upgrade, version is now 2 and state is correct", async () => { 130 | await adminWithDeputy 131 | .connect(timelock) 132 | .upgrade(storage.address, newImplementationAddress) 133 | expect(await storage.storedValue()).to.equal(42) 134 | expect(await storage.implementationVersion()).to.equal(2) 135 | }) 136 | }) 137 | }) 138 | }) 139 | -------------------------------------------------------------------------------- /test/governance/StakerGovernor.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai") 2 | const { defaultAbiCoder } = require("@ethersproject/abi") 3 | 4 | const { mineBlocks } = helpers.time 5 | const { to1e18 } = helpers.number 6 | const { AddressZero } = ethers.constants 7 | 8 | const ProposalStates = { 9 | Pending: 0, 10 | Active: 1, 11 | Canceled: 2, 12 | } 13 | 14 | const secondsInADay = ethers.BigNumber.from(60 * 60 * 24) 15 | const averageBlockTime = 13 16 | 17 | describe("StakerGovernor", () => { 18 | let tToken 19 | let staker 20 | let whale 21 | let vetoer 22 | 23 | let proposalThresholdFunction 24 | 25 | // Initial scenario is 2 stakers, whose total amount is 30,000 tokens. 26 | const initialStakerBalance = to1e18(75) 27 | const whaleBalance = to1e18(30000 - 75) 28 | 29 | // Proposal threshold is 0.25%, so first stake (5 tokens) is below the 30 | // threshold. After the top-up, it should be over it. 31 | const firstStake = to1e18(5) 32 | const topUpAmount = to1e18(70) 33 | 34 | beforeEach(async () => { 35 | const T = await ethers.getContractFactory("T") 36 | tToken = await T.deploy() 37 | await tToken.deployed() 38 | 39 | const TestStaking = await ethers.getContractFactory( 40 | "TestStakingCheckpoints" 41 | ) 42 | tStaking = await TestStaking.deploy(tToken.address) 43 | await tStaking.deployed() 44 | ;[staker, whale, vetoer] = await ethers.getSigners() 45 | await tToken.mint(staker.address, initialStakerBalance) 46 | await tToken.mint(whale.address, whaleBalance) 47 | 48 | const TokenholderGovStub = await ethers.getContractFactory( 49 | "TestTokenholderGovernorStub" 50 | ) 51 | tokenholderGov = await TokenholderGovStub.deploy() 52 | 53 | const TestGovernor = await ethers.getContractFactory("TestStakerGovernor") 54 | tGov = await TestGovernor.deploy( 55 | tStaking.address, 56 | tokenholderGov.address, 57 | vetoer.address 58 | ) 59 | await tGov.deployed() 60 | 61 | // ethers.js can't resolve overloaded functions so we need to specify the 62 | // fully qualified signature of the function to call it. This is the case of 63 | // the `proposalThreshold()` function, as there's also a 64 | // `proposalThreshold(uint256)`. 65 | // See https://github.com/ethers-io/ethers.js/issues/1160 66 | proposalThresholdFunction = tGov["proposalThreshold()"] 67 | }) 68 | 69 | describe("initial parameters", () => { 70 | it("quorum denominator is 10000", async () => { 71 | expect(await tGov.FRACTION_DENOMINATOR()).to.equal(10000) 72 | }) 73 | 74 | it("quorum numerator is 150", async () => { 75 | expect(await tGov.quorumNumerator()).to.equal(150) 76 | }) 77 | 78 | it("proposal threshold numerator is 25", async () => { 79 | expect(await tGov.proposalThresholdNumerator()).to.equal(25) 80 | }) 81 | 82 | it("voting delay is 2 days", async () => { 83 | expect(await tGov.votingDelay()).to.equal( 84 | secondsInADay.mul(2).div(averageBlockTime) 85 | ) 86 | }) 87 | 88 | it("voting period is 10 days", async () => { 89 | expect(await tGov.votingPeriod()).to.equal( 90 | secondsInADay.mul(10).div(averageBlockTime) 91 | ) 92 | }) 93 | 94 | it("executor is Tokenholder DAO's timelock", async () => { 95 | const timelock = await tokenholderGov.timelock() 96 | expect(await tGov.executor()).to.equal(timelock) 97 | }) 98 | }) 99 | 100 | describe("when stakers deposit tokens", () => { 101 | const expectedTotalStake = whaleBalance.add(firstStake) 102 | const expectedThreshold = expectedTotalStake.mul(25).div(10000) 103 | const mockDescription = "Mock Proposal" 104 | const mockProposal = [[AddressZero], [42], [0xfabada]] 105 | const mockProposalWithDescription = [...mockProposal, mockDescription] 106 | 107 | beforeEach(async () => { 108 | await tToken.connect(whale).approve(tStaking.address, whaleBalance) 109 | await tStaking.connect(whale).deposit(whaleBalance) 110 | 111 | await tToken.connect(staker).approve(tStaking.address, firstStake) 112 | await tStaking.connect(staker).deposit(firstStake) 113 | 114 | lastBlock = (await mineBlocks(1)) - 1 115 | }) 116 | 117 | context("only whale has enough stake to propose", () => { 118 | it("proposal threshold is as expected", async () => { 119 | expect(await proposalThresholdFunction()).to.equal(expectedThreshold) 120 | }) 121 | 122 | it("whale can make a proposal", async () => { 123 | await tGov.connect(whale).propose(...mockProposalWithDescription) 124 | }) 125 | 126 | it("staker can't make a proposal", async () => { 127 | await expect( 128 | tGov.connect(staker).propose(...mockProposalWithDescription) 129 | ).to.be.revertedWith("Proposal below threshold") 130 | }) 131 | }) 132 | 133 | context("after top-up, staker has enough tokens to propose", () => { 134 | const newTotalStake = expectedTotalStake.add(topUpAmount) 135 | const newExpectedThreshold = newTotalStake.mul(25).div(10000) 136 | 137 | beforeEach(async () => { 138 | await tToken.connect(staker).approve(tStaking.address, topUpAmount) 139 | await tStaking.connect(staker).deposit(topUpAmount) 140 | lastBlock = (await mineBlocks(1)) - 1 141 | }) 142 | 143 | it("proposal threshold is as expected", async () => { 144 | expect(await proposalThresholdFunction()).to.equal(newExpectedThreshold) 145 | }) 146 | 147 | it("whale still can make a proposal", async () => { 148 | await tGov.connect(whale).propose(...mockProposalWithDescription) 149 | }) 150 | 151 | it("staker can make a proposal too", async () => { 152 | await tGov.connect(staker).propose(...mockProposalWithDescription) 153 | }) 154 | }) 155 | 156 | context("when there's a proposal", () => { 157 | const descriptionHash = ethers.utils.id(mockDescription) 158 | const mockProposalWithHash = [...mockProposal, descriptionHash] 159 | const proposalID = ethers.utils.keccak256( 160 | defaultAbiCoder.encode( 161 | ["address[]", "uint256[]", "bytes[]", "bytes32"], 162 | mockProposalWithHash 163 | ) 164 | ) 165 | 166 | beforeEach(async () => { 167 | await tGov.connect(whale).propose(...mockProposalWithDescription) 168 | }) 169 | 170 | it("proposal state is 'pending' initially", async () => { 171 | expect(await tGov.state(proposalID)).to.equal(ProposalStates.Pending) 172 | }) 173 | 174 | it("stakers can't cancel the proposal", async () => { 175 | await expect(tGov.connect(whale).cancel(...mockProposalWithHash)).to.be 176 | .reverted 177 | await expect(tGov.connect(staker).cancel(...mockProposalWithHash)).to.be 178 | .reverted 179 | }) 180 | 181 | it("vetoer can cancel the proposal", async () => { 182 | await tGov.connect(vetoer).cancel(...mockProposalWithHash) 183 | expect(await tGov.state(proposalID)).to.equal(ProposalStates.Canceled) 184 | }) 185 | }) 186 | }) 187 | }) 188 | -------------------------------------------------------------------------------- /test/governance/StakerGovernorVotes.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai") 2 | 3 | const { mineBlocks, lastBlockNumber } = helpers.time 4 | const { to1e18 } = helpers.number 5 | 6 | describe("StakerGovernorVotes", () => { 7 | let tToken 8 | 9 | // Staker has 5 T tokens 10 | let staker 11 | const initialStakerBalance = to1e18(5) 12 | 13 | beforeEach(async () => { 14 | const T = await ethers.getContractFactory("T") 15 | tToken = await T.deploy() 16 | await tToken.deployed() 17 | 18 | const TestStaking = await ethers.getContractFactory( 19 | "TestStakingCheckpoints" 20 | ) 21 | tStaking = await TestStaking.deploy(tToken.address) 22 | await tStaking.deployed() 23 | ;[staker, thirdParty] = await ethers.getSigners() 24 | await tToken.mint(staker.address, initialStakerBalance) 25 | await tToken.connect(staker).delegate(staker.address) 26 | 27 | const TestVotes = await ethers.getContractFactory("TestStakerGovernorVotes") 28 | tVotes = await TestVotes.deploy(tStaking.address) 29 | await tVotes.deployed() 30 | }) 31 | 32 | describe("setup", () => { 33 | context("static parameters", () => { 34 | it("quorum denominator is 10000", async () => { 35 | expect(await tVotes.FRACTION_DENOMINATOR()).to.equal(10000) 36 | }) 37 | 38 | it("quorum numerator is 125", async () => { 39 | expect(await tVotes.quorumNumerator()).to.equal(125) 40 | }) 41 | }) 42 | 43 | context("once deployed", () => { 44 | let atLastBlock 45 | beforeEach(async () => { 46 | atLastBlock = (await lastBlockNumber()) - 1 47 | }) 48 | 49 | it("staked balance should be 0", async () => { 50 | expect(await tStaking.stake(staker.address)).to.equal(0) 51 | }) 52 | 53 | it("past votes at last block should be 0", async () => { 54 | expect(await tVotes.getVotes(staker.address, atLastBlock)).to.equal(0) 55 | }) 56 | 57 | it("total supply at last block should be 0", async () => { 58 | expect(await tVotes.getPastTotalSupply(atLastBlock)).to.equal(0) 59 | }) 60 | }) 61 | 62 | context("after a deposit", () => { 63 | let lastBlock 64 | const stake = to1e18(4) 65 | beforeEach(async () => { 66 | await tToken.connect(staker).approve(tStaking.address, stake) 67 | await tStaking.connect(staker).deposit(stake) 68 | lastBlock = (await mineBlocks(1)) - 1 69 | }) 70 | 71 | it("liquid balance should decrease by 4", async () => { 72 | expect(await tToken.balanceOf(staker.address)).to.equal( 73 | initialStakerBalance.sub(stake) 74 | ) 75 | }) 76 | 77 | it("staked balance should be 4", async () => { 78 | expect(await tStaking.stake(staker.address)).to.equal(stake) 79 | }) 80 | 81 | it("past votes at last block should be 4", async () => { 82 | expect(await tVotes.getVotes(staker.address, lastBlock)).to.equal(stake) 83 | }) 84 | 85 | it("total supply at last block should be 4", async () => { 86 | expect(await tVotes.getPastTotalSupply(lastBlock)).to.equal(stake) 87 | }) 88 | 89 | it("quorum at last block should be 1.25% (125/10000) of 4", async () => { 90 | expect(await tVotes.quorum(lastBlock)).to.equal( 91 | stake.mul(125).div(10000) 92 | ) 93 | }) 94 | }) 95 | 96 | context("after a withdrawal", () => { 97 | let lastBlock 98 | const stake = to1e18(4) 99 | const withdrawal = to1e18(2) 100 | const newStake = stake.sub(withdrawal) 101 | beforeEach(async () => { 102 | await tToken.connect(staker).approve(tStaking.address, stake) 103 | await tStaking.connect(staker).deposit(stake) 104 | await tStaking.connect(staker).withdraw(withdrawal) 105 | lastBlock = (await mineBlocks(1)) - 1 106 | }) 107 | 108 | it("liquid balance should increase by 2", async () => { 109 | expect(await tToken.balanceOf(staker.address)).to.equal( 110 | initialStakerBalance.sub(newStake) 111 | ) 112 | }) 113 | 114 | it("staked balance should be 2", async () => { 115 | expect(await tStaking.stake(staker.address)).to.equal(newStake) 116 | }) 117 | 118 | it("past votes at last block should be 2", async () => { 119 | expect(await tVotes.getVotes(staker.address, lastBlock)).to.equal( 120 | newStake 121 | ) 122 | }) 123 | 124 | it("total supply at last block should be 2", async () => { 125 | expect(await tVotes.getPastTotalSupply(lastBlock)).to.equal(newStake) 126 | }) 127 | 128 | it("quorum at last block should be 1.25% (125/10000) of 2", async () => { 129 | expect(await tVotes.quorum(lastBlock)).to.equal( 130 | newStake.mul(125).div(10000) 131 | ) 132 | }) 133 | }) 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /test/governance/TokenholderGovernorVotes.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai") 2 | 3 | const { lastBlockNumber } = helpers.time 4 | const { to1e18 } = helpers.number 5 | 6 | describe("TokenholderGovernorVotes", () => { 7 | let tToken 8 | 9 | // Staker has 5 T tokens 10 | let staker 11 | const initialStakerBalance = to1e18(5) 12 | 13 | beforeEach(async () => { 14 | const T = await ethers.getContractFactory("T") 15 | tToken = await T.deploy() 16 | await tToken.deployed() 17 | 18 | const TestStaking = await ethers.getContractFactory( 19 | "TestStakingCheckpoints" 20 | ) 21 | tStaking = await TestStaking.deploy(tToken.address) 22 | await tStaking.deployed() 23 | ;[staker, thirdParty] = await ethers.getSigners() 24 | await tToken.mint(staker.address, initialStakerBalance) 25 | await tToken.connect(staker).delegate(staker.address) 26 | 27 | const TestVotes = await ethers.getContractFactory( 28 | "TestTokenholderGovernorVotes" 29 | ) 30 | tVotes = await TestVotes.deploy(tToken.address, tStaking.address) 31 | await tVotes.deployed() 32 | }) 33 | 34 | describe("setup", () => { 35 | context("static parameters", () => { 36 | it("quorum denominator is 10000", async () => { 37 | expect(await tVotes.FRACTION_DENOMINATOR()).to.equal(10000) 38 | }) 39 | 40 | it("quorum numerator is 125", async () => { 41 | expect(await tVotes.quorumNumerator()).to.equal(125) 42 | }) 43 | }) 44 | 45 | context("once deployed", () => { 46 | let atLastBlock 47 | beforeEach(async () => { 48 | atLastBlock = (await lastBlockNumber()) - 1 49 | }) 50 | 51 | it("liquid balance should be 5", async () => { 52 | expect(await tToken.balanceOf(staker.address)).to.equal( 53 | initialStakerBalance 54 | ) 55 | }) 56 | 57 | it("staked balance should be 0", async () => { 58 | expect(await tStaking.stake(staker.address)).to.equal(0) 59 | }) 60 | 61 | it("past votes at last block should be 5", async () => { 62 | expect(await tVotes.getVotes(staker.address, atLastBlock)).to.equal( 63 | initialStakerBalance 64 | ) 65 | }) 66 | 67 | it("total supply at last block should be 5", async () => { 68 | expect(await tVotes.getPastTotalSupply(atLastBlock)).to.equal( 69 | initialStakerBalance 70 | ) 71 | }) 72 | 73 | it("quorum at last block should be 1.25% (125/10000) of 5", async () => { 74 | expect(await tVotes.quorum(atLastBlock)).to.equal( 75 | initialStakerBalance.mul(125).div(10000) 76 | ) 77 | }) 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /test/helpers/contract-test-helpers.js: -------------------------------------------------------------------------------- 1 | module.exports.MAX_UINT96 = ethers.BigNumber.from( 2 | "79228162514264337593543950335" 3 | ) 4 | -------------------------------------------------------------------------------- /tsconfig.export.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "esModuleInterop": true, 6 | "outDir": "export" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["./hardhat.config.ts"], 3 | "include": ["./deploy"] 4 | } 5 | --------------------------------------------------------------------------------