diff --git a/.env.alfajores b/.env.alfajores index 0fdc929ba61..b243f5ef99e 100644 --- a/.env.alfajores +++ b/.env.alfajores @@ -23,7 +23,7 @@ CELOSTATS_BANNED_ADDRESSES="" CELOSTATS_RESERVED_ADDRESSES="" ORACLE_DOCKER_IMAGE_REPOSITORY="us-west1-docker.pkg.dev/celo-testnet-production/celo-oracle/celo-oracle" -ORACLE_DOCKER_IMAGE_TAG="1.0.12" +ORACLE_DOCKER_IMAGE_TAG="459947a" AZURE_ORACLE_CENTRALUS_AZURE_SUBSCRIPTION_ID=7a6f5f20-bd43-4267-8c35-a734efca140c AZURE_ORACLE_CENTRALUS_AZURE_TENANT_ID=7cb7628a-e37c-4afb-8332-2029e418980e @@ -40,6 +40,9 @@ AZURE_ORACLE_CENTRALUS_EUROCEUR_ORACLE_ADDRESSES_FROM_MNEMONIC_COUNT=4 AZURE_ORACLE_CENTRALUS_CELOXOF_ORACLE_ADDRESSES_FROM_MNEMONIC_COUNT=4 AZURE_ORACLE_CENTRALUS_EURXOF_ORACLE_ADDRESSES_FROM_MNEMONIC_COUNT=4 AZURE_ORACLE_CENTRALUS_EUROCXOF_ORACLE_ADDRESSES_FROM_MNEMONIC_COUNT=4 +AZURE_ORACLE_CENTRALUS_KESUSD_ORACLE_ADDRESSES_FROM_MNEMONIC_COUNT=4 +AZURE_ORACLE_CENTRALUS_CELOKES_ORACLE_ADDRESSES_FROM_MNEMONIC_COUNT=4 +AZURE_ORACLE_CENTRALUS_USDTUSD_ORACLE_ADDRESSES_FROM_MNEMONIC_COUNT=4 AZURE_ORACLE_CENTRALUS_FULL_NODES_COUNT=2 AZURE_ORACLE_CENTRALUS_FULL_NODES_DISK_SIZE=30 AZURE_ORACLE_CENTRALUS_FULL_NODES_ROLLING_UPDATE_PARTITION=0 diff --git a/.env.baklava b/.env.baklava index 81ddf2d77cb..ad905f981f6 100644 --- a/.env.baklava +++ b/.env.baklava @@ -36,7 +36,7 @@ CELOCLI_STANDALONE_IMAGE_REPOSITORY="gcr.io/celo-testnet/celocli-standalone" CELOCLI_STANDALONE_IMAGE_TAG="0.0.30-beta2" ORACLE_DOCKER_IMAGE_REPOSITORY="us-west1-docker.pkg.dev/celo-testnet-production/celo-oracle/celo-oracle" -ORACLE_DOCKER_IMAGE_TAG="1.0.12" +ORACLE_DOCKER_IMAGE_TAG="459947a" # ---- Full Node Chain Restore ---- @@ -68,6 +68,9 @@ AZURE_ORACLE_WESTUS2_EUROCEUR_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x6866e306b32acae7 AZURE_ORACLE_WESTUS2_CELOXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x96eda2cad69c8cd1daeb80da86d24825f45f46b7:baklava-celoxof-oracle2,0x4e9d441fd1c77222395a1853d851fea8a0e3aed8:baklava-celoxof-oracle3 AZURE_ORACLE_WESTUS2_EURXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x7fe5f297dd812ca21e7bf1cbf145a0b59227b35f:baklava-eurxof-oracle2,0x2addc69c2ce3a9d93a8291419319bf7f0a2c6c82:baklava-eurxof-oracle3 AZURE_ORACLE_WESTUS2_EUROCXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x729e058e97c099c79af674bbe2f687171432dd17:baklava-eurocxof-oracle2,0xd226aa9ee80ee282339c1ae69f3f811dbe5d895a:baklava-eurocxof-oracle4 +AZURE_ORACLE_WESTUS2_CELOKES_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x84f0d0c9385de3509cdf6eb2fb168e35b0dbad92:baklava-celokes-oracle2,0x2db4d3bf7e744b422812b63b036c401828be7778:baklava-celokes-oracle3 +AZURE_ORACLE_WESTUS2_KESUSD_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x94cd5463902630dd22db8ac41242002e6a7a6844:baklava-kesusd-oracle2,0xd3e70b118b674c4db7fde6946b16070bf9ec5ce3:baklava-kesusd-oracle3 +AZURE_ORACLE_WESTUS2_USDTUSD_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x9829cf05869f1b9770f4ce9d5653909f1f9e4c5e:baklava-usdtusd-oracle2,0xdfbcbae6de4fb7b72dbad402b975e374441395ea:baklava-usdtusd-oracle3 AZURE_ORACLE_WESTUS2_FULL_NODES_COUNT=2 AZURE_ORACLE_WESTUS2_FULL_NODES_ROLLING_UPDATE_PARTITION=0 AZURE_ORACLE_WESTUS2_FULL_NODES_DISK_SIZE=30 @@ -94,6 +97,9 @@ AZURE_ORACLE_CENTRALUS_EUROCEUR_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x9a0613e8a1ff6c AZURE_ORACLE_CENTRALUS_CELOXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xd056a29e86161a34692c34f4c95933b59de077dc:baklava-celoxof-oracle0,0x5ad07f89176298ae3a0f3d20d0b4a756307d46e7:baklava-celoxof-oracle1 AZURE_ORACLE_CENTRALUS_EURXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xa4a46db00840e6525ffe79aee5990abaebb7479d:baklava-eurxof-oracle0,0x6e537c9462ed968ff08eab430c5f8c11eab7df1a:baklava-eurxof-oracle1 AZURE_ORACLE_CENTRALUS_EUROCXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x1a637c38671512866317475d19df5f55b0802276:baklava-eurocxof-oracle0,0x8589f0bb307581b96877f9e1a5ce3fcb05127fd0:baklava-eurocxof-oracle1 +AZURE_ORACLE_CENTRALUS_CELOKES_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x0468aabc726f2f8d6bc612af99bf994026654a34:baklava-celokes-oracle0,0x8fc0c18b0fc7c11d4af89f0be046ed17dd1fe0f4:baklava-celokes-oracle1 +AZURE_ORACLE_CENTRALUS_KESUSD_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xf44345ff4ae8a3e18ae3e7d9c6b3de62736fb01c:baklava-kesusd-oracle0,0xb9410ac25ae1424190f6b4e45dcabd4d32168e5f:baklava-kesusd-oracle1 +AZURE_ORACLE_CENTRALUS_USDTUSD_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x2b4b450daecaf5a011497762380cdf1938791f85:baklava-usdtusd-oracle0,0x179282dcbf4c506332b0376cf5bcebd6ca9ec2f3:baklava-usdtusd-oracle1 AZURE_ORACLE_CENTRALUS_FULL_NODES_COUNT=2 AZURE_ORACLE_CENTRALUS_FULL_NODES_ROLLING_UPDATE_PARTITION=0 AZURE_ORACLE_CENTRALUS_FULL_NODES_DISK_SIZE=30 diff --git a/.env.rc1 b/.env.rc1 index 25b860514cf..1012a6eb2da 100644 --- a/.env.rc1 +++ b/.env.rc1 @@ -45,7 +45,7 @@ CELOCLI_STANDALONE_IMAGE_TAG="0.0.42" MOCK_ORACLE_CRON_SCHEDULE="*/5 * * * *" ORACLE_DOCKER_IMAGE_REPOSITORY="us-west1-docker.pkg.dev/celo-testnet-production/celo-oracle/celo-oracle" -ORACLE_DOCKER_IMAGE_TAG="1.0.12" +ORACLE_DOCKER_IMAGE_TAG="4c63b13" ORACLE_UNUSED_ORACLE_ADDRESSES=0xB93Fe7906ea4221b3fbe23412D18Ab1B07FE2F71,0x8d25D74E43789079Ef3C6B965c3D22b63A1233aC,0xCD88Cc79342a7cFE78E91FAa173eC87704bDcA9a,0x5091110175318A2A8aF88309D1648c1D84d31B29,0xBBd6e54Af7A5722f42461C6313F37Bd50729F195,0xE23a4c6615669526Ab58E9c37088bee4eD2b2dEE @@ -80,6 +80,8 @@ AZURE_ORACLE_WESTUS_EUROCEUR_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x6e7c84f8377856901 AZURE_ORACLE_WESTUS_CELOXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xce696d465dde582095fce8b67e1a31ceb45ad922:mainnet-celoxof-wus0,0xbc211b8dfecdd5784f9c419ce64f7de1377bae88:mainnet-celoxof-wus1,0xc659ab5c049b726c2945a8a44b783ce6afbd2ceb:mainnet-celoxof-wus2,0x9094bf2b2eb028c6fcc56e7d46ea28bb6e03c9a5:mainnet-celoxof-wus3,0xb947c54be882314623ee3d74684d0d785dd50335:mainnet-celoxof-wus4 AZURE_ORACLE_WESTUS_EURXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x6a033b7217fbae843a3ffc9783ef9f87dd3a1c04:mainnet-eurxof-wus0,0x8a164c0523bbd7ec70172807723cca9a948858bb:mainnet-eurxof-wus1,0xd0066f198ed7f8dc3684ff3ac77511ef58a9aed3:mainnet-eurxof-wus2,0x441061f8b1f8ee2722d3608bfa0b5c4c14dee813:mainnet-eurxof-wus3,0x87089ec6adbf3c994ae7c47d3aa7d4fc104d0422:mainnet-eurxof-wus4 AZURE_ORACLE_WESTUS_EUROCXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xc5a86597d514b423579684cdf9f49b6df37e3689:mainnet-eurocxof-wus0,0x8e1423ca0bcb15093f52d1d07675e0aa04e3da75:mainnet-eurocxof-wus1,0xa47e6a8a7db5ee22b5293704a4f0f5f8fdaab06f:mainnet-eurocxof-wus2,0x77d148efdd40202d0eec787073a70c7f6bc9c485:mainnet-eurocxof-wus3,0xfef8748fd3f039fb8cfa77c7744b171f4396659c:mainnet-eurocxof-wus4 +AZURE_ORACLE_WESTUS_CELOKES_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x7b50b90144ce27557ed352d499a13f458aef74d0:mainnet-celokes-wus0,0x0eb570af5ab2a9eea97bb413d7dcc12edbf87172:mainnet-celokes-wus1 +AZURE_ORACLE_WESTUS_KESUSD_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xf72c29b7047166c6576378d976c44c58fa767bb9:mainnet-kesusd-wus0,0x5bcc4f89b1176f5e68269d232d2c5b274ad1d81e:mainnet-kesusd-wus1 AZURE_ORACLE_WESTUS_FULL_NODES_COUNT=5 AZURE_ORACLE_WESTUS_FULL_NODES_ROLLING_UPDATE_PARTITION=0 AZURE_ORACLE_WESTUS_FULL_NODES_DISK_SIZE=100 @@ -106,6 +108,8 @@ AZURE_ORACLE_WESTEUROPE_EUROCEUR_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xa633c79ac2c68 AZURE_ORACLE_WESTEUROPE_CELOXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0x4d89a0c95de82ae78c42fad4f8d3f87c4495fd37:mainnet-celoxof-weu0,0xa97dbefac6026f93cc5714c4c150b7466e9502ef:mainnet-celoxof-weu1,0x676931c73c8d6b09b0c192baf821e3fd2d693750:mainnet-celoxof-weu2,0xfb8f294c8cd98cf059672c1a6153f85555f10a90:mainnet-celoxof-weu3,0xb7614f7174a07028a5ff5e1adc68a031b646857f:mainnet-celoxof-weu4 AZURE_ORACLE_WESTEUROPE_EURXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xd2c4f59724df51026f857a7e188b322e35256e24:mainnet-eurxof-weu0,0xe47c9867dbb37110834aaaf65b8d760c49c22081:mainnet-eurxof-weu1,0x9cb4896447a8f2611f5fb6f5fc853ffa16a1d864:mainnet-eurxof-weu2,0x0f9786b083c8c22e2e839286230098048a20a0ec:mainnet-eurxof-weu3,0xe01890c7760445908128f0e64e1170866566e1f6:mainnet-eurxof-weu4 AZURE_ORACLE_WESTEUROPE_EUROCXOF_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xdda1d71f3d5a6090bc04b77a18925fab7054d9c3:mainnet-eurocxof-weu0,0xee1d05f81e90b8ece440de6141282404e83830ce:mainnet-eurocxof-weu1,0xff6e35c6119742fd1eb3db780d976c4e55585108:mainnet-eurocxof-weu2,0x59eac333453279e71a3a98b4b72bdfa99ca51ad3:mainnet-eurocxof-weu3,0x378b95092bed2acb0d3ae6ab9c045eef1c250872:mainnet-eurocxof-weu4 +AZURE_ORACLE_WESTEUROPE_CELOKES_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xcfba5ea29501d26fc3e6e0851bb13375ea0401ef:mainnet-celokes-weu0,0xe0a634c4aac4494930ba50f59b751a5ddabe4679:mainnet-celokes-weu1,0xe000bce6c6f87ac39f3c4f4b5daa38dd32433217:mainnet-celokes-weu2 +AZURE_ORACLE_WESTEUROPE_KESUSD_ORACLE_ADDRESS_AZURE_KEY_VAULTS=0xeab3df01269abd314465148e0c075d49fbd4b59b:mainnet-kesusd-weu0,0x28cd8a609560fb1ff1011387e4c40deabef029c0:mainnet-kesusd-weu1,0xf6d2d7ec798ae1b80046594345805298e0ac1624:mainnet-kesusd-weu2 AZURE_ORACLE_WESTEUROPE_FULL_NODES_COUNT=5 AZURE_ORACLE_WESTEUROPE_FULL_NODES_ROLLING_UPDATE_PARTITION=0 AZURE_ORACLE_WESTEUROPE_FULL_NODES_DISK_SIZE=100 diff --git a/.github/workflows/celo-monorepo.yml b/.github/workflows/celo-monorepo.yml index 6cdefcd3929..a60af96775e 100644 --- a/.github/workflows/celo-monorepo.yml +++ b/.github/workflows/celo-monorepo.yml @@ -8,10 +8,19 @@ on: push: branches: - master + - 'release/**' + - 'feat/l2-epoch-system' + - 'martinvol/**' + - 'pahor167/**' + - 'soloseng/**' pull_request: branches: - master - 'release/**' + - 'feat/l2-epoch-system' + - 'martinvol/**' + - 'pahor167/**' + - 'soloseng/**' concurrency: group: celo-monorepo-${{ github.ref }} @@ -23,7 +32,7 @@ defaults: env: # Increment these to force cache rebuilding - NODE_MODULE_CACHE_VERSION: 7 + NODE_MODULE_CACHE_VERSION: 8 NODE_OPTIONS: '--max-old-space-size=4096' TERM: dumb GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.parallel=false -Dorg.gradle.configureondemand=true -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"' @@ -194,7 +203,7 @@ jobs: timeout-minutes: 60 needs: [install-dependencies, lint-checks] if: | - github.base_ref == 'master' || contains(github.base_ref, 'staging') || contains(github.base_ref, 'production') || + github.base_ref == 'master' || contains(github.base_ref, 'release') || contains(github.base_ref, 'staging') || contains(github.base_ref, 'production') || contains(needs.install-dependencies.outputs.all_modified_files, 'packages/protocol') || contains(needs.install-dependencies.outputs.all_modified_files, 'packages/typescript') || contains(needs.install-dependencies.outputs.all_modified_files, ',package.json') || diff --git a/.github/workflows/containers.yaml b/.github/workflows/containers.yaml index 7d0a8830158..31fcf296564 100644 --- a/.github/workflows/containers.yaml +++ b/.github/workflows/containers.yaml @@ -8,6 +8,7 @@ on: - 'packages/celotool/**' branches: - master + - 'release/**' pull_request: paths: - 'dockerfiles/**' diff --git a/.github/workflows/protocol-devchain-anvil.yml b/.github/workflows/protocol-devchain-anvil.yml index de0608208c5..483a5dcb6d6 100644 --- a/.github/workflows/protocol-devchain-anvil.yml +++ b/.github/workflows/protocol-devchain-anvil.yml @@ -3,10 +3,25 @@ on: push: branches: - master + - 'release/**' + tags: + - core-contracts.v* + pull_request: + branches: [release/core-contracts/*, master] + paths: + - 'packages/protocol/**' + workflow_dispatch: + inputs: + npm_tag: + description: 'NPM TAG e.g. alpha, pre-merge (default: canary) ' + required: true + type: string env: # Increment these to force cache rebuilding FOUNDRY_CACHE_KEY: 1 + # Supported Foundry version defined at celo-org (GitHub organisation) level, for consistency across workflows. + SUPPORTED_FOUNDRY_VERSION: ${{ vars.SUPPORTED_FOUNDRY_VERSION }} jobs: build: @@ -14,8 +29,8 @@ jobs: run: working-directory: packages/protocol name: Generate anvil - runs-on: ["self-hosted", "org", "npm-publish"] - permissions: + runs-on: ['self-hosted', 'org', 'npm-publish'] + permissions: contents: read pull-requests: read id-token: write @@ -32,13 +47,13 @@ jobs: fi - name: Foundry cache id: foundry-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./cache key: ${{ runner.os }}-foundry-cache-${{ env.FOUNDRY_CACHE_KEY }} - name: Foundry out id: foundry-out - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ./out key: ${{ runner.os }}-foundry-out-${{ env.FOUNDRY_CACHE_KEY }} @@ -52,14 +67,14 @@ jobs: uses: actions/github-script@v7 with: script: | - const result = ( - await github.rest.repos.listPullRequestsAssociatedWithCommit({ - commit_sha: context.sha, - owner: context.repo.owner, - repo: context.repo.repo, - }) - ).data[0]; - core.setOutput("number", result ? result.number : ""); + const result = ( + await github.rest.repos.listPullRequestsAssociatedWithCommit({ + commit_sha: context.sha, + owner: context.repo.owner, + repo: context.repo.repo, + }) + ).data[0]; + core.setOutput("number", result ? result.number : ""); - name: Set PR Number id: set_pr_number @@ -77,7 +92,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: "nightly-f625d0fa7c51e65b4bf1e8f7931cd1c6e2e285e9" + version: 'nightly-fa0e0c2ca3ae75895dd19173a02faf88509c0608' - name: Install forge dependencies run: forge install @@ -86,8 +101,53 @@ jobs: id: date run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - name: Generate migrations - run: ./migrations_sol/run_integration_tests_in_anvil.sh + - name: Akeyless Get Secrets + id: get_auth_token + uses: docker://us-west1-docker.pkg.dev/devopsre/akeyless-public/akeyless-action:latest + with: + api-url: https://api.gateway.akeyless.celo-networks-dev.org + access-id: p-kf9vjzruht6l + static-secrets: '{"/static-secrets/NPM/npm-publish-token":"NPM_TOKEN"}' + + - uses: actions/setup-node@v4 + with: + node-version: '18.x' + registry-url: 'https://registry.npmjs.org' + + - name: 'Setup yarn' + shell: bash + run: | + npm install --global yarn + source ~/.bashrc + + - name: 'Install packages' + shell: bash + run: yarn + + # Starting L1 from scratch instead of JSON state to circumvent this Anvil bug https://github.com/foundry-rs/foundry/issues/7502 + # Install `lsof` dependency, because it's not readily available on CI, but is required by + # `create_and_migrate_anvil_l2_devchain.sh`, because it uses `stop_anvil.sh` to kill + # existing anvil servers. + - name: Generate L1 migrations and run migration tests against L1 devchain + if: success() || failure() + run: | + sudo apt-get update + sudo apt-get install -y lsof + + source ./scripts/foundry/constants.sh + + echo "Starting L1 from scratch to circumvent Anvil bug" + source ./scripts/foundry/create_and_migrate_anvil_devchain.sh + + FOUNDRY_PROFILE=devchain forge test -vvv \ + --match-path "test-sol/devchain/migration/*" \ + --fork-url $ANVIL_RPC_URL + + ./scripts/foundry/stop_anvil.sh + + - name: Generate L2 migration + if: success() || failure() + run: ./scripts/foundry/create_and_migrate_anvil_l2_devchain.sh - name: Sanitize ref name id: sanitize-ref-name @@ -95,11 +155,22 @@ jobs: sanitized_ref_name=$(echo "${{ github.ref_name }}" | tr -cd '[:alnum:]-_') echo "sanitized_ref_name=${sanitized_ref_name}" >> $GITHUB_ENV - - name: Set package.json version based on GitHub ref - run: | - VERSION=${{ env.PR_NUMBER }} - echo "Setting version to 0.0.$VERSION" - jq ".version = \"0.0.$VERSION\"" .tmp/package.json > .tmp/temp.json && mv .tmp/temp.json .tmp/package.json + - name: + Determine release type and version (or dry run) + # This is what sets the RELEASE_TYPE and RELEASE_VERSION env variables + run: yarn --silent determine-release-version >> "$GITHUB_ENV" + working-directory: packages/protocol + env: + GITHUB_TAG: ${{ github.ref_name }} + NPM_PACKAGE: '@celo/devchain-anvil' + NPM_TAG: ${{ inputs.npm_tag }} + + - name: Prepare package for publishing + run: yarn prepare_devchain_anvil_publishing + working-directory: packages/protocol + env: + RELEASE_TYPE: ${{ env.RELEASE_TYPE }} + RELEASE_VERSION: ${{ env.RELEASE_VERSION }} - name: Upload devchain as artifact uses: actions/upload-artifact@v4 @@ -110,23 +181,13 @@ jobs: # https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#artifact-and-log-retention-policy retention-days: 90 - - name: Akeyless Get Secrets - id: get_auth_token - uses: docker://us-west1-docker.pkg.dev/devopsre/akeyless-public/akeyless-action:latest - with: - api-url: https://api.gateway.akeyless.celo-networks-dev.org - access-id: p-kf9vjzruht6l - static-secrets: '{"/static-secrets/NPM/npm-publish-token":"NPM_TOKEN"}' - - - uses: actions/setup-node@v4 - with: - node-version: '18.x' - registry-url: 'https://registry.npmjs.org' - - name: Publish @celo/devchain-anvil run: | cat package.json - npm publish --access public + npm publish $RELEASE_TYPE $DRY_RUN --access public working-directory: packages/protocol/.tmp env: + RELEASE_TYPE: --tag ${{ env.RELEASE_TYPE != '' && env.RELEASE_TYPE || 'canary' }} + RELEASE_VERSION: ${{ env.RELEASE_VERSION }} NODE_AUTH_TOKEN: ${{ env.NPM_TOKEN }} + DRY_RUN: ${{ env.RELEASE_VERSION == '' && '--dry-run' || '' }} diff --git a/.github/workflows/protocol_tests.yml b/.github/workflows/protocol_tests.yml index 10eff4b0881..c6025ebe8c8 100644 --- a/.github/workflows/protocol_tests.yml +++ b/.github/workflows/protocol_tests.yml @@ -3,14 +3,25 @@ on: push: branches: - master + - 'release/**' + - 'feat/l2-epoch-system' + - 'martinvol/**' + - 'pahor167/**' + - 'soloseng/**' pull_request: branches: - master - 'release/**' + - 'feat/l2-epoch-system' + - 'martinvol/**' + - 'pahor167/**' + - 'soloseng/**' env: # Increment these to force cache rebuilding FOUNDRY_CACHE_KEY: 2 + # Supported Foundry version defined at celo-org (GitHub organisation) level, for consistency across workflows. Please contact DevOps to update value. + SUPPORTED_FOUNDRY_VERSION: ${{ vars.SUPPORTED_FOUNDRY_VERSION }} jobs: check: @@ -51,7 +62,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: "nightly-f625d0fa7c51e65b4bf1e8f7931cd1c6e2e285e9" + version: 'nightly-fa0e0c2ca3ae75895dd19173a02faf88509c0608' # TODO: revert back to env var - name: Install forge dependencies run: forge install @@ -61,30 +72,46 @@ jobs: - name: Compile Contracts run: forge --version && forge compile - - name: Run tests common + - name: Run unit tests common # can't use gas limit because some setUp function use more than the limit - run: forge test -vvv --match-path "test-sol/common/*" # --block-gas-limit 50000000 + run: | + forge test -vvv \ + --match-path "test-sol/unit/common/*" - - name: Run tests governance/network + - name: Run unit tests governance/network if: success() || failure() - run: forge test -vvv --block-gas-limit 50000000 --match-path "test-sol/governance/network/*" + run: | + forge test -vvv \ + --match-path "test-sol/unit/governance/network/*" \ + --block-gas-limit 50000000 - - name: Run tests governance/validators + - name: Run unit tests governance/validators if: success() || failure() - run: forge test -vvv --block-gas-limit 50000000 --match-path "test-sol/governance/validators/*" + run: | + forge test -vvv \ + --match-path "test-sol/unit/governance/validators/*" \ + --block-gas-limit 50000000 - - name: Run tests governance/voting + - name: Run unit tests governance/voting # can't use gas limit because some setUp function use more than the limit if: success() || failure() - run: forge test -vvv --match-path "test-sol/governance/voting/*" --block-gas-limit 50000000 + run: | + forge test -vvv \ + --match-path "test-sol/unit/governance/voting/*" - - name: Run tests stability + - name: Run unit tests stability if: success() || failure() - run: forge test -vvv --block-gas-limit 50000000 --match-path "test-sol/stability/*" + run: | + forge test -vvv \ + --match-path "test-sol/unit/stability/*" \ + --block-gas-limit 50000000 - - name: Run tests identity + - name: Run unit tests identity if: success() || failure() - run: forge test -vvv --block-gas-limit 50000000 --match-path "test-sol/identity/*" + run: | + forge test -vvv \ + --match-path "test-sol/unit/identity/*" \ + --block-gas-limit 50000000 - name: Fail if there are tests without folder if: success() || failure() @@ -94,11 +121,36 @@ jobs: exit 1 fi - - name: Run Everything just in case something was missed + - name: Run all unit tests in case some were missed (excl. integration and e2e tests) # can't use gas limit because some setUp function use more than the limit - # Excludes integration tests, because they require an anvil devchain running on the localhost. - run: forge test -vvv --no-match-contract RegistryIntegrationTest + # Excludes e2e and integration tests, because they require a connection to an anvil devchain + # serving at localhost. + run: | + forge test -vvv \ + --match-path "test-sol/unit/*" - - name: Generate migrations + - name: Run integration tests (that don't require an anvil devchain) if: success() || failure() - run: ./migrations_sol/run_integration_tests_in_anvil.sh + run: | + forge test -vvv \ + --match-path "test-sol/integration/*" \ + + - name: Generate migrations and run devchain + if: success() || failure() + run: ./scripts/foundry/create_and_migrate_anvil_devchain.sh + + - name: Run migration tests against local anvil devchain + run: | + source ./scripts/foundry/constants.sh + + FOUNDRY_PROFILE=devchain forge test -vvv \ + --match-path "test-sol/devchain/migration/*" \ + --fork-url $ANVIL_RPC_URL + + - name: Run e2e tests against local anvil devchain + run: | + source ./scripts/foundry/constants.sh + + FOUNDRY_PROFILE=devchain forge test -vvv \ + --match-path "test-sol/devchain/e2e/*" \ + --fork-url $ANVIL_RPC_URL diff --git a/.github/workflows/publish-contracts-abi-release.yml b/.github/workflows/publish-contracts-abi-release.yml index e7f4e7158a6..ae842861100 100644 --- a/.github/workflows/publish-contracts-abi-release.yml +++ b/.github/workflows/publish-contracts-abi-release.yml @@ -59,6 +59,7 @@ jobs: working-directory: packages/protocol env: GITHUB_TAG: ${{ github.ref_name }} + NPM_PACKAGE: "@celo/contracts" NPM_TAG: ${{ inputs.npm_tag }} - name: 'Build packages' shell: bash diff --git a/.gitmodules b/.gitmodules index 5d63bcd6e59..f3440efa785 100644 --- a/.gitmodules +++ b/.gitmodules @@ -20,3 +20,7 @@ path = packages/protocol/lib/celo-foundry url = https://github.com/celo-org/celo-foundry branch = celo-foundry-v0.5.13 +[submodule "packages/protocol/lib/solidity-bytes-utils-8"] + path = packages/protocol/lib/solidity-bytes-utils-8 + url = https://github.com/GNSPS/solidity-bytes-utils + branch = master diff --git a/packages/celotool/src/cmds/account/list.ts b/packages/celotool/src/cmds/account/list.ts deleted file mode 100644 index dae437d275b..00000000000 --- a/packages/celotool/src/cmds/account/list.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* tslint:disable no-console */ -import fetch from 'node-fetch' -import { switchToClusterFromEnv } from 'src/lib/cluster' -import { getBlockscoutUrl } from 'src/lib/endpoints' -import { Argv } from 'yargs' -import { AccountArgv } from '../account' - -export const command = 'list' -export const describe = 'Command for listing users' - -type ListArgv = AccountArgv - -// Known accounts for argentinaproduction -const knownAccounts = new Map([ - ['0xd4662aed4aa8719cd7dd9bc01034627951184a64', "Will's Phone number +5491126196048"], - ['0x5feb9f304d8309f4802dda947eac50558a7e0c62', "Anca's Phone number +5491126033185 "], - ['0x2e7e480e7d6a2577a72a869788dc2acd7b069e9d', "Nicolas's Phone number +5491150466926"], - ['0x3cb712ff0114ca421d2aefb9e4fdbe8e78be0936', "Vanessa's Phone number +5491157641239"], - ['0x1f2b673387a470c9b45397f7cd9f81eeff37af3a', "Jason's Phone number +5491136815320"], -]) - -export const builder = (yargs: Argv) => { - return yargs -} - -export const handler = async (argv: ListArgv) => { - const domain = getBlockscoutUrl(argv.celoEnv) - const listUsersUrl = `${domain}/api?module=account&action=tokentx&address=0x0000000000000000000000000000000000000abe` - - await switchToClusterFromEnv(argv.celoEnv, false, true) - - console.info(`Getting list of users for "${argv.celoEnv}" environment`) - const resp = await fetch(listUsersUrl) - const jsonResp = await resp.json() - await handleListOfUsers(domain, jsonResp, process.env.CELOTOOL_VERBOSE === 'true') -} - -async function handleListOfUsers(domain: string, json: any, verboseMode: boolean) { - if (verboseMode) { - console.info('verbose mode enabled') - } - const transactionUrlPrefix = `${domain}/api?module=account&action=tokentx&address=` - const users = new Set() - for (const object of json.result) { - users.add(object.from) - } - - console.info(`Num of users: ${users.size}`) - const usersArray = Array.from(users.values()) - const usersAndTransactions = new Array(usersArray.length) - - console.debug('Getting transactions for all the users, this will take time...') - - const transactions = await Promise.all( - usersArray.map((address: string) => - getNonverificationTransactions(transactionUrlPrefix + address) - ) - ) - for (let i = 0; i < usersArray.length; i++) { - const address = usersArray[i] - const numRealTransactions = transactions[i].length - const latestTransactionTimestamp = getLatestTransactionTimestamp(transactions[i]) - usersAndTransactions[i] = { address, numRealTransactions, latestTransactionTimestamp } - if (verboseMode) { - console.debug( - `Address ${i + 1}/${ - usersArray.length - }: ${address}, non-verification transactions: ${numRealTransactions}` - ) - const transactionUrl = transactionUrlPrefix + address - console.debug(`Transaction address: ${transactionUrl}`) - } - } - - console.debug('Sorting users in the decreasing order of transactions...') - // Sort in decreasing order of the number of transactions. For users with same number - // of transactions, put the user who made the latest transaction first. - usersAndTransactions - .sort((a: any, b: any) => { - if (a.numRealTransactions !== b.numRealTransactions) { - return a.numRealTransactions - b.numRealTransactions - } - return a.latestTransactionTimestamp - b.latestTransactionTimestamp - }) - .reverse() - - for (const entry of usersAndTransactions) { - const address = entry.address - const numRealTransactions = entry.numRealTransactions - const latestTransactionTimestampInSeconds = entry.latestTransactionTimestamp - const lastTransactionHumanReadableTime = - latestTransactionTimestampInSeconds > 0 - ? new Date(latestTransactionTimestampInSeconds * 1000).toLocaleString() - : 'Not applicable' - let infoString = - `Address: ${address}\t` + - `non-verification transactions: ${numRealTransactions}\t` + - `last transaction on: ${lastTransactionHumanReadableTime}` - - if (knownAccounts.has(address)) { - infoString = `${infoString} (${knownAccounts.get(address)}` - } - console.info(infoString) - } -} - -async function getNonverificationTransactions(transactionUrl: string) { - let jsonResp: any - - // Try thrice before giving up. - for (let i = 0; i < 3; i++) { - try { - const resp = await fetch(transactionUrl) - jsonResp = await resp.json() - } catch (e) { - await sleep(Math.random() * 5000) - } - } - - if (jsonResp === null) { - console.error(`Failed to get valid response for ${transactionUrl}`) - return [] - } - - if (jsonResp == null || jsonResp.result == null) { - return [] - } - return jsonResp.result.filter( - (transaction: any) => transaction.to !== '0x0000000000000000000000000000000000000abe' - ) -} - -const getLatestTransactionTimestamp = (transactions: any[]): number => { - let maxValue = -1 - for (const transaction of transactions) { - if (parseInt(transaction.timeStamp, 10) > maxValue) { - maxValue = transaction.timeStamp - } - } - return maxValue -} - -function sleep(milliseconds: number) { - return new Promise((resolve) => setTimeout(resolve, milliseconds)) -} diff --git a/packages/celotool/src/lib/oracle.ts b/packages/celotool/src/lib/oracle.ts index 9f90949921c..f2d84845771 100644 --- a/packages/celotool/src/lib/oracle.ts +++ b/packages/celotool/src/lib/oracle.ts @@ -231,6 +231,10 @@ export function addCurrencyPairMiddleware(argv: yargs.Argv) { 'EUROCEUR', 'EURXOF', 'EUROCXOF', + 'KESUSD', + 'COPUSD', + 'CELOKES', + 'USDTUSD', ], description: 'Oracle deployment to target based on currency pair', demandOption: true, diff --git a/packages/helm-charts/oracle/CELOKES.yaml b/packages/helm-charts/oracle/CELOKES.yaml new file mode 100644 index 00000000000..d10c50f4359 --- /dev/null +++ b/packages/helm-charts/oracle/CELOKES.yaml @@ -0,0 +1,95 @@ +oracle: + currencyPair: CELOKES + overrideOracleCount: 12 # At 5s block time, every client reports once per minute + aggregation: + mid: + maxExchangeVolumeShare: 1 + maxPercentageDeviation: 0.01 + maxPercentageBidAskSpread: 0.03 + metrics: + enabled: true + prometheusPort: 9090 + apiRequestTimeoutMs: 5000 + circuitBreakerPriceChangeThreshold: 0.25 + gasPriceMultiplier: 1.5 + priceSources: "[ + [ + {exchange: 'BINANCE', symbol: 'CELOUSDT', toInvert: false}, + {exchange: 'KRAKEN', symbol: 'USDTUSD', toInvert: false}, + {exchange: 'ALPHAVANTAGE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + ], + [ + {exchange: 'BINANCE', symbol: 'CELOUSDT', toInvert: false}, + {exchange: 'KRAKEN', symbol: 'USDTUSD', toInvert: false}, + {exchange: 'XIGNITE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + ], + + [ + {exchange: 'COINBASE', symbol: 'CELOUSD', toInvert: false}, + {exchange: 'ALPHAVANTAGE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + ], + [ + {exchange: 'COINBASE', symbol: 'CELOUSD', toInvert: false}, + {exchange: 'XIGNITE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + ], + + [ + {exchange: 'OKX', symbol: 'CELOUSDT', toInvert: false}, + {exchange: 'BITSTAMP', symbol: 'USDTUSD', toInvert: false}, + {exchange: 'ALPHAVANTAGE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + ], + [ + {exchange: 'OKX', symbol: 'CELOUSDT', toInvert: false}, + {exchange: 'BITSTAMP', symbol: 'USDTUSD', toInvert: false}, + {exchange: 'XIGNITE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + ], + + [ + {exchange: 'KUCOIN', symbol: 'CELOUSDT', toInvert: false}, + {exchange: 'COINBASE', symbol: 'USDTUSD', toInvert: false}, + {exchange: 'ALPHAVANTAGE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + ], + [ + {exchange: 'KUCOIN', symbol: 'CELOUSDT', toInvert: false}, + {exchange: 'COINBASE', symbol: 'USDTUSD', toInvert: false}, + {exchange: 'XIGNITE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + ], + [ + {exchange: 'BITMART', symbol: 'CELOUSDT', toInvert: false}, + {exchange: 'COINBASE', symbol: 'USDTUSD', toInvert: false}, + {exchange: 'ALPHAVANTAGE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + ], + [ + {exchange: 'BITMART', symbol: 'CELOUSDT', toInvert: false}, + {exchange: 'COINBASE', symbol: 'USDTUSD', toInvert: false}, + {exchange: 'XIGNITE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + ], + + + + ]" + # Additional sources missing adapters [ + # {exchange: 'GATEIO', symbol: 'CELOUSDT', toInvert: false}, + # {exchange: 'BITSTAMP', symbol: 'USDTUSD', toInvert: false}, + # {exchange: 'ALPHAVANTAGE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + # ], + # [ + # {exchange: 'GATEIO', symbol: 'CELOUSDT', toInvert: false}, + # {exchange: 'BITSTAMP', symbol: 'USDTUSD', toInvert: false}, + # {exchange: 'XIGNITE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + # ], + # {exchange: 'BYBIT', symbol: 'CELOUSDT', toInvert: false}, + # {exchange: 'BITSTAMP', symbol: 'USDTUSD', toInvert: false}, + # {exchange: 'ALPHAVANTAGE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + # ], + # [ + # {exchange: 'BYBIT', symbol: 'CELOUSDT', toInvert: false}, + # {exchange: 'BITSTAMP', symbol: 'USDTUSD', toInvert: false}, + # {exchange: 'XIGNITE', symbol: 'USDKES', toInvert: false, ignoreVolume: true} + # ], + + minPriceSourceCount: 7 + reportStrategy: BLOCK_BASED + reporter: + blockBased: + minReportPriceChangeThreshold: 0.005 diff --git a/packages/helm-charts/oracle/COPUSD.yaml b/packages/helm-charts/oracle/COPUSD.yaml new file mode 100644 index 00000000000..76cf08c8c86 --- /dev/null +++ b/packages/helm-charts/oracle/COPUSD.yaml @@ -0,0 +1,27 @@ +oracle: + currencyPair: COPUSD + overrideOracleCount: 12 # At 5s block time, every client reports once per minute + aggregation: + mid: + maxExchangeVolumeShare: 1 + maxPercentageDeviation: 0.01 + maxPercentageBidAskSpread: 0.03 + metrics: + enabled: true + prometheusPort: 9090 + apiRequestTimeoutMs: 5000 + circuitBreakerPriceChangeThreshold: 0.25 + gasPriceMultiplier: 1.5 + priceSources: "[ + [ + {exchange: 'ALPHAVANTAGE', symbol: 'USDCOP', toInvert: true} + ], + [ + {exchange: 'XIGNITE', symbol: 'COPUSD', toInvert: false} + ] + ]" + minPriceSourceCount: 2 + reportStrategy: BLOCK_BASED + reporter: + blockBased: + minReportPriceChangeThreshold: 0.0005 # 0.05% diff --git a/packages/helm-charts/oracle/KESUSD.yaml b/packages/helm-charts/oracle/KESUSD.yaml new file mode 100644 index 00000000000..731c76316be --- /dev/null +++ b/packages/helm-charts/oracle/KESUSD.yaml @@ -0,0 +1,31 @@ +oracle: + currencyPair: KESUSD + overrideOracleCount: 12 # At 5s block time, every client reports once per minute + aggregation: + mid: + maxExchangeVolumeShare: 1 + maxPercentageDeviation: 0.01 + maxPercentageBidAskSpread: 0.03 + metrics: + enabled: true + prometheusPort: 9090 + apiRequestTimeoutMs: 5000 + circuitBreakerPriceChangeThreshold: 0.25 + gasPriceMultiplier: 1.5 + priceSources: "[ + [ + {exchange: 'ALPHAVANTAGE', symbol: 'KESUSD', toInvert: false} + ], + [ + {exchange: 'XIGNITE', symbol: 'KESUSD', toInvert: false} + ] + ]" + # Additional sources missing adapters [ + # [ + # {exchange: 'OPENEXCHANGERATES', symbol: 'KESUSD', toInvert: false} + # ], + minPriceSourceCount: 2 + reportStrategy: BLOCK_BASED + reporter: + blockBased: + minReportPriceChangeThreshold: 0.0005 # 0.05% diff --git a/packages/helm-charts/oracle/USDTUSD.yaml b/packages/helm-charts/oracle/USDTUSD.yaml new file mode 100644 index 00000000000..1a979a67e7d --- /dev/null +++ b/packages/helm-charts/oracle/USDTUSD.yaml @@ -0,0 +1,44 @@ +oracle: + currencyPair: USDTUSD + aggregation: + mid: + maxExchangeVolumeShare: 1 + maxPercentageDeviation: 0.005 + maxPercentageBidAskSpread: 0.005 + metrics: + enabled: true + prometheusPort: 9090 + apiRequestTimeoutMs: 5000 + circuitBreakerPriceChangeThreshold: 0.25 + gasPriceMultiplier: 1.5 + priceSources: "[ + [ + {exchange: 'OKX', symbol: 'USDCUSDT', toInvert: true}, + {exchange: 'KRAKEN', symbol: 'USDCUSD', toInvert: false } + ], + [ + {exchange: 'KRAKEN', symbol: 'USDTUSD', toInvert: false} + ], + [ + {exchange: 'BITSTAMP', symbol: 'USDTUSD', toInvert: false} + ], + [ + {exchange: 'COINBASE', symbol: 'USDTUSD', toInvert: false} + ], + ]" + # Additional sources missing adapters + # [ + # {exchange: 'BYBIT', symbol: 'USDCUSDT', toInvert: true}, + # {exchange: 'BITSTAMP', symbol: 'USDCUSD', toInvert: false } + # ], + # + # https://api.bybit.com/v5/market/tickers?category=spot&symbol=USDCUSDT + # [ + # {exchange: 'CRYPTO', symbol: 'USDTUSD', toInvert: false} + # ], + # https://api.crypto.com/exchange/v1/public/get-tickers?instrument_name=USDT_USD + minPriceSourceCount: 3 # 4 with additional sources + reportStrategy: BLOCK_BASED + reporter: + blockBased: + minReportPriceChangeThreshold: 0.0005 # 0.05% diff --git a/packages/helm-charts/oracle/templates/statefulset.yaml b/packages/helm-charts/oracle/templates/statefulset.yaml index 423881c0e0b..dbc43037927 100644 --- a/packages/helm-charts/oracle/templates/statefulset.yaml +++ b/packages/helm-charts/oracle/templates/statefulset.yaml @@ -109,7 +109,7 @@ spec: ADDRESSES={{- range $index, $identity := .Values.oracle.identities -}}{{ $identity.address }},{{- end }} export ADDRESS=`echo -n $ADDRESSES | cut -d ',' -f $((RID + 1))` - exec yarn start + exec pnpm start env: - name: REPLICA_NAME valueFrom: diff --git a/packages/protocol/.solhint.json b/packages/protocol/.solhint.json index 60ed47634e3..b0e56524354 100644 --- a/packages/protocol/.solhint.json +++ b/packages/protocol/.solhint.json @@ -3,6 +3,6 @@ "rules": { "function-max-lines": "off", "max-line-length": "off", - "ordering": "error" + "ordering": "warn" } } diff --git a/packages/protocol/artifacts/Proxy/Readme.md b/packages/protocol/artifacts/Proxy/Readme.md new file mode 100644 index 00000000000..0a5f3d8c50f --- /dev/null +++ b/packages/protocol/artifacts/Proxy/Readme.md @@ -0,0 +1,12 @@ +# About this folder + +This folder has files for the init code and bytecode used with the Celo smart contracts. + +Leaving a note for future reference: + +1. The `proxyInitCode...` file seems to be require the bytecode for `Proxy.sol`. I'm not sure if this is the correct way to do it, but I simply copy/pasted the JSON value at `packages/protocol/out/Proxy.sol/Proxy.json` > `bytecode.object.` which is from the Foundry build artifacts. +1. The `proxyBytecode...` file seems to be require the deployed bytecode for `Proxy.sol`. I'm not sure if this is the correct way to do it, but I simply copy/pasted the JSON value at `packages/protocol/out/Proxy.sol/Proxy.json` > `deployedBytecode.object.` which is from the Foundry build artifacts. + +Unless the bytecodes in these manual artifacts matches the actual Foundry artifacts, the `test_verifyArtifacts()` test in [`ProxyFactory08.t.sol`](packages/protocol/test-sol/unit/common/ProxyFactory08.t.sol) will fail. + +I didn't have time to investigate this further or question why manual artifacts are needed in the first place. But, I'm leaving a note here for future reference. diff --git a/packages/protocol/artifacts/Proxy/proxyBytecode0.5.17+commit.d19bba13.hex b/packages/protocol/artifacts/Proxy/proxyBytecode0.5.17+commit.d19bba13.hex new file mode 100644 index 00000000000..c54669257a7 --- /dev/null +++ b/packages/protocol/artifacts/Proxy/proxyBytecode0.5.17+commit.d19bba13.hex @@ -0,0 +1 @@ +0x60806040526004361061004a5760003560e01c806303386ba31461015e57806342404e07146101e0578063bb913f4114610211578063d29d44ee14610244578063f7e6af8014610277575b604080517f656970313936372e70726f78792e696d706c656d656e746174696f6e000000008152905190819003601c0190206000190180546001600160a01b0381166100d5576040805162461bcd60e51b8152602060048201526015602482015274139bc8125b5c1b195b595b9d185d1a5bdb881cd95d605a1b604482015290519081900360640190fd5b6100de8161028c565b61012a576040805162461bcd60e51b8152602060048201526018602482015277496e76616c696420636f6e7472616374206164647265737360401b604482015290519081900360640190fd5b60405136810160405236600082376000803683855af43d604051818101604052816000823e82801561015a578282f35b8282fd5b6101de6004803603604081101561017457600080fd5b6001600160a01b03823516919081019060408101602082013564010000000081111561019f57600080fd5b8201836020820111156101b157600080fd5b803590602001918460018302840111640100000000831117156101d357600080fd5b5090925090506102c8565b005b3480156101ec57600080fd5b506101f56103f8565b604080516001600160a01b039092168252519081900360200190f35b34801561021d57600080fd5b506101de6004803603602081101561023457600080fd5b50356001600160a01b0316610432565b34801561025057600080fd5b506101de6004803603602081101561026757600080fd5b50356001600160a01b031661055c565b34801561028357600080fd5b506101f56105cc565b6000813f7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708181148015906102c057508115155b949350505050565b6102d06105cc565b6001600160a01b0316336001600160a01b03161461032c576040805162461bcd60e51b815260206004820152601460248201527339b2b73232b9103bb0b9903737ba1037bbb732b960611b604482015290519081900360640190fd5b61033583610432565b60006060846001600160a01b031684846040518083838082843760405192019450600093509091505080830381855af49150503d8060008114610394576040519150601f19603f3d011682016040523d82523d6000602084013e610399565b606091505b509092509050816103f1576040805162461bcd60e51b815260206004820152601e60248201527f696e697469616c697a6174696f6e2063616c6c6261636b206661696c65640000604482015290519081900360640190fd5b5050505050565b604080517f656970313936372e70726f78792e696d706c656d656e746174696f6e000000008152905190819003601c019020600019015490565b61043a6105cc565b6001600160a01b0316336001600160a01b031614610496576040805162461bcd60e51b815260206004820152601460248201527339b2b73232b9103bb0b9903737ba1037bbb732b960611b604482015290519081900360640190fd5b604080517f656970313936372e70726f78792e696d706c656d656e746174696f6e000000008152905190819003601c019020600019016104d58261028c565b610521576040805162461bcd60e51b8152602060048201526018602482015277496e76616c696420636f6e7472616374206164647265737360401b604482015290519081900360640190fd5b8181556040516001600160a01b038316907fab64f92ab780ecbf4f3866f57cee465ff36c89450dcce20237ca7a8d81fb7d1390600090a25050565b6105646105cc565b6001600160a01b0316336001600160a01b0316146105c0576040805162461bcd60e51b815260206004820152601460248201527339b2b73232b9103bb0b9903737ba1037bbb732b960611b604482015290519081900360640190fd5b6105c9816105fc565b50565b604080517232b4b8189c9b1b97383937bc3c9730b236b4b760691b81529051908190036013019020600019015490565b6001600160a01b03811661064b576040805162461bcd60e51b815260206004820152601160248201527006f776e65722063616e6e6f74206265203607c1b604482015290519081900360640190fd5b604080517232b4b8189c9b1b97383937bc3c9730b236b4b760691b8152905190819003601301812060001901828155906001600160a01b038316907f50146d0e3c60aa1d17a70635b05494f864e86144a2201275021014fbf08bafe290600090a2505056fea265627a7a723158205647774ea8a1df5ec9899eb5995d1c2cd5e816f65dacd3198f8b85f9912af1c464736f6c63430005110032 \ No newline at end of file diff --git a/packages/protocol/artifacts/Proxy/proxyInitCode0.5.17+commit.d19bba13.hex b/packages/protocol/artifacts/Proxy/proxyInitCode0.5.17+commit.d19bba13.hex new file mode 100644 index 00000000000..8de7f661bed --- /dev/null +++ b/packages/protocol/artifacts/Proxy/proxyInitCode0.5.17+commit.d19bba13.hex @@ -0,0 +1 @@ +0x608060405234801561001057600080fd5b50610023336001600160e01b0361002816565b6100e6565b6001600160a01b038116610077576040805162461bcd60e51b815260206004820152601160248201527006f776e65722063616e6e6f74206265203607c1b604482015290519081900360640190fd5b604080517f656970313936372e70726f78792e61646d696e000000000000000000000000008152905190819003601301812060001901828155906001600160a01b038316907f50146d0e3c60aa1d17a70635b05494f864e86144a2201275021014fbf08bafe290600090a25050565b6106e5806100f56000396000f3fe60806040526004361061004a5760003560e01c806303386ba31461015e57806342404e07146101e0578063bb913f4114610211578063d29d44ee14610244578063f7e6af8014610277575b604080517f656970313936372e70726f78792e696d706c656d656e746174696f6e000000008152905190819003601c0190206000190180546001600160a01b0381166100d5576040805162461bcd60e51b8152602060048201526015602482015274139bc8125b5c1b195b595b9d185d1a5bdb881cd95d605a1b604482015290519081900360640190fd5b6100de8161028c565b61012a576040805162461bcd60e51b8152602060048201526018602482015277496e76616c696420636f6e7472616374206164647265737360401b604482015290519081900360640190fd5b60405136810160405236600082376000803683855af43d604051818101604052816000823e82801561015a578282f35b8282fd5b6101de6004803603604081101561017457600080fd5b6001600160a01b03823516919081019060408101602082013564010000000081111561019f57600080fd5b8201836020820111156101b157600080fd5b803590602001918460018302840111640100000000831117156101d357600080fd5b5090925090506102c8565b005b3480156101ec57600080fd5b506101f56103f8565b604080516001600160a01b039092168252519081900360200190f35b34801561021d57600080fd5b506101de6004803603602081101561023457600080fd5b50356001600160a01b0316610432565b34801561025057600080fd5b506101de6004803603602081101561026757600080fd5b50356001600160a01b031661055c565b34801561028357600080fd5b506101f56105cc565b6000813f7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708181148015906102c057508115155b949350505050565b6102d06105cc565b6001600160a01b0316336001600160a01b03161461032c576040805162461bcd60e51b815260206004820152601460248201527339b2b73232b9103bb0b9903737ba1037bbb732b960611b604482015290519081900360640190fd5b61033583610432565b60006060846001600160a01b031684846040518083838082843760405192019450600093509091505080830381855af49150503d8060008114610394576040519150601f19603f3d011682016040523d82523d6000602084013e610399565b606091505b509092509050816103f1576040805162461bcd60e51b815260206004820152601e60248201527f696e697469616c697a6174696f6e2063616c6c6261636b206661696c65640000604482015290519081900360640190fd5b5050505050565b604080517f656970313936372e70726f78792e696d706c656d656e746174696f6e000000008152905190819003601c019020600019015490565b61043a6105cc565b6001600160a01b0316336001600160a01b031614610496576040805162461bcd60e51b815260206004820152601460248201527339b2b73232b9103bb0b9903737ba1037bbb732b960611b604482015290519081900360640190fd5b604080517f656970313936372e70726f78792e696d706c656d656e746174696f6e000000008152905190819003601c019020600019016104d58261028c565b610521576040805162461bcd60e51b8152602060048201526018602482015277496e76616c696420636f6e7472616374206164647265737360401b604482015290519081900360640190fd5b8181556040516001600160a01b038316907fab64f92ab780ecbf4f3866f57cee465ff36c89450dcce20237ca7a8d81fb7d1390600090a25050565b6105646105cc565b6001600160a01b0316336001600160a01b0316146105c0576040805162461bcd60e51b815260206004820152601460248201527339b2b73232b9103bb0b9903737ba1037bbb732b960611b604482015290519081900360640190fd5b6105c9816105fc565b50565b604080517232b4b8189c9b1b97383937bc3c9730b236b4b760691b81529051908190036013019020600019015490565b6001600160a01b03811661064b576040805162461bcd60e51b815260206004820152601160248201527006f776e65722063616e6e6f74206265203607c1b604482015290519081900360640190fd5b604080517232b4b8189c9b1b97383937bc3c9730b236b4b760691b8152905190819003601301812060001901828155906001600160a01b038316907f50146d0e3c60aa1d17a70635b05494f864e86144a2201275021014fbf08bafe290600090a2505056fea265627a7a723158205647774ea8a1df5ec9899eb5995d1c2cd5e816f65dacd3198f8b85f9912af1c464736f6c63430005110032 \ No newline at end of file diff --git a/packages/protocol/contractPackages.ts b/packages/protocol/contractPackages.ts index ac7e96deb20..b75d67fa521 100644 --- a/packages/protocol/contractPackages.ts +++ b/packages/protocol/contractPackages.ts @@ -48,12 +48,24 @@ export const SOLIDITY_08_PACKAGE = { proxiesPath: '/', // Proxies are still with 0.5 contracts // Proxies shouldn't have to be added to a list manually // https://github.com/celo-org/celo-monorepo/issues/10555 - contracts: ['GasPriceMinimum', 'FeeCurrencyDirectory', 'MintGoldSchedule'], + contracts: [ + 'GasPriceMinimum', + 'FeeCurrencyDirectory', + 'CeloUnreleasedTreasury', + 'Validators', + 'EpochManager', + 'EpochManagerEnabler', + 'ScoreManager', + ], proxyContracts: [ 'GasPriceMinimumProxy', 'FeeCurrencyDirectoryProxy', 'MentoFeeCurrencyAdapterV1', - 'MintGoldScheduleProxy', + 'CeloUnreleasedTreasuryProxy', + 'ValidatorsProxy', + 'EpochManagerProxy', + 'EpochManagerEnablerProxy', + 'ScoreManagerProxy', ], truffleConfig: 'truffle-config0.8.js', } satisfies ContractPackage diff --git a/packages/protocol/contracts-0.8/common/CeloUnreleasedTreasury.sol b/packages/protocol/contracts-0.8/common/CeloUnreleasedTreasury.sol new file mode 100644 index 00000000000..b5ab2252a7e --- /dev/null +++ b/packages/protocol/contracts-0.8/common/CeloUnreleasedTreasury.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "@openzeppelin/contracts8/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts8/utils/math/Math.sol"; + +import "./UsingRegistry.sol"; + +import "../../contracts/common/Initializable.sol"; +import "./interfaces/ICeloUnreleasedTreasuryInitializer.sol"; + +/** + * @title Contract for unreleased Celo tokens. + * @notice This contract is not allowed to receive transfers of CELO, + * to avoid miscalculating the epoch rewards and to prevent any malicious actor + * from routing stolen fund through the epoch reward distribution. + */ +contract CeloUnreleasedTreasury is + ICeloUnreleasedTreasuryInitializer, + UsingRegistry, + ReentrancyGuard, + Initializable +{ + bool internal hasAlreadyReleased; + + // Remaining epoch rewards to distribute. + uint256 internal remainingBalanceToRelease; + + event Released(address indexed to, uint256 amount); + + /** + * @notice Only allows EpochManager to call. + */ + modifier onlyEpochManager() { + require( + msg.sender == registry.getAddressForOrDie(EPOCH_MANAGER_REGISTRY_ID), + "Only the EpochManager contract can call this function." + ); + _; + } + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) Initializable(test) {} + + /** + * @notice A constructor for initialising a new instance of a CeloUnreleasedTreasury contract. + * @param registryAddress The address of the registry core smart contract. + */ + function initialize(address registryAddress) external initializer { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + } + + /** + * @notice Releases the Celo to the specified address. + * @param to The address to release the amount to. + * @param amount The amount to release. + */ + function release(address to, uint256 amount) external onlyEpochManager { + if (!hasAlreadyReleased) { + remainingBalanceToRelease = address(this).balance; + hasAlreadyReleased = true; + } + + require(remainingBalanceToRelease >= amount, "Insufficient balance."); + remainingBalanceToRelease -= amount; + require(getCeloToken().transfer(to, amount), "CELO transfer failed."); + + emit Released(to, amount); + } + + /** + * @notice Returns the remaining balance this contract has left to release. + * @dev This uses internal accounting of the released balance, + * to avoid recounting CELO that was transferred back to this contract. + */ + function getRemainingBalanceToRelease() external view returns (uint256) { + if (!hasAlreadyReleased) { + return address(this).balance; + } else { + return remainingBalanceToRelease; + } + } + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } +} diff --git a/packages/protocol/contracts-0.8/common/EpochManager.sol b/packages/protocol/contracts-0.8/common/EpochManager.sol new file mode 100644 index 00000000000..5f847e56def --- /dev/null +++ b/packages/protocol/contracts-0.8/common/EpochManager.sol @@ -0,0 +1,808 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts8/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts8/access/Ownable.sol"; + +import "./interfaces/IOracle.sol"; +import "../common/UsingRegistry.sol"; + +import "../../contracts/common/FixidityLib.sol"; +import "../../contracts/common/Initializable.sol"; +import "../../contracts/common/interfaces/IEpochManager.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "./interfaces/IEpochManagerInitializer.sol"; + +/** + * @title Contract used for managing CELO L2 epoch and elections. + * @dev DESIGN_DESICION: we assume that the first epoch on the L2 starts as soon as the system is initialized + * to minimize amount of "limbo blocks" the network should stop relatively close to an epoch number (but with enough time) + * to have time to call the function `EpochInitializer.migrateEpochAndValidators()` + */ +contract EpochManager is + Initializable, + UsingRegistry, + IEpochManager, + ReentrancyGuard, + ICeloVersionedContract, + IEpochManagerInitializer +{ + using FixidityLib for FixidityLib.Fraction; + + struct Epoch { + uint256 firstBlock; + uint256 lastBlock; + uint256 startTimestamp; + uint256 rewardsBlock; + } + + enum EpochProcessStatus { + NotStarted, + Started, + IndivudualGroupsProcessing + } + + struct EpochProcessState { + EpochProcessStatus status; + uint256 perValidatorReward; // The per validator epoch reward. + uint256 totalRewardsVoter; // The total rewards to voters. + uint256 totalRewardsCommunity; // The total community reward. + uint256 totalRewardsCarbonFund; // The total carbon offsetting partner reward. + } + + bool public isSystemInitialized; + + // the length of an epoch in seconds + uint256 public epochDuration; + + uint256 public firstKnownEpoch; + uint256 internal currentEpochNumber; + address public oracleAddress; + address[] public electedAccounts; + mapping(address => uint256) public processedGroups; + + EpochProcessState public epochProcessing; + mapping(uint256 => Epoch) internal epochs; + mapping(address => uint256) public validatorPendingPayments; + // Electeds in the L1 assumed signers can not change during the epoch + // so we keep a copy + address[] public electedSigners; + + uint256 public toProcessGroups = 0; + + /** + * @notice Event emited when epochProcessing has begun. + * @param epochNumber The epoch number that is being processed. + */ + event EpochProcessingStarted(uint256 indexed epochNumber); + + /** + * @notice Event emited when epochProcessing has ended. + * @param epochNumber The epoch number that is finished being processed. + */ + event EpochProcessingEnded(uint256 indexed epochNumber); + + /** + * @notice Event emited when a new epoch duration is set. + * @param newEpochDuration The new epoch duration. + */ + event EpochDurationSet(uint256 indexed newEpochDuration); + + /** + * @notice Event emited when a new oracle address is set. + * @param newOracleAddress The new oracle address. + */ + event OracleAddressSet(address indexed newOracleAddress); + + /** + * @notice Emitted when an epoch payment is sent. + * @param validator Address of the validator. + * @param validatorPayment Amount of cUSD sent to the validator. + * @param group Address of the validator's group. + * @param groupPayment Amount of cUSD sent to the group. + */ + event ValidatorEpochPaymentDistributed( + address indexed validator, + uint256 validatorPayment, + address indexed group, + uint256 groupPayment, + address indexed beneficiary, + uint256 delegatedPayment + ); + + /** + * @notice Emitted when group is marked for processing. + * @param group Address of the group to be processed. + * @param epochNumber The epoch number for which the group gets marked for processing. + */ + event GroupMarkedForProcessing(address indexed group, uint256 indexed epochNumber); + + /** + * @notice Emitted when group is processed. + * @param group Address of the processed group. + * @param epochNumber The epoch number for which the group gets processed. + */ + event GroupProcessed(address indexed group, uint256 indexed epochNumber); + + /** + * @notice Throws if called by other than EpochManagerEnabler contract. + */ + modifier onlyEpochManagerEnabler() { + require( + msg.sender == registry.getAddressForOrDie(EPOCH_MANAGER_ENABLER_REGISTRY_ID), + "msg.sender is not Enabler" + ); + _; + } + + /** + * @notice Throws if called when EpochManager system has not yet been initalized. + */ + modifier onlySystemAlreadyInitialized() { + require(systemAlreadyInitialized(), "Epoch system not initialized"); + _; + } + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + * @param registryAddress The address of the registry core smart contract. + * @param newEpochDuration The duration of an epoch in seconds. + */ + function initialize(address registryAddress, uint256 newEpochDuration) external initializer { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + setEpochDuration(newEpochDuration); + setOracleAddress(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)); + } + + /** + * @notice Initializes the EpochManager system, allowing it to start processing epoch + * and distributing the epoch rewards. + * @dev Can only be called by the EpochManagerEnabler contract. + */ + function initializeSystem( + uint256 firstEpochNumber, + uint256 firstEpochBlock, + address[] memory firstElected + ) external onlyEpochManagerEnabler { + require( + getCeloToken().balanceOf(registry.getAddressForOrDie(CELO_UNRELEASED_TREASURY_REGISTRY_ID)) > + 0, + "CeloUnreleasedTreasury not yet funded." + ); + require(!systemAlreadyInitialized(), "Epoch system already initialized"); + require(firstEpochNumber > 0, "First epoch number must be greater than 0"); + require(firstEpochBlock > 0, "First epoch block must be greater than 0"); + require( + firstEpochBlock <= block.number, + "First epoch block must be less or equal than current block" + ); + require(firstElected.length > 0, "First elected validators must be greater than 0"); + isSystemInitialized = true; + firstKnownEpoch = firstEpochNumber; + currentEpochNumber = firstEpochNumber; + + Epoch storage _currentEpoch = epochs[currentEpochNumber]; + _currentEpoch.firstBlock = firstEpochBlock; + _currentEpoch.startTimestamp = block.timestamp; + + electedAccounts = firstElected; + + _setElectedSigners(firstElected); + } + + /** + * @notice Starts processing an epoch and allocates funds to the beneficiaries. + * @dev Epoch rewards are frozen at the time of execution. + * @dev Can only be called once the system is initialized. + */ + function startNextEpochProcess() external nonReentrant onlySystemAlreadyInitialized { + require(isTimeForNextEpoch(), "Epoch is not ready to start"); + require(!isOnEpochProcess(), "Epoch process is already started"); + + epochProcessing.status = EpochProcessStatus.Started; + epochs[currentEpochNumber].rewardsBlock = block.number; + + // calculate rewards + getEpochRewards().updateTargetVotingYield(); + + ( + uint256 perValidatorReward, + uint256 totalRewardsVoter, + uint256 totalRewardsCommunity, + uint256 totalRewardsCarbonFund + ) = getEpochRewards().calculateTargetEpochRewards(); + + epochProcessing.perValidatorReward = perValidatorReward; + epochProcessing.totalRewardsVoter = totalRewardsVoter; + epochProcessing.totalRewardsCommunity = totalRewardsCommunity; + epochProcessing.totalRewardsCarbonFund = totalRewardsCarbonFund; + + allocateValidatorsRewards(); + + emit EpochProcessingStarted(currentEpochNumber); + } + + /** + * @notice Starts individual processing of the elected groups. + * As second step it is necessary to call processGroup + */ + function setToProcessGroups() external { + require(isOnEpochProcess(), "Epoch process is not started"); + + EpochProcessState storage _epochProcessing = epochProcessing; + _epochProcessing.status = EpochProcessStatus.IndivudualGroupsProcessing; + + IValidators validators = getValidators(); + IElection election = getElection(); + IScoreReader scoreReader = getScoreReader(); + require( + electedAccounts.length == electedSigners.length, + "Elected accounts and signers of different lengths." + ); + for (uint i = 0; i < electedAccounts.length; i++) { + address group = validators.getValidatorsGroup(electedAccounts[i]); + if (processedGroups[group] == 0) { + toProcessGroups++; + uint256 groupScore = scoreReader.getGroupScore(group); + // We need to precompute epoch rewards for each group since computation depends on total active votes for all groups. + uint256 epochRewards = election.getGroupEpochRewardsBasedOnScore( + group, + _epochProcessing.totalRewardsVoter, + groupScore + ); + processedGroups[group] = epochRewards == 0 ? type(uint256).max : epochRewards; + emit GroupMarkedForProcessing(group, currentEpochNumber); + } + } + } + + /** + * @notice Processes the rewards for a list of groups. For last group it will also finalize the epoch. + * @param groups List of validator groups to be processed. + * @param lessers List of validator groups that hold less votes that indexed group. + * @param greaters List of validator groups that hold more votes that indexed group. + */ + function processGroups( + address[] calldata groups, + address[] calldata lessers, + address[] calldata greaters + ) external { + for (uint i = 0; i < groups.length; i++) { + processGroup(groups[i], lessers[i], greaters[i]); + } + } + + /** + * @notice Processes the rewards for a group. For last group it will also finalize the epoch. + * @param group The group to process. + * @param lesser The group with less votes than the indexed group. + * @param greater The group with more votes than the indexed group. + */ + function processGroup(address group, address lesser, address greater) public { + EpochProcessState storage _epochProcessing = epochProcessing; + require( + _epochProcessing.status == EpochProcessStatus.IndivudualGroupsProcessing, + "Indivudual epoch process is not started" + ); + require(toProcessGroups > 0, "no more groups to process"); + + uint256 epochRewards = processedGroups[group]; + // checks that group is actually from elected group + require(epochRewards > 0, "group not from current elected set"); + IElection election = getElection(); + + if (epochRewards != type(uint256).max) { + election.distributeEpochRewards(group, epochRewards, lesser, greater); + } + + delete processedGroups[group]; + toProcessGroups--; + + emit GroupProcessed(group, currentEpochNumber); + + if (toProcessGroups == 0) { + _finishEpochHelper(_epochProcessing, election); + } + } + + /** + * @notice Finishes processing an epoch and releasing funds to the beneficiaries. + * @param groups List of validator groups to be processed. + * @param lessers List of validator groups that hold less votes that indexed group. + * @param greaters List of validator groups that hold more votes that indexed group. + */ + function finishNextEpochProcess( + address[] calldata groups, + address[] calldata lessers, + address[] calldata greaters + ) external virtual nonReentrant { + require(isOnEpochProcess(), "Epoch process is not started"); + require(toProcessGroups == 0, "Can't finish epoch while individual groups are being processed"); + + EpochProcessState storage _epochProcessing = epochProcessing; + + uint256 _toProcessGroups = 0; + IValidators validators = getValidators(); + IElection election = getElection(); + IScoreReader scoreReader = getScoreReader(); + require( + electedAccounts.length == electedSigners.length, + "Elected accounts and signers of different lengths." + ); + for (uint i = 0; i < electedAccounts.length; i++) { + address group = validators.getValidatorsGroup(electedAccounts[i]); + if (processedGroups[group] == 0) { + _toProcessGroups++; + uint256 groupScore = scoreReader.getGroupScore(group); + // We need to precompute epoch rewards for each group since computation depends on total active votes for all groups. + uint256 epochRewards = election.getGroupEpochRewardsBasedOnScore( + group, + _epochProcessing.totalRewardsVoter, + groupScore + ); + processedGroups[group] = epochRewards == 0 ? type(uint256).max : epochRewards; + } + delete electedAccounts[i]; + delete electedSigners[i]; + } + + require(_toProcessGroups == groups.length, "number of groups does not match"); + + for (uint i = 0; i < groups.length; i++) { + uint256 epochRewards = processedGroups[groups[i]]; + // checks that group is actually from elected group + require(epochRewards > 0, "group not from current elected set"); + if (epochRewards != type(uint256).max) { + election.distributeEpochRewards(groups[i], epochRewards, lessers[i], greaters[i]); + } + + delete processedGroups[groups[i]]; + emit GroupProcessed(groups[i], currentEpochNumber); + } + + _finishEpochHelper(_epochProcessing, election); + } + + /** + * @notice Sends the allocated epoch payment to a validator, their group, and + * delegation beneficiary. + * @param validator Account of the validator. + * @dev Can only be called once the system is initialized. + */ + function sendValidatorPayment(address validator) external onlySystemAlreadyInitialized { + FixidityLib.Fraction memory totalPayment = FixidityLib.newFixed( + validatorPendingPayments[validator] + ); + validatorPendingPayments[validator] = 0; + + IValidators validators = getValidators(); + address group = validators.getValidatorsGroup(validator); + (, uint256 commissionUnwrapped, , , , , ) = validators.getValidatorGroup(group); + + uint256 groupPayment = totalPayment.multiply(FixidityLib.wrap(commissionUnwrapped)).fromFixed(); + FixidityLib.Fraction memory remainingPayment = FixidityLib.newFixed( + totalPayment.fromFixed() - groupPayment + ); + (address beneficiary, uint256 delegatedFraction) = getAccounts().getPaymentDelegation( + validator + ); + uint256 delegatedPayment = remainingPayment + .multiply(FixidityLib.wrap(delegatedFraction)) + .fromFixed(); + uint256 validatorPayment = remainingPayment.fromFixed() - delegatedPayment; + + IERC20 stableToken = IERC20(getStableToken()); + + if (validatorPayment > 0) { + require(stableToken.transfer(validator, validatorPayment), "transfer failed to validator"); + } + + if (groupPayment > 0) { + require(stableToken.transfer(group, groupPayment), "transfer failed to validator group"); + } + + if (delegatedPayment > 0) { + require(stableToken.transfer(beneficiary, delegatedPayment), "transfer failed to delegatee"); + } + + emit ValidatorEpochPaymentDistributed( + validator, + validatorPayment, + group, + groupPayment, + beneficiary, + delegatedPayment + ); + } + + /** + * @notice Returns the epoch info for the current epoch. + * @return firstEpoch The first block of the current epoch. + * @return lastBlock The last block of the current epoch. + * @return startTimestamp The starting timestamp of the current epoch. + * @return rewardsBlock The reward block of the current epoch. + */ + function getCurrentEpoch() + external + view + onlySystemAlreadyInitialized + returns (uint256, uint256, uint256, uint256) + { + return getEpochByNumber(currentEpochNumber); + } + + /** + * @return The current epoch number. + * @dev Can only be called once the system is initialized. + */ + function getCurrentEpochNumber() external view onlySystemAlreadyInitialized returns (uint256) { + return currentEpochNumber; + } + + /** + * @return The latest epoch processing state. + */ + function getEpochProcessingState() + external + view + returns (uint256, uint256, uint256, uint256, uint256) + { + EpochProcessState storage _epochProcessing = epochProcessing; + return ( + uint256(_epochProcessing.status), + _epochProcessing.perValidatorReward, + _epochProcessing.totalRewardsVoter, + _epochProcessing.totalRewardsCommunity, + _epochProcessing.totalRewardsCarbonFund + ); + } + + /** + * @notice Used to block select functions in blockable contracts. + * @return Whether or not the blockable functions are blocked. + */ + function isBlocked() external view returns (bool) { + return isOnEpochProcess(); + } + + /** + * @return The number of elected accounts in the current set. + */ + function numberOfElectedInCurrentSet() + external + view + onlySystemAlreadyInitialized + returns (uint256) + { + return electedAccounts.length; + } + + /** + * @return The list of currently elected validators. + */ + function getElectedAccounts() + external + view + onlySystemAlreadyInitialized + returns (address[] memory) + { + return electedAccounts; + } + + /** + * @notice Returns the currently elected account at a specified index. + * @param index The index of the currently elected account. + */ + function getElectedAccountByIndex( + uint256 index + ) external view onlySystemAlreadyInitialized returns (address) { + return electedAccounts[index]; + } + + /** + * @return The list of the validator signers of elected validators. + */ + function getElectedSigners() + external + view + onlySystemAlreadyInitialized + returns (address[] memory) + { + return electedSigners; + } + + /** + * @notice Returns the currently elected signer address at a specified index. + * @param index The index of the currently elected signer. + */ + function getElectedSignerByIndex( + uint256 index + ) external view onlySystemAlreadyInitialized returns (address) { + return electedSigners[index]; + } + + /** + * @param epoch The epoch number of interest. + * @return The First block of the specified epoch. + */ + function getFirstBlockAtEpoch(uint256 epoch) external view returns (uint256) { + require(epoch >= firstKnownEpoch, "Epoch not known"); + require(epoch <= currentEpochNumber, "Epoch not created yet"); + return epochs[epoch].firstBlock; + } + + /** + * @param epoch The epoch number of interest. + * @return The last block of the specified epoch. + */ + function getLastBlockAtEpoch(uint256 epoch) external view returns (uint256) { + require(epoch >= firstKnownEpoch, "Epoch not known"); + require(epoch < currentEpochNumber, "Epoch not finished yet"); + return epochs[epoch].lastBlock; + } + + /** + * @notice Returns the epoch number of a specified blockNumber. + * @param _blockNumber Block number of the epoch info is retreived. + */ + function getEpochNumberOfBlock( + uint256 _blockNumber + ) external view onlySystemAlreadyInitialized returns (uint256) { + (uint256 _epochNumber, , , , ) = _getEpochByBlockNumber(_blockNumber); + return _epochNumber; + } + + /** + * @notice Returns the epoch info of a specified blockNumber. + * @param _blockNumber Block number of the epoch info is retreived. + * @return firstEpoch The first block of the given block number. + * @return lastBlock The first block of the given block number. + * @return startTimestamp The starting timestamp of the given block number. + * @return rewardsBlock The reward block of the given block number. + */ + function getEpochByBlockNumber( + uint256 _blockNumber + ) external view onlySystemAlreadyInitialized returns (uint256, uint256, uint256, uint256) { + ( + , + uint256 _firstBlock, + uint256 _lastBlock, + uint256 _startTimestamp, + uint256 _rewardsBlock + ) = _getEpochByBlockNumber(_blockNumber); + return (_firstBlock, _lastBlock, _startTimestamp, _rewardsBlock); + } + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } + + /** + * @notice Sets the time duration of an epoch. + * @param newEpochDuration The duration of an epoch in seconds. + * @dev Can only be set by owner. + */ + function setEpochDuration(uint256 newEpochDuration) public onlyOwner { + require(newEpochDuration > 0, "New epoch duration must be greater than zero."); + require(!isOnEpochProcess(), "Cannot change epoch duration during processing."); + epochDuration = newEpochDuration; + emit EpochDurationSet(newEpochDuration); + } + + /** + * @notice Sets the address of the Oracle used by this contract. + * @param newOracleAddress The address of the new oracle. + * @dev Can only be set by owner. + */ + function setOracleAddress(address newOracleAddress) public onlyOwner { + require(newOracleAddress != address(0), "Cannot set address zero as the Oracle."); + require(newOracleAddress != oracleAddress, "Oracle address cannot be the same."); + require(!isOnEpochProcess(), "Cannot change oracle address during epoch processing."); + oracleAddress = newOracleAddress; + emit OracleAddressSet(newOracleAddress); + } + + /** + * @return Whether or not the next epoch can be processed. + */ + function isTimeForNextEpoch() public view returns (bool) { + return block.timestamp >= epochs[currentEpochNumber].startTimestamp + epochDuration; + } + + /** + * @return Whether or not the current epoch is being processed. + */ + function isOnEpochProcess() public view returns (bool) { + return epochProcessing.status == EpochProcessStatus.Started; + } + + /** + * @return Whether or not the EpochManager contract has been activated to start processing epochs. + */ + function systemAlreadyInitialized() public view returns (bool) { + return initialized && isSystemInitialized; + } + + /** + * @notice Returns the epoch info of a specified epoch. + * @param epochNumber Epoch number where the epoch info is retreived. + * @return firstEpoch The first block of the given epoch. + * @return lastBlock The first block of the given epoch. + * @return startTimestamp The starting timestamp of the given epoch. + * @return rewardsBlock The reward block of the given epoch. + */ + function getEpochByNumber( + uint256 epochNumber + ) public view onlySystemAlreadyInitialized returns (uint256, uint256, uint256, uint256) { + Epoch memory _epoch = epochs[epochNumber]; + return (_epoch.firstBlock, _epoch.lastBlock, _epoch.startTimestamp, _epoch.rewardsBlock); + } + + /** + * @notice Allocates rewards to elected validator accounts. + */ + function allocateValidatorsRewards() internal { + uint256 totalRewards = 0; + IScoreReader scoreReader = getScoreReader(); + IValidators validators = getValidators(); + + EpochProcessState storage _epochProcessing = epochProcessing; + + for (uint i = 0; i < electedAccounts.length; i++) { + uint256 validatorScore = scoreReader.getValidatorScore(electedAccounts[i]); + uint256 validatorReward = validators.computeEpochReward( + electedAccounts[i], + validatorScore, + _epochProcessing.perValidatorReward + ); + validatorPendingPayments[electedAccounts[i]] += validatorReward; + totalRewards += validatorReward; + } + if (totalRewards == 0) { + return; + } + + // Mint all cUSD required for payment and the corresponding CELO + validators.mintStableToEpochManager(totalRewards); + + (uint256 numerator, uint256 denominator) = IOracle(oracleAddress).getExchangeRate( + address(getStableToken()) + ); + + uint256 CELOequivalent = (numerator * totalRewards) / denominator; + getCeloUnreleasedTreasury().release( + registry.getAddressForOrDie(RESERVE_REGISTRY_ID), + CELOequivalent + ); + } + + /** + * @notice Updates the list of elected validator signers. + */ + function _setElectedSigners(address[] memory _elected) internal { + require(electedAccounts.length > 0, "Elected list length cannot be zero."); + IAccounts accounts = getAccounts(); + electedSigners = new address[](_elected.length); + for (uint i = 0; i < _elected.length; i++) { + electedSigners[i] = accounts.getValidatorSigner(_elected[i]); + } + } + + /** + * @notice Finishes processing an epoch and releasing funds to the beneficiaries. + * @param _epochProcessing The current epoch processing state. + * @param election The Election contract. + */ + function _finishEpochHelper( + EpochProcessState storage _epochProcessing, + IElection election + ) internal { + // finalize epoch + // last block should be the block before and timestamp from previous block + epochs[currentEpochNumber].lastBlock = block.number - 1; + currentEpochNumber++; + // start new epoch + epochs[currentEpochNumber].firstBlock = block.number; + epochs[currentEpochNumber].startTimestamp = block.timestamp; + + // run elections + address[] memory _newlyElected = election.electValidatorAccounts(); + electedAccounts = _newlyElected; + _setElectedSigners(_newlyElected); + + ICeloUnreleasedTreasury celoUnreleasedTreasury = getCeloUnreleasedTreasury(); + celoUnreleasedTreasury.release( + registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID), + _epochProcessing.totalRewardsCommunity + ); + celoUnreleasedTreasury.release( + getEpochRewards().carbonOffsettingPartner(), + _epochProcessing.totalRewardsCarbonFund + ); + + _epochProcessing.status = EpochProcessStatus.NotStarted; + _epochProcessing.perValidatorReward = 0; + _epochProcessing.totalRewardsVoter = 0; + _epochProcessing.totalRewardsCommunity = 0; + _epochProcessing.totalRewardsCarbonFund = 0; + + emit EpochProcessingEnded(currentEpochNumber - 1); + } + + /** + * @notice Returns the epoch info of a specified blockNumber. + * @dev This function is here for backward compatibility. It is rather gas heavy and can run out of gas. + * @param _blockNumber Block number of the epoch info is retreived. + * @return firstEpoch The first block of the given block number. + * @return lastBlock The first block of the given block number. + * @return startTimestamp The starting timestamp of the given block number. + * @return rewardsBlock The reward block of the given block number. + */ + function _getEpochByBlockNumber( + uint256 _blockNumber + ) + internal + view + onlySystemAlreadyInitialized + returns (uint256, uint256, uint256, uint256, uint256) + { + require(_blockNumber <= block.number, "Invalid blockNumber. Value too high."); + + (uint256 _firstBlockOfFirstEpoch, , , ) = getEpochByNumber(firstKnownEpoch); + + require(_blockNumber >= _firstBlockOfFirstEpoch, "Invalid blockNumber. Value too low."); + + uint256 _firstBlockOfCurrentEpoch = epochs[currentEpochNumber].firstBlock; + + if (_blockNumber >= _firstBlockOfCurrentEpoch) { + ( + uint256 _firstBlock, + uint256 _lastBlock, + uint256 _startTimestamp, + uint256 _rewardsBlock + ) = getEpochByNumber(currentEpochNumber); + return (currentEpochNumber, _firstBlock, _lastBlock, _startTimestamp, _rewardsBlock); + } + + uint256 left = firstKnownEpoch; + uint256 right = currentEpochNumber - 1; + + while (left <= right) { + uint256 mid = (left + right) / 2; + uint256 _epochFirstBlock = epochs[mid].firstBlock; + uint256 _epochLastBlock = epochs[mid].lastBlock; + + if (_blockNumber >= _epochFirstBlock && _blockNumber <= _epochLastBlock) { + Epoch memory _epoch = epochs[mid]; + return ( + mid, + _epoch.firstBlock, + _epoch.lastBlock, + _epoch.startTimestamp, + _epoch.rewardsBlock + ); + } else if (_blockNumber < _epochFirstBlock) { + right = mid - 1; + } else { + left = mid + 1; + } + } + + revert("No matching epoch found for the given block number."); + } +} diff --git a/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol b/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol new file mode 100644 index 00000000000..cd15299f441 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/EpochManagerEnabler.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "../common/UsingRegistry.sol"; +import "../common/UsingPrecompiles.sol"; + +import "../../contracts/common/Initializable.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../../contracts/governance/interfaces/IEpochRewards.sol"; +import "../../contracts/common/interfaces/IEpochManagerEnabler.sol"; +import "./interfaces/IEpochManagerEnablerInitializer.sol"; + +/** + * @title Contract Used to initialize the EpochManager system after L2 transition. + */ +contract EpochManagerEnabler is + Initializable, + UsingRegistry, + UsingPrecompiles, + IEpochManagerEnabler, + IEpochManagerEnablerInitializer +{ + uint256 public lastKnownEpochNumber; + uint256 public lastKnownFirstBlockOfEpoch; + address[] public lastKnownElectedAccounts; + + event LastKnownEpochNumberSet(uint256 lastKnownEpochNumber); + event LastKnownFirstBlockOfEpochSet(uint256 lastKnownFirstBlockOfEpoch); + event LastKnownElectedAccountsSet(); + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + * @param registryAddress The address of the registry core smart contract. + */ + function initialize(address registryAddress) external initializer { + _transferOwnership(msg.sender); + setRegistry(registryAddress); + } + + /** + * @notice initializes the epochManager contract during L2 transition. + */ + function initEpochManager() external onlyL2 { + require(lastKnownEpochNumber != 0, "lastKnownEpochNumber not set."); + require(lastKnownFirstBlockOfEpoch != 0, "lastKnownFirstBlockOfEpoch not set."); + require(lastKnownElectedAccounts.length > 0, "lastKnownElectedAccounts not set."); + getEpochManager().initializeSystem( + lastKnownEpochNumber, + lastKnownFirstBlockOfEpoch, + lastKnownElectedAccounts + ); + } + + /** + * @notice Stores the last known epochNumber and the related elected validator accounts. + */ + function captureEpochAndValidators() external onlyL1 { + lastKnownEpochNumber = getEpochNumber(); + emit LastKnownEpochNumberSet(lastKnownEpochNumber); + + uint256 numberElectedValidators = numberValidatorsInCurrentSet(); + lastKnownElectedAccounts = new address[](numberElectedValidators); + _setFirstBlockOfEpoch(); + + for (uint256 i = 0; i < numberElectedValidators; i++) { + address validatorAccountAddress = getAccounts().validatorSignerToAccount( + validatorSignerAddressFromCurrentSet(i) + ); + lastKnownElectedAccounts[i] = validatorAccountAddress; + } + emit LastKnownElectedAccountsSet(); + } + + /** + * @return a list of know elected validator accounts. + */ + function getlastKnownElectedAccounts() external view returns (address[] memory) { + return lastKnownElectedAccounts; + } + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } + + /** + * @notice Sets the first block of the current epoch. + * @dev Only callable on L1. + */ + function _setFirstBlockOfEpoch() internal onlyL1 { + uint256 blocksSinceEpochBlock = block.number % getEpochSize(); + uint256 epochBlock = block.number - blocksSinceEpochBlock; + lastKnownFirstBlockOfEpoch = epochBlock; + emit LastKnownFirstBlockOfEpochSet(lastKnownFirstBlockOfEpoch); + } +} diff --git a/packages/protocol/contracts-0.8/common/FeeCurrencyDirectory.sol b/packages/protocol/contracts-0.8/common/FeeCurrencyDirectory.sol index 604f51f9a12..5be3710e96d 100644 --- a/packages/protocol/contracts-0.8/common/FeeCurrencyDirectory.sol +++ b/packages/protocol/contracts-0.8/common/FeeCurrencyDirectory.sol @@ -4,13 +4,33 @@ pragma solidity ^0.8.0; import "../../contracts/common/Initializable.sol"; import "./interfaces/IOracle.sol"; import "./interfaces/IFeeCurrencyDirectory.sol"; +import "./interfaces/IFeeCurrencyDirectoryInitializer.sol"; import "@openzeppelin/contracts8/access/Ownable.sol"; -contract FeeCurrencyDirectory is IFeeCurrencyDirectory, Initializable, Ownable { +contract FeeCurrencyDirectory is + IFeeCurrencyDirectory, + IFeeCurrencyDirectoryInitializer, + Initializable, + Ownable +{ mapping(address => CurrencyConfig) public currencies; address[] private currencyList; - constructor(bool test) public Initializable(test) {} + /** + * @notice Emitted when currency config is set. + * @param token Address of the added currency token. + * @param oracle Address of the currency token oracle. + * @param intrinsicGas The intrinsic gas value for transactions. + */ + event CurrencyConfigSet(address indexed token, address indexed oracle, uint256 intrinsicGas); + + /** + * @notice Emitted when currency is removed. + * @param token Address of the removed currency token. + */ + event CurrencyRemoved(address indexed token); + + constructor(bool test) Initializable(test) {} /** * @notice Initializes the contract with the owner set. @@ -37,6 +57,7 @@ contract FeeCurrencyDirectory is IFeeCurrencyDirectory, Initializable, Ownable { currencies[token] = CurrencyConfig({ oracle: oracle, intrinsicGas: intrinsicGas }); currencyList.push(token); + emit CurrencyConfigSet(token, oracle, intrinsicGas); } /** @@ -52,6 +73,7 @@ contract FeeCurrencyDirectory is IFeeCurrencyDirectory, Initializable, Ownable { delete currencies[token]; currencyList[index] = currencyList[currencyList.length - 1]; currencyList.pop(); + emit CurrencyRemoved(token); } /** diff --git a/packages/protocol/contracts-0.8/common/GasPriceMinimum.sol b/packages/protocol/contracts-0.8/common/GasPriceMinimum.sol index 3890d00e622..4a30ce92d3d 100644 --- a/packages/protocol/contracts-0.8/common/GasPriceMinimum.sol +++ b/packages/protocol/contracts-0.8/common/GasPriceMinimum.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.8.7 <0.8.20; import "@openzeppelin/contracts8/access/Ownable.sol"; @@ -23,20 +23,19 @@ contract GasPriceMinimum is IsL2Check, CalledByVm { - // TODO add IGasPriceMinimum using FixidityLib for FixidityLib.Fraction; - uint256 public deprecated_gasPriceMinimum; - uint256 public gasPriceMinimumFloor; + uint256 private deprecated_gasPriceMinimum; + uint256 private deprecated_gasPriceMinimumFloor; // Block congestion level targeted by the gas price minimum calculation. - FixidityLib.Fraction public targetDensity; + FixidityLib.Fraction private deprecated_targetDensity; // Speed of gas price minimum adjustment due to congestion. - FixidityLib.Fraction public adjustmentSpeed; + FixidityLib.Fraction private deprecated_adjustmentSpeed; - uint256 public baseFeeOpCodeActivationBlock; - uint256 public constant ABSOLUTE_MINIMAL_GAS_PRICE = 1; + uint256 private deprecated_baseFeeOpCodeActivationBlock; + uint256 private constant ABSOLUTE_MINIMAL_GAS_PRICE = 1; event TargetDensitySet(uint256 targetDensity); event GasPriceMinimumFloorSet(uint256 gasPriceMinimumFloor); @@ -48,7 +47,7 @@ contract GasPriceMinimum is * @notice Sets initialized == true on implementation contracts * @param test Set to true to skip implementation initialization */ - constructor(bool test) public Initializable(test) {} + constructor(bool test) Initializable(test) {} /** * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. @@ -112,7 +111,7 @@ contract GasPriceMinimum is * @param tokenAddress The currency the gas price should be in (defaults to Celo). * @return current gas price minimum in the requested currency */ - function getGasPriceMinimum(address tokenAddress) external view returns (uint256) { + function getGasPriceMinimum(address tokenAddress) external view onlyL1 returns (uint256) { return Math.max(_getGasPriceMinimum(tokenAddress), ABSOLUTE_MINIMAL_GAS_PRICE); } /** @@ -132,8 +131,11 @@ contract GasPriceMinimum is * @dev Value is expected to be < 1. */ function setAdjustmentSpeed(uint256 _adjustmentSpeed) public onlyOwner onlyL1 { - adjustmentSpeed = FixidityLib.wrap(_adjustmentSpeed); - require(adjustmentSpeed.lt(FixidityLib.fixed1()), "adjustment speed must be smaller than 1"); + deprecated_adjustmentSpeed = FixidityLib.wrap(_adjustmentSpeed); + require( + deprecated_adjustmentSpeed.lt(FixidityLib.fixed1()), + "adjustment speed must be smaller than 1" + ); emit AdjustmentSpeedSet(_adjustmentSpeed); } @@ -143,8 +145,11 @@ contract GasPriceMinimum is * @dev Value is expected to be < 1. */ function setTargetDensity(uint256 _targetDensity) public onlyOwner onlyL1 { - targetDensity = FixidityLib.wrap(_targetDensity); - require(targetDensity.lt(FixidityLib.fixed1()), "target density must be smaller than 1"); + deprecated_targetDensity = FixidityLib.wrap(_targetDensity); + require( + deprecated_targetDensity.lt(FixidityLib.fixed1()), + "target density must be smaller than 1" + ); emit TargetDensitySet(_targetDensity); } @@ -155,12 +160,19 @@ contract GasPriceMinimum is */ function setGasPriceMinimumFloor(uint256 _gasPriceMinimumFloor) public onlyOwner onlyL1 { require(_gasPriceMinimumFloor > 0, "gas price minimum floor must be greater than zero"); - gasPriceMinimumFloor = _gasPriceMinimumFloor; + deprecated_gasPriceMinimumFloor = _gasPriceMinimumFloor; emit GasPriceMinimumFloorSet(_gasPriceMinimumFloor); } - function gasPriceMinimum() public view returns (uint256) { - if (baseFeeOpCodeActivationBlock > 0 && block.number >= baseFeeOpCodeActivationBlock) { + /** + * @notice Returns the gas price minimum. + * @return The gas price minimum. + */ + function gasPriceMinimum() public view onlyL1 returns (uint256) { + if ( + deprecated_baseFeeOpCodeActivationBlock > 0 && + block.number >= deprecated_baseFeeOpCodeActivationBlock + ) { return block.basefee; } else { return deprecated_gasPriceMinimum; @@ -179,25 +191,60 @@ contract GasPriceMinimum is function getUpdatedGasPriceMinimum( uint256 blockGasTotal, uint256 blockGasLimit - ) public view returns (uint256) { + ) public view onlyL1 returns (uint256) { FixidityLib.Fraction memory blockDensity = FixidityLib.newFixedFraction( blockGasTotal, blockGasLimit ); - bool densityGreaterThanTarget = blockDensity.gt(targetDensity); + bool densityGreaterThanTarget = blockDensity.gt(deprecated_targetDensity); FixidityLib.Fraction memory densityDelta = densityGreaterThanTarget - ? blockDensity.subtract(targetDensity) - : targetDensity.subtract(blockDensity); + ? blockDensity.subtract(deprecated_targetDensity) + : deprecated_targetDensity.subtract(blockDensity); FixidityLib.Fraction memory adjustment = densityGreaterThanTarget - ? FixidityLib.fixed1().add(adjustmentSpeed.multiply(densityDelta)) - : FixidityLib.fixed1().subtract(adjustmentSpeed.multiply(densityDelta)); + ? FixidityLib.fixed1().add(deprecated_adjustmentSpeed.multiply(densityDelta)) + : FixidityLib.fixed1().subtract(deprecated_adjustmentSpeed.multiply(densityDelta)); uint256 newGasPriceMinimum = adjustment .multiply(FixidityLib.newFixed(gasPriceMinimum())) .add(FixidityLib.fixed1()) .fromFixed(); - return newGasPriceMinimum >= gasPriceMinimumFloor ? newGasPriceMinimum : gasPriceMinimumFloor; + return + newGasPriceMinimum >= deprecated_gasPriceMinimumFloor + ? newGasPriceMinimum + : deprecated_gasPriceMinimumFloor; + } + + /** + * @notice Returns the gas price minimum floor. + * @return The gas price minimum floor. + */ + function gasPriceMinimumFloor() external view onlyL1 returns (uint256) { + return deprecated_gasPriceMinimumFloor; + } + + /** + * @notice Returns the target density. + * @return The target density. + */ + function targetDensity() external view onlyL1 returns (uint256) { + return deprecated_targetDensity.unwrap(); + } + + /** + * @notice Returns the adjustment speed. + * @return The adjustment speed. + */ + function adjustmentSpeed() external view onlyL1 returns (uint256) { + return deprecated_adjustmentSpeed.unwrap(); + } + + /** + * @notice Returns the basefee opcode activation block. + * @return The basefee opcode activation block. + */ + function baseFeeOpCodeActivationBlock() external view onlyL1 returns (uint256) { + return deprecated_baseFeeOpCodeActivationBlock; } /** @@ -213,14 +260,14 @@ contract GasPriceMinimum is allowZero || _baseFeeOpCodeActivationBlock > 0, "baseFee opCode activation block must be greater than zero" ); - baseFeeOpCodeActivationBlock = _baseFeeOpCodeActivationBlock; + deprecated_baseFeeOpCodeActivationBlock = _baseFeeOpCodeActivationBlock; emit BaseFeeOpCodeActivationBlockSet(_baseFeeOpCodeActivationBlock); } function _getGasPriceMinimum(address tokenAddress) private view returns (uint256) { if ( tokenAddress == address(0) || - tokenAddress == registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID) + tokenAddress == registry.getAddressForOrDie(CELO_TOKEN_REGISTRY_ID) ) { return gasPriceMinimum(); } else { diff --git a/packages/protocol/contracts-0.8/common/IsL2Check.sol b/packages/protocol/contracts-0.8/common/IsL2Check.sol index d222ecdb5c6..1ed706c08f5 100644 --- a/packages/protocol/contracts-0.8/common/IsL2Check.sol +++ b/packages/protocol/contracts-0.8/common/IsL2Check.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.5.13 <0.8.20; /** @@ -6,11 +7,17 @@ pragma solidity >=0.5.13 <0.8.20; contract IsL2Check { address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; + /** + * @notice Throws if called on L2. + */ modifier onlyL1() { allowOnlyL1(); _; } + /** + * @notice Throws if called on L1. + */ modifier onlyL2() { if (!isL2()) { revert("This method is not supported in L1."); @@ -18,7 +25,11 @@ contract IsL2Check { _; } - function isL2() public view returns (bool) { + /** + * @notice Checks to see if current network is Celo L2. + * @return Whether or not the current network is a Celo L2. + */ + function isL2() internal view returns (bool) { uint32 size; address _addr = proxyAdminAddress; assembly { @@ -27,6 +38,10 @@ contract IsL2Check { return (size > 0); } + /** + * @notice Used to restrict usage of the parent function to L1 execution. + * @dev Reverts if called on a Celo L2 network. + */ function allowOnlyL1() internal view { if (isL2()) { revert("This method is no longer supported in L2."); diff --git a/packages/protocol/contracts-0.8/common/MentoFeeCurrencyAdapter.sol b/packages/protocol/contracts-0.8/common/MentoFeeCurrencyAdapter.sol index 47f29504417..fa76f743478 100644 --- a/packages/protocol/contracts-0.8/common/MentoFeeCurrencyAdapter.sol +++ b/packages/protocol/contracts-0.8/common/MentoFeeCurrencyAdapter.sol @@ -14,7 +14,7 @@ contract MentoFeeCurrencyAdapter is IOracle, Initializable, Ownable { mapping(address => MentoCurrencyConfig) public currencies; address[] private currencyList; - constructor(bool test) public Initializable(test) {} + constructor(bool test) Initializable(test) {} /** * @notice Initializes the contract with the owner set. diff --git a/packages/protocol/contracts-0.8/common/MintGoldSchedule.sol b/packages/protocol/contracts-0.8/common/MintGoldSchedule.sol deleted file mode 100644 index e423cf40b1d..00000000000 --- a/packages/protocol/contracts-0.8/common/MintGoldSchedule.sol +++ /dev/null @@ -1,330 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.7 <0.8.20; - -import "@openzeppelin/contracts8/security/ReentrancyGuard.sol"; -import "@openzeppelin/contracts8/utils/math/Math.sol"; - -import "./UsingRegistry.sol"; -import "../common/IsL2Check.sol"; - -import "../../contracts/common/FixidityLib.sol"; -import "../../contracts/common/Initializable.sol"; -import "../../contracts-0.8/common/interfaces/IGoldToken.sol"; - -/** - * @title Contract for minting new CELO token based on a schedule. - */ -contract MintGoldSchedule is UsingRegistry, ReentrancyGuard, Initializable, IsL2Check { - using FixidityLib for FixidityLib.Fraction; - - uint256 constant GENESIS_GOLD_SUPPLY = 600000000 ether; // 600 million Gold - uint256 constant GOLD_SUPPLY_CAP = 1000000000 ether; // 1 billion Gold - uint256 constant YEARS_LINEAR = 15; - uint256 constant SECONDS_LINEAR = YEARS_LINEAR * 365 * 1 days; - - bool public areDependenciesSet; - uint256 constant GENESIS_START_TIME = 1587587214; // Copied over from `EpochRewards().startTime()`. - uint256 public l2StartTime; - uint256 public totalSupplyAtL2Start; - - uint256 public totalMintedBySchedule; - address public communityRewardFund; - address public carbonOffsettingPartner; - - FixidityLib.Fraction private communityRewardFraction; - FixidityLib.Fraction private carbonOffsettingFraction; - - event CommunityRewardFractionSet(uint256 fraction); - event CarbonOffsettingFundSet(address indexed partner, uint256 fraction); - - modifier whenActivated() { - require(areDependenciesSet, "Minting schedule has not been activated."); - _; - } - - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Initializable(test) {} - - /** - * @notice A constructor for initialising a new instance of a MintGoldSchedule contract. - */ - function initialize() external initializer { - _transferOwnership(msg.sender); - } - - /** - * @notice Sets the minting schedule dependencies during L2 transition. - * @param _l2StartTime The timestamp of L1 to L2 transition. - * @param _communityRewardFraction The percentage of rewards that go the community funds. - * @param _carbonOffsettingPartner The address of the carbon offsetting partner. - * @param _carbonOffsettingFraction The percentage of rewards going to carbon offsetting partner. - * @param registryAddress Address of the deployed contracts registry. - */ - function activate( - uint256 _l2StartTime, - uint256 _communityRewardFraction, - address _carbonOffsettingPartner, - uint256 _carbonOffsettingFraction, - address registryAddress - ) external onlyOwner onlyL2 { - require(!areDependenciesSet, "Contract has already been activated."); - require(registryAddress != address(0), "The registry address cannot be the zero address"); - require(block.timestamp > _l2StartTime, "L2 start time cannot be set to a future date."); - areDependenciesSet = true; - l2StartTime = _l2StartTime; - setRegistry(registryAddress); - communityRewardFund = address(getGovernance()); - totalSupplyAtL2Start = getGoldToken().totalSupply(); - setCommunityRewardFraction(_communityRewardFraction); - setCarbonOffsettingFund(_carbonOffsettingPartner, _carbonOffsettingFraction); - } - - /** - * @notice Mints CELO to the community and carbon offsetting funds according to the predefined schedule. - */ - function mintAccordingToSchedule() external nonReentrant onlyL2 returns (bool) { - ( - uint256 targetGoldTotalSupply, - uint256 communityRewardFundMintAmount, - uint256 carbonOffsettingPartnerMintAmount - ) = getTargetGoldTotalSupply(); - - uint256 mintableAmount = Math.min( - getRemainingBalanceToMint(), - targetGoldTotalSupply - getGoldToken().totalSupply() - ); - - require(mintableAmount > 0, "Mintable amount must be greater than zero"); - totalMintedBySchedule += mintableAmount; - - IGoldToken goldToken = IGoldToken(address(getGoldToken())); - require( - goldToken.mint(communityRewardFund, communityRewardFundMintAmount), - "Failed to mint to community partner." - ); - - require( - goldToken.mint(carbonOffsettingPartner, carbonOffsettingPartnerMintAmount), - "Failed to mint to carbon offsetting partner." - ); - return true; - } - - /** - * @notice Returns the community reward fraction. - * @return The percentage of total reward which goes to the community funds. - */ - function getCommunityRewardFraction() external view returns (uint256) { - return communityRewardFraction.unwrap(); - } - - /** - * @notice Returns the carbon offsetting partner reward fraction. - * @return The percentage of total reward which goes to the carbon offsetting partner. - */ - function getCarbonOffsettingFraction() external view returns (uint256) { - return carbonOffsettingFraction.unwrap(); - } - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 0, 0); - } - - /** - * @notice Sets the community reward percentage - * @param value The percentage of the total reward to be sent to the community funds as Fixidity fraction. - * @return True upon success. - */ - function setCommunityRewardFraction(uint256 value) public onlyOwner whenActivated returns (bool) { - uint256 timeSinceL2Start = block.timestamp - l2StartTime; - uint256 totalL2LinearSecondsAvailable = SECONDS_LINEAR - (l2StartTime - GENESIS_START_TIME); - FixidityLib.Fraction memory wrappedValue = FixidityLib.wrap(value); - require( - timeSinceL2Start < totalL2LinearSecondsAvailable, - "Can only update fraction once block reward calculation for years 15-30 has been implemented." - ); - require( - !wrappedValue.equals(communityRewardFraction) && wrappedValue.lt(FixidityLib.fixed1()), - "Value must be different from existing community reward fraction and less than 1." - ); - communityRewardFraction = wrappedValue; - require( - FixidityLib.fixed1().gte(communityRewardFraction.add(carbonOffsettingFraction)), - "Sum of partner fractions must be less than or equal to 1." - ); - emit CommunityRewardFractionSet(value); - return true; - } - - /** - * @notice Sets the carbon offsetting fund. - * @param partner The address of the carbon offsetting partner. - * @param value The percentage of the total reward to be sent to the carbon offsetting partner as Fixidity fraction. - * @return True upon success. - */ - function setCarbonOffsettingFund( - address partner, - uint256 value - ) public onlyOwner whenActivated returns (bool) { - require(partner != address(0), "Partner cannot be the zero address."); - uint256 timeSinceL2Start = block.timestamp - l2StartTime; - uint256 totalL2LinearSecondsAvailable = SECONDS_LINEAR - (l2StartTime - GENESIS_START_TIME); - require( - timeSinceL2Start < totalL2LinearSecondsAvailable, - "Can only update fraction once block reward calculation for years 15-30 has been implemented." - ); - FixidityLib.Fraction memory wrappedValue = FixidityLib.wrap(value); - require( - partner != carbonOffsettingPartner || !wrappedValue.equals(carbonOffsettingFraction), - "Partner and value must be different from existing carbon offsetting fund." - ); - require(wrappedValue.lt(FixidityLib.fixed1()), "Value must be less than 1."); - carbonOffsettingPartner = partner; - carbonOffsettingFraction = wrappedValue; - require( - FixidityLib.fixed1().gte(communityRewardFraction.add(carbonOffsettingFraction)), - "Sum of partner fractions must be less than or equal to 1." - ); - emit CarbonOffsettingFundSet(partner, value); - return true; - } - - /** - * @notice Calculates remaining CELO balance to mint. - * @return The remaining CELO balance to mint. - */ - function getRemainingBalanceToMint() public view returns (uint256) { - return GOLD_SUPPLY_CAP - getGoldToken().totalSupply(); - } - - /** - * @return The total balance minted by the MintGoldSchedule contract. - */ - function getTotalMintedBySchedule() public view returns (uint256) { - return totalMintedBySchedule; - } - - /** - * @return The currently mintable amount. - */ - function getMintableAmount() public view returns (uint256) { - (uint256 targetGoldTotalSupply, , ) = getTargetGoldTotalSupply(); - return targetGoldTotalSupply - getGoldToken().totalSupply(); - } - - /** - * @notice Returns the target CELO supply according to the target schedule. - * @return targetGoldTotalSupply The target total CELO supply according to the target schedule. - * @return communityTargetRewards The community reward that can be minted according to the target schedule. - * @return carbonFundTargetRewards The carbon offsetting reward that can be minted according to the target schedule. - */ - function getTargetGoldTotalSupply() - public - view - whenActivated - returns ( - uint256 targetGoldTotalSupply, - uint256 communityTargetRewards, - uint256 carbonFundTargetRewards - ) - { - require(block.timestamp > GENESIS_START_TIME, "GENESIS_START_TIME has not yet been reached."); - require(block.timestamp > l2StartTime, "l2StartTime has not yet been reached."); - - uint256 timeSinceL2Start = block.timestamp - l2StartTime; - uint256 totalL2LinearSecondsAvailable = SECONDS_LINEAR - (l2StartTime - GENESIS_START_TIME); - uint256 mintedOnL1 = totalSupplyAtL2Start - GENESIS_GOLD_SUPPLY; - - bool isLinearDistribution = timeSinceL2Start < totalL2LinearSecondsAvailable; - if (isLinearDistribution) { - ( - targetGoldTotalSupply, - communityTargetRewards, - carbonFundTargetRewards - ) = _calculateTargetReward(timeSinceL2Start, totalL2LinearSecondsAvailable, mintedOnL1); - - return (targetGoldTotalSupply, communityTargetRewards, carbonFundTargetRewards); - } else { - ( - targetGoldTotalSupply, - communityTargetRewards, - carbonFundTargetRewards - ) = _calculateTargetReward( - totalL2LinearSecondsAvailable - 1, - totalL2LinearSecondsAvailable, - mintedOnL1 - ); - - bool hasNotYetMintedAllLinearRewards = totalMintedBySchedule + - GENESIS_GOLD_SUPPLY + - mintedOnL1 < - targetGoldTotalSupply; - - if (hasNotYetMintedAllLinearRewards) { - return (targetGoldTotalSupply, communityTargetRewards, carbonFundTargetRewards); - } - revert("Block reward calculation for years 15-30 unimplemented"); - return (0, 0, 0); - } - } - - function _calculateTargetReward( - uint256 elapsedTime, - uint256 _totalL2LinearSecondsAvailable, - uint256 _mintedOnL1 - ) - internal - view - returns ( - uint256 targetGoldTotalSupply, - uint256 communityTargetRewards, - uint256 carbonFundTargetRewards - ) - { - FixidityLib.Fraction memory elapsedTimeFraction = FixidityLib.wrap(elapsedTime); - FixidityLib.Fraction memory totalL2LinearSecondsAvailableFraction = FixidityLib.wrap( - _totalL2LinearSecondsAvailable - ); - // Pay out half of all block rewards linearly. - uint256 totalLinearRewards = (GOLD_SUPPLY_CAP - GENESIS_GOLD_SUPPLY) / 2; //(200 million) includes validator rewards. - - FixidityLib.Fraction memory l2LinearRewards = FixidityLib.newFixed( - totalLinearRewards - _mintedOnL1 - ); - - FixidityLib.Fraction memory linearRewardsToCommunity = l2LinearRewards.multiply( - communityRewardFraction - ); - - FixidityLib.Fraction memory linearRewardsToCarbon = l2LinearRewards.multiply( - carbonOffsettingFraction - ); - - communityTargetRewards = ( - linearRewardsToCommunity.multiply(elapsedTimeFraction).divide( - totalL2LinearSecondsAvailableFraction - ) - ).fromFixed(); - - carbonFundTargetRewards = linearRewardsToCarbon - .multiply(elapsedTimeFraction) - .divide(totalL2LinearSecondsAvailableFraction) - .fromFixed(); - - targetGoldTotalSupply = - communityTargetRewards + - carbonFundTargetRewards + - GENESIS_GOLD_SUPPLY + - _mintedOnL1; - } -} diff --git a/packages/protocol/contracts-0.8/common/PrecompilesOverride.sol b/packages/protocol/contracts-0.8/common/PrecompilesOverride.sol new file mode 100644 index 00000000000..5fe623d8b0a --- /dev/null +++ b/packages/protocol/contracts-0.8/common/PrecompilesOverride.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../../contracts-0.8/common/IsL2Check.sol"; + +import "./UsingPrecompiles.sol"; +import "./UsingRegistry.sol"; + +/** + * @title PrecompilesOverride Contract + * @notice This contract allows for a smoother transition from L1 to L2 + * by abstracting away the usingPrecompile contract, and taking care of the L1 to L2 swtiching logic. + **/ +contract PrecompilesOverride is UsingPrecompiles, UsingRegistry { + /** + * @notice Returns the epoch number at a block. + * @param blockNumber Block number where epoch number is calculated. + * @return Epoch number. + */ + function getEpochNumberOfBlock(uint256 blockNumber) public view override returns (uint256) { + if (isL2()) { + return getEpochManager().getEpochNumberOfBlock(blockNumber); + } else { + return epochNumberOfBlock(blockNumber, getEpochSize()); + } + } + + /** + * @notice Returns the epoch number at a block. + * @return Current epoch number. + */ + function getEpochNumber() public view override returns (uint256) { + return getEpochNumberOfBlock(block.number); + } + + /** + * @notice Gets a validator signer address from the current validator set. + * @param index Index of requested validator in the validator set. + * @return Address of validator signer at the requested index. + */ + function validatorSignerAddressFromCurrentSet( + uint256 index + ) public view override returns (address) { + if (isL2()) { + return getEpochManager().getElectedSignerByIndex(index); + } else { + return super.validatorSignerAddressFromCurrentSet(index); + } + } + + /** + * @notice Gets a validator address from the current validator set. + * @param index Index of requested validator in the validator set. + * @return Address of validator at the requested index. + */ + function validatorAddressFromCurrentSet(uint256 index) public view onlyL2 returns (address) { + return getEpochManager().getElectedAccountByIndex(index); + } + + /** + * @notice Gets the size of the current elected validator set. + * @return Size of the current elected validator set. + */ + function numberValidatorsInCurrentSet() public view override returns (uint256) { + if (isL2()) { + return getEpochManager().numberOfElectedInCurrentSet(); + } else { + return super.numberValidatorsInCurrentSet(); + } + } +} diff --git a/packages/protocol/contracts-0.8/common/ProxyFactory08.sol b/packages/protocol/contracts-0.8/common/ProxyFactory08.sol new file mode 100644 index 00000000000..726969e62b3 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/ProxyFactory08.sol @@ -0,0 +1,41 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import "../../contracts/common/interfaces/IProxy.sol"; + +import "@openzeppelin/contracts8/utils/Create2.sol"; + +/** + * @title Used for deploying Proxy contracts using Create2. + */ +contract ProxyFactory08 { + /** + * @notice Deploys a new bytecode and transfers ownership to the provided address. + * @param value Amount of CELO to transfer to the new contract. + * @param owner The proxy owner to set. + * @param _salt The Create2 salt to use. + * @param initCode The contract init code to use for deployment. + * @return Address of the deployed contract. + * @dev Calls with the same initCode, sender, and salt will revert as two + * contracts cannot get deployed to the same address. + * @dev Assumes the deployed contract is a Proxy with a `_transferOwnership` + * function that will be called. Will revert if that call reverts. + */ + function deployArbitraryByteCode( + uint256 value, + address owner, + uint256 _salt, + bytes memory initCode + ) external returns (address) { + address deployedContract = Create2.deploy( + value, + keccak256(abi.encode(_salt, msg.sender)), + abi.encodePacked(initCode) + ); + + IProxy proxy = IProxy(deployedContract); + proxy._transferOwnership(owner); + + return deployedContract; + } +} diff --git a/packages/protocol/contracts-0.8/common/ScoreManager.sol b/packages/protocol/contracts-0.8/common/ScoreManager.sol new file mode 100644 index 00000000000..6888a2c433b --- /dev/null +++ b/packages/protocol/contracts-0.8/common/ScoreManager.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "../../contracts/common/Initializable.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "@openzeppelin/contracts8/access/Ownable.sol"; + +import "../../contracts/common/interfaces/IScoreManagerGovernance.sol"; +import "../../contracts/common/interfaces/IScoreManager.sol"; + +/** + * @title ScoreManager contract + * @notice This contract updates the score of validators and validator groups on L2. + * This replaces the previous method of calculating scores based on validator uptime + * with a governable score. + */ +contract ScoreManager is + Initializable, + Ownable, + IScoreManager, + IScoreManagerGovernance, + ICeloVersionedContract +{ + struct Score { + uint256 score; + bool exists; + } + + uint256 private constant FIXED1_UINT = 1e24; + uint256 public constant ZERO_SCORE = FIXED1_UINT + 1; + + mapping(address => uint256) public groupScores; + mapping(address => uint256) public validatorScores; + address private scoreManagerSetter; + + event GroupScoreSet(address indexed group, uint256 score); + event ValidatorScoreSet(address indexed validator, uint256 score); + event ScoreManagerSetterSet(address indexed scoreManagerSetter); + + /** + * @notice Reverts if msg.sender is not authorized to update score. + */ + modifier onlyAuthorizedToUpdateScore() { + require( + msg.sender == owner() || scoreManagerSetter == msg.sender, + "Sender not authorized to update score" + ); + _; + } + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + */ + function initialize() external initializer { + _transferOwnership(msg.sender); + } + + /** + * @notice Sets the group score for a specified group. + * @param group The address of the group whose score will be updated. + * @param score The new score of the group to be updated. + * @dev Set value to `ZERO_SCORE` to set score to zero. + */ + function setGroupScore(address group, uint256 score) external onlyAuthorizedToUpdateScore { + groupScores[group] = checkScore(score); + emit GroupScoreSet(group, score); + } + + /** + * @notice Sets the score for a specified validator. + * @param validator The address of the validator whose score will be updated. + * @param score The new score of the validator to be updated. + * @dev Set value to `ZERO_SCORE` to set score to zero. + */ + function setValidatorScore( + address validator, + uint256 score + ) external onlyAuthorizedToUpdateScore { + validatorScores[validator] = checkScore(score); + emit ValidatorScoreSet(validator, score); + } + + /** + * @notice Sets the whitelisted address allowed to set validator and group scores. + * @param _scoreManagerSetter Address of whitelisted score setter. + */ + function setScoreManagerSetter(address _scoreManagerSetter) external onlyOwner { + scoreManagerSetter = _scoreManagerSetter; + emit ScoreManagerSetterSet(_scoreManagerSetter); + } + + /** + * @notice Returns the score of the specified group. + * @param group The address of the group of interest. + */ + function getGroupScore(address group) external view returns (uint256) { + return getScore(groupScores[group]); + } + + /** + * @notice Returns the score of the specified validator. + * @param validator The address of the validator of interest. + */ + function getValidatorScore(address validator) external view returns (uint256) { + return getScore(validatorScores[validator]); + } + + /** + * @notice Returns the address of the whitelisted score setter. + */ + function getScoreManagerSetter() external view returns (address) { + return scoreManagerSetter; + } + + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 0, 0); + } + + /** + * @notice Returns the actual score based on the input value. + * @param score The value from `validatorScores` or `groupScores` mappings. + * @dev To set the score to 100% by default when the contract is first initialized + * or when new groups or validators are added, the default score of 0 returns 1e24. + * To encode a score value of 0 we use the magic number ZERO_SCORE = (1e24)+1. + */ + function getScore(uint256 score) internal pure returns (uint256) { + if (score == 0) { + return FIXED1_UINT; + } else if (score == ZERO_SCORE) { + return 0; + } + return score; + } + + /** + * @notice Checks if the score is valid and returns the score. + * @param score The score to be checked. + */ + function checkScore(uint256 score) internal pure returns (uint256) { + if (score == 0) { + return ZERO_SCORE; + } + + require(score <= FIXED1_UINT, "Score must be less than or equal to 1e24."); + return score; + } +} diff --git a/packages/protocol/contracts-0.8/common/UsingPrecompiles.sol b/packages/protocol/contracts-0.8/common/UsingPrecompiles.sol new file mode 100644 index 00000000000..b575953304b --- /dev/null +++ b/packages/protocol/contracts-0.8/common/UsingPrecompiles.sol @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0 <0.8.20; + +// Note: This is not an exact copy of UsingPrecompiles in the contract's folder, but in solidity 0.8 +import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../common/IsL2Check.sol"; + +contract UsingPrecompiles is IsL2Check { + using SafeMath for uint256; + + address constant TRANSFER = address(0xff - 2); + address constant FRACTION_MUL = address(0xff - 3); + address constant PROOF_OF_POSSESSION = address(0xff - 4); + address constant GET_VALIDATOR = address(0xff - 5); + address constant NUMBER_VALIDATORS = address(0xff - 6); + address constant EPOCH_SIZE = address(0xff - 7); + address constant BLOCK_NUMBER_FROM_HEADER = address(0xff - 8); + address constant HASH_HEADER = address(0xff - 9); + address constant GET_PARENT_SEAL_BITMAP = address(0xff - 10); + address constant GET_VERIFIED_SEAL_BITMAP = address(0xff - 11); + uint256 constant DAY = 86400; + + /** + * @notice calculate a * b^x for fractions a, b to `decimals` precision + * @param aNumerator Numerator of first fraction + * @param aDenominator Denominator of first fraction + * @param bNumerator Numerator of exponentiated fraction + * @param bDenominator Denominator of exponentiated fraction + * @param exponent exponent to raise b to + * @param _decimals precision + * @return Numerator of the computed quantity (not reduced). + * @return Denominator of the computed quantity (not reduced). + * @dev This function will be deprecated in L2. + */ + function fractionMulExp( + uint256 aNumerator, + uint256 aDenominator, + uint256 bNumerator, + uint256 bDenominator, + uint256 exponent, + uint256 _decimals + ) public view onlyL1 returns (uint256, uint256) { + require(aDenominator != 0 && bDenominator != 0, "a denominator is zero"); + uint256 returnNumerator; + uint256 returnDenominator; + bool success; + bytes memory out; + (success, out) = FRACTION_MUL.staticcall( + abi.encodePacked(aNumerator, aDenominator, bNumerator, bDenominator, exponent, _decimals) + ); + require(success, "error calling fractionMulExp precompile"); + returnNumerator = getUint256FromBytes(out, 0); + returnDenominator = getUint256FromBytes(out, 32); + return (returnNumerator, returnDenominator); + } + + /** + * @notice Returns the current epoch size in blocks. + * @return The current epoch size in blocks. + * @dev This function will be deprecated in L2. + */ + function getEpochSize() public view onlyL1 returns (uint256) { + bytes memory out; + bool success; + (success, out) = EPOCH_SIZE.staticcall(abi.encodePacked(true)); + require(success, "error calling getEpochSize precompile"); + return getUint256FromBytes(out, 0); + } + + /** + * @notice Returns the epoch number at a block. + * @param blockNumber Block number where epoch number is calculated. + * @return Epoch number. + * @dev This function will be deprecated in L2. + */ + function getEpochNumberOfBlock(uint256 blockNumber) public view virtual onlyL1 returns (uint256) { + return epochNumberOfBlock(blockNumber, getEpochSize()); + } + + /** + * @notice Returns the epoch number at a block. + * @return Current epoch number. + * @dev This function will be deprecated in L2. + */ + function getEpochNumber() public view virtual onlyL1 returns (uint256) { + return getEpochNumberOfBlock(block.number); + } + + /** + * @notice Gets a validator signer address from the current validator set. + * @param index Index of requested validator in the validator set. + * @return Address of validator signer at the requested index. + * @dev This function will be deprecated in L2. + */ + function validatorSignerAddressFromCurrentSet( + uint256 index + ) public view virtual onlyL1 returns (address) { + bytes memory out; + bool success; + (success, out) = GET_VALIDATOR.staticcall(abi.encodePacked(index, uint256(block.number))); + require(success, "error calling validatorSignerAddressFromCurrentSet precompile"); + return address(uint160(getUint256FromBytes(out, 0))); + } + + /** + * @notice Gets a validator signer address from the validator set at the given block number. + * @param index Index of requested validator in the validator set. + * @param blockNumber Block number to retrieve the validator set from. + * @return Address of validator signer at the requested index. + * @dev This function will be deprecated in L2. + */ + function validatorSignerAddressFromSet( + uint256 index, + uint256 blockNumber + ) public view virtual onlyL1 returns (address) { + bytes memory out; + bool success; + (success, out) = GET_VALIDATOR.staticcall(abi.encodePacked(index, blockNumber)); + require(success, "error calling validatorSignerAddressFromSet precompile"); + return address(uint160(getUint256FromBytes(out, 0))); + } + + /** + * @notice Gets the size of the current elected validator set. + * @return Size of the current elected validator set. + * @dev This function will be deprecated in L2. + */ + function numberValidatorsInCurrentSet() public view virtual onlyL1 returns (uint256) { + bytes memory out; + bool success; + (success, out) = NUMBER_VALIDATORS.staticcall(abi.encodePacked(uint256(block.number))); + require(success, "error calling numberValidatorsInCurrentSet precompile"); + return getUint256FromBytes(out, 0); + } + + /** + * @notice Gets the size of the validator set that must sign the given block number. + * @param blockNumber Block number to retrieve the validator set from. + * @return Size of the validator set. + * @dev This function will be deprecated in L2. + */ + function numberValidatorsInSet(uint256 blockNumber) public view virtual onlyL1 returns (uint256) { + bytes memory out; + bool success; + (success, out) = NUMBER_VALIDATORS.staticcall(abi.encodePacked(blockNumber)); + require(success, "error calling numberValidatorsInSet precompile"); + return getUint256FromBytes(out, 0); + } + + /** + * @notice Checks a BLS proof of possession. + * @param sender The address signed by the BLS key to generate the proof of possession. + * @param blsKey The BLS public key that the validator is using for consensus, should pass proof + * of possession. 48 bytes. + * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the + * account address. 96 bytes. + * @return True upon success. + * @dev This function will be deprecated in L2. + */ + function checkProofOfPossession( + address sender, + bytes memory blsKey, + bytes memory blsPop + ) public view onlyL1 returns (bool) { + bool success; + (success, ) = PROOF_OF_POSSESSION.staticcall(abi.encodePacked(sender, blsKey, blsPop)); + return success; + } + + /** + * @notice Parses block number out of header. + * @param header RLP encoded header + * @return Block number. + * @dev This function will be deprecated in L2. + */ + function getBlockNumberFromHeader(bytes memory header) public view onlyL1 returns (uint256) { + bytes memory out; + bool success; + (success, out) = BLOCK_NUMBER_FROM_HEADER.staticcall(abi.encodePacked(header)); + require(success, "error calling getBlockNumberFromHeader precompile"); + return getUint256FromBytes(out, 0); + } + + /** + * @notice Computes hash of header. + * @param header RLP encoded header + * @return Header hash. + * @dev This function will be deprecated in L2. + */ + function hashHeader(bytes memory header) public view onlyL1 returns (bytes32) { + bytes memory out; + bool success; + (success, out) = HASH_HEADER.staticcall(abi.encodePacked(header)); + require(success, "error calling hashHeader precompile"); + return getBytes32FromBytes(out, 0); + } + + /** + * @notice Gets the parent seal bitmap from the header at the given block number. + * @param blockNumber Block number to retrieve. Must be within 4 epochs of the current number. + * @return Bitmap parent seal with set bits at indices corresponding to signing validators. + * @dev This function will be deprecated in L2. + */ + function getParentSealBitmap(uint256 blockNumber) public view onlyL1 returns (bytes32) { + bytes memory out; + bool success; + (success, out) = GET_PARENT_SEAL_BITMAP.staticcall(abi.encodePacked(blockNumber)); + require(success, "error calling getParentSealBitmap precompile"); + return getBytes32FromBytes(out, 0); + } + + /** + * @notice Verifies the BLS signature on the header and returns the seal bitmap. + * The validator set used for verification is retrieved based on the parent hash field of the + * header. If the parent hash is not in the blockchain, verification fails. + * @param header RLP encoded header + * @return Bitmap parent seal with set bits at indices correspoinding to signing validators. + * @dev This function will be deprecated in L2. + */ + function getVerifiedSealBitmapFromHeader( + bytes memory header + ) public view onlyL1 returns (bytes32) { + bytes memory out; + bool success; + (success, out) = GET_VERIFIED_SEAL_BITMAP.staticcall(abi.encodePacked(header)); + require(success, "error calling getVerifiedSealBitmapFromHeader precompile"); + return getBytes32FromBytes(out, 0); + } + + /** + * @notice Returns the minimum number of required signers for a given block number. + * @dev Computed in celo-blockchain as int(math.Ceil(float64(2*valSet.Size()) / 3)) + * @dev This function will be deprecated in L2. + */ + function minQuorumSize(uint256 blockNumber) public view onlyL1 returns (uint256) { + return numberValidatorsInSet(blockNumber).mul(2).add(2).div(3); + } + + /** + * @notice Computes byzantine quorum from current validator set size + * @return Byzantine quorum of validators. + * @dev This function will be deprecated in L2. + */ + function minQuorumSizeInCurrentSet() public view onlyL1 returns (uint256) { + return minQuorumSize(block.number); + } + + /** + * @notice Returns the epoch number at a block. + * @param blockNumber Block number where epoch number is calculated. + * @param epochSize The epoch size in blocks. + * @return Epoch number. + */ + function epochNumberOfBlock( + uint256 blockNumber, + uint256 epochSize + ) internal pure returns (uint256) { + // Follows GetEpochNumber from celo-blockchain/blob/master/consensus/istanbul/utils.go + uint256 epochNumber = blockNumber / epochSize; + if (blockNumber % epochSize == 0) { + return epochNumber; + } else { + return epochNumber.add(1); + } + } + + /** + * @notice Converts bytes to uint256. + * @param bs byte[] data + * @param start offset into byte data to convert + * @return uint256 data + */ + function getUint256FromBytes(bytes memory bs, uint256 start) internal pure returns (uint256) { + return uint256(getBytes32FromBytes(bs, start)); + } + + /** + * @notice Converts bytes to bytes32. + * @param bs byte[] data + * @param start offset into byte data to convert + * @return bytes32 data + */ + function getBytes32FromBytes(bytes memory bs, uint256 start) internal pure returns (bytes32) { + require(bs.length >= start.add(32), "slicing out of range"); + bytes32 x; + assembly { + x := mload(add(bs, add(start, 32))) + } + return x; + } +} diff --git a/packages/protocol/contracts-0.8/common/UsingRegistry.sol b/packages/protocol/contracts-0.8/common/UsingRegistry.sol index 1c87bff7ef8..dbe23037a40 100644 --- a/packages/protocol/contracts-0.8/common/UsingRegistry.sol +++ b/packages/protocol/contracts-0.8/common/UsingRegistry.sol @@ -9,15 +9,21 @@ import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; import "../../contracts/common/interfaces/IRegistry.sol"; import "../../contracts/common/interfaces/IAccounts.sol"; +import "../../contracts/common/interfaces/IEpochManager.sol"; import "../../contracts/common/interfaces/IFreezer.sol"; +import "../../contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; +import "../../contracts/common/interfaces/IFeeCurrencyWhitelist.sol"; +import "../../contracts/common/interfaces/IFeeHandlerSeller.sol"; +import "../../contracts/common/interfaces/IEpochManager.sol"; import "../../contracts/governance/interfaces/IGovernance.sol"; import "../../contracts/governance/interfaces/ILockedGold.sol"; +import "../../contracts/governance/interfaces/ILockedCelo.sol"; import "../../contracts/governance/interfaces/IValidators.sol"; -import "../../contracts/stability/interfaces/ISortedOracles.sol"; -import "../../contracts/common/interfaces/IFeeCurrencyWhitelist.sol"; import "../../contracts/governance/interfaces/IElection.sol"; +import "../../contracts/governance/interfaces/IEpochRewards.sol"; +import "../../contracts/stability/interfaces/ISortedOracles.sol"; -import "../../contracts/common/interfaces/IFeeHandlerSeller.sol"; +import "./interfaces/IScoreReader.sol"; contract UsingRegistry is Ownable { // solhint-disable state-visibility @@ -27,6 +33,7 @@ contract UsingRegistry is Ownable { bytes32 constant DOUBLE_SIGNING_SLASHER_REGISTRY_ID = keccak256(abi.encodePacked("DoubleSigningSlasher")); bytes32 constant ELECTION_REGISTRY_ID = keccak256(abi.encodePacked("Election")); + bytes32 constant EPOCH_REWARDS_REGISTRY_ID = keccak256(abi.encodePacked("EpochRewards")); bytes32 constant EXCHANGE_REGISTRY_ID = keccak256(abi.encodePacked("Exchange")); bytes32 constant FEE_CURRENCY_WHITELIST_REGISTRY_ID = keccak256(abi.encodePacked("FeeCurrencyWhitelist")); @@ -43,6 +50,14 @@ contract UsingRegistry is Ownable { bytes32 constant VALIDATORS_REGISTRY_ID = keccak256(abi.encodePacked("Validators")); bytes32 constant MENTOFEEHANDLERSELLER_REGISTRY_ID = keccak256(abi.encodePacked("MentoFeeHandlerSeller")); + bytes32 constant CELO_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("CeloToken")); + bytes32 constant LOCKED_CELO_REGISTRY_ID = keccak256(abi.encodePacked("LockedCelo")); + bytes32 constant CELO_UNRELEASED_TREASURY_REGISTRY_ID = + keccak256(abi.encodePacked("CeloUnreleasedTreasury")); + bytes32 constant EPOCH_MANAGER_ENABLER_REGISTRY_ID = + keccak256(abi.encodePacked("EpochManagerEnabler")); + bytes32 constant EPOCH_MANAGER_REGISTRY_ID = keccak256(abi.encodePacked("EpochManager")); + bytes32 constant SCORE_MANAGER_REGISTRY_ID = keccak256(abi.encodePacked("ScoreManager")); // solhint-enable state-visibility IRegistry public registry; @@ -73,6 +88,10 @@ contract UsingRegistry is Ownable { return IERC20(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); } + function getCeloToken() internal view returns (IERC20) { + return IERC20(registry.getAddressForOrDie(CELO_TOKEN_REGISTRY_ID)); + } + function getFreezer() internal view returns (IFreezer) { return IFreezer(registry.getAddressForOrDie(FREEZER_REGISTRY_ID)); } @@ -89,6 +108,10 @@ contract UsingRegistry is Ownable { return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); } + function getLockedCelo() internal view returns (ILockedCelo) { + return ILockedCelo(registry.getAddressForOrDie(LOCKED_CELO_REGISTRY_ID)); + } + // Current version of Mento doesn't support 0.8 function getStableToken() internal view returns (address) { return registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID); @@ -109,7 +132,25 @@ contract UsingRegistry is Ownable { function getElection() internal view returns (IElection) { return IElection(registry.getAddressForOrDie(ELECTION_REGISTRY_ID)); } + + function getEpochRewards() internal view returns (IEpochRewards) { + return IEpochRewards(registry.getAddressForOrDie(EPOCH_REWARDS_REGISTRY_ID)); + } + function getGovernance() internal view returns (IGovernance) { return IGovernance(registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID)); } + + function getCeloUnreleasedTreasury() internal view returns (ICeloUnreleasedTreasury) { + return + ICeloUnreleasedTreasury(registry.getAddressForOrDie(CELO_UNRELEASED_TREASURY_REGISTRY_ID)); + } + + function getEpochManager() internal view returns (IEpochManager) { + return IEpochManager(registry.getAddressForOrDie(EPOCH_MANAGER_REGISTRY_ID)); + } + + function getScoreReader() internal view returns (IScoreReader) { + return IScoreReader(registry.getAddressForOrDie(SCORE_MANAGER_REGISTRY_ID)); + } } diff --git a/packages/protocol/contracts-0.8/common/interfaces/ICeloUnreleasedTreasuryInitializer.sol b/packages/protocol/contracts-0.8/common/interfaces/ICeloUnreleasedTreasuryInitializer.sol new file mode 100644 index 00000000000..d87e8514ca1 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/ICeloUnreleasedTreasuryInitializer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface ICeloUnreleasedTreasuryInitializer { + function initialize(address registryAddress) external; +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerEnablerInitializer.sol b/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerEnablerInitializer.sol new file mode 100644 index 00000000000..3643d5ec711 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerEnablerInitializer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochManagerEnablerInitializer { + function initialize(address registryAddress) external; +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerInitializer.sol b/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerInitializer.sol new file mode 100644 index 00000000000..ee418889182 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IEpochManagerInitializer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochManagerInitializer { + function initialize(address registryAddress, uint256 newEpochDuration) external; +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectory.sol b/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectory.sol index b9d6735b1e1..28850334637 100644 --- a/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectory.sol +++ b/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectory.sol @@ -7,6 +7,15 @@ interface IFeeCurrencyDirectory { uint256 intrinsicGas; } + /** + * @notice Sets the currency configuration for a token. + * @dev This action can only be performed by the contract owner. + * @param token The token address. + * @param oracle The oracle address for price fetching. + * @param intrinsicGas The intrinsic gas value for transactions. + */ + function setCurrencyConfig(address token, address oracle, uint256 intrinsicGas) external; + /** * @notice Returns the list of all currency addresses. * @return An array of addresses. diff --git a/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectoryInitializer.sol b/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectoryInitializer.sol new file mode 100644 index 00000000000..4731a9ca1cd --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IFeeCurrencyDirectoryInitializer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IFeeCurrencyDirectoryInitializer { + function initialize() external; +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IGasPriceMinimum.sol b/packages/protocol/contracts-0.8/common/interfaces/IGasPriceMinimum.sol index 5443840e3ed..24d89284c42 100644 --- a/packages/protocol/contracts-0.8/common/interfaces/IGasPriceMinimum.sol +++ b/packages/protocol/contracts-0.8/common/interfaces/IGasPriceMinimum.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.5.13 <0.9.0; // TODO add to GasPrice diff --git a/packages/protocol/contracts-0.8/common/interfaces/IGasPriceMinimumInitializer.sol b/packages/protocol/contracts-0.8/common/interfaces/IGasPriceMinimumInitializer.sol index a5e0f2bbac1..046cf9faf98 100644 --- a/packages/protocol/contracts-0.8/common/interfaces/IGasPriceMinimumInitializer.sol +++ b/packages/protocol/contracts-0.8/common/interfaces/IGasPriceMinimumInitializer.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.5.13 <0.9.0; interface IGasPriceMinimumInitializer { diff --git a/packages/protocol/contracts-0.8/common/interfaces/IGoldToken.sol b/packages/protocol/contracts-0.8/common/interfaces/IGoldToken.sol deleted file mode 100644 index aa2f579a7f3..00000000000 --- a/packages/protocol/contracts-0.8/common/interfaces/IGoldToken.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.5.13 <0.9.0; - -import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; - -/** - * @dev Interface of the ERC20 standard as defined in the EIP. Does not include - * the optional functions; to access them see {ERC20Detailed}. - */ -interface IGoldToken is IERC20 { - /** - * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. - * @param registryAddress Address of the Registry contract. - */ - function initialize(address registryAddress) external; - - /** - * @notice Updates the address pointing to a Registry contract. - * @param registryAddress The address of a registry contract for routing to other contracts. - */ - function setRegistry(address registryAddress) external; - - /** - * @notice Used set the address of the MintGoldSchedule contract. - * @param goldTokenMintingScheduleAddress The address of the MintGoldSchedule contract. - */ - function setGoldTokenMintingScheduleAddress(address goldTokenMintingScheduleAddress) external; - - /** - * @dev Mints a new token. - * @param to The address that will own the minted token. - * @param value The amount of token to be minted. - */ - function mint(address to, uint256 value) external returns (bool); -} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IPrecompiles.sol b/packages/protocol/contracts-0.8/common/interfaces/IPrecompiles.sol new file mode 100644 index 00000000000..cf36e67ee4b --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IPrecompiles.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.13 <0.9.0; + +interface IPrecompiles { + function getEpochSize() external view returns (uint256); + function getEpochNumber() external view returns (uint256); +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IScoreManager.sol b/packages/protocol/contracts-0.8/common/interfaces/IScoreManager.sol new file mode 100644 index 00000000000..0020fd65df0 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IScoreManager.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IScoreManager { + function setGroupScore(address group, uint256 score) external; + function setValidatorScore(address validator, uint256 score) external; + function getValidatorScore(address validator) external view returns (uint256); + function getGroupScore(address validator) external view returns (uint256); + function owner() external view returns (address); +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IMintGoldScheduleInitializer.sol b/packages/protocol/contracts-0.8/common/interfaces/IScoreManagerInitializer.sol similarity index 73% rename from packages/protocol/contracts-0.8/common/interfaces/IMintGoldScheduleInitializer.sol rename to packages/protocol/contracts-0.8/common/interfaces/IScoreManagerInitializer.sol index 37d71ed70c0..f6229cf72b5 100644 --- a/packages/protocol/contracts-0.8/common/interfaces/IMintGoldScheduleInitializer.sol +++ b/packages/protocol/contracts-0.8/common/interfaces/IScoreManagerInitializer.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.5.13 <0.9.0; -interface IMintGoldScheduleInitializer { +interface IScoreManagerInitializer { function initialize() external; } diff --git a/packages/protocol/contracts-0.8/common/interfaces/IScoreReader.sol b/packages/protocol/contracts-0.8/common/interfaces/IScoreReader.sol new file mode 100644 index 00000000000..f45d57af1eb --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IScoreReader.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +interface IScoreReader { + function getValidatorScore(address validator) external view returns (uint256); + function getGroupScore(address validator) external view returns (uint256); +} diff --git a/packages/protocol/contracts-0.8/common/interfaces/IStableToken.sol b/packages/protocol/contracts-0.8/common/interfaces/IStableToken.sol new file mode 100644 index 00000000000..1fa10340fec --- /dev/null +++ b/packages/protocol/contracts-0.8/common/interfaces/IStableToken.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +/** + * @title This interface describes the functions specific to Celo Stable Tokens, and in the + * absence of interface inheritance is intended as a companion to IERC20.sol and ICeloToken.sol. + */ +interface IStableToken { + function transfer(address, uint256) external returns (bool); + + function mint(address, uint256) external returns (bool); + + function burn(uint256) external returns (bool); + + function setInflationParameters(uint256, uint256) external; + + function valueToUnits(uint256) external view returns (uint256); + + function unitsToValue(uint256) external view returns (uint256); + + function getInflationParameters() external view returns (uint256, uint256, uint256, uint256); + + // NOTE: duplicated with IERC20.sol, remove once interface inheritance is supported. + function balanceOf(address) external view returns (uint256); +} diff --git a/packages/protocol/contracts-0.8/common/linkedlists/AddressLinkedList.sol b/packages/protocol/contracts-0.8/common/linkedlists/AddressLinkedList.sol new file mode 100644 index 00000000000..ce0c4e9fa43 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/linkedlists/AddressLinkedList.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0 <0.8.20; + +import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; + +import "./LinkedList.sol"; + +/** + * @title Maintains a doubly linked list keyed by address. + * @dev Following the `next` pointers will lead you to the head, rather than the tail. + */ +library AddressLinkedList { + using LinkedList for LinkedList.List; + using SafeMath for uint256; + /** + * @notice Inserts an element into a doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + * @param previousKey The key of the element that comes before the element to insert. + * @param nextKey The key of the element that comes after the element to insert. + */ + function insert( + LinkedList.List storage list, + address key, + address previousKey, + address nextKey + ) public { + list.insert(toBytes(key), toBytes(previousKey), toBytes(nextKey)); + } + + /** + * @notice Inserts an element at the end of the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + */ + function push(LinkedList.List storage list, address key) public { + list.insert(toBytes(key), bytes32(0), list.tail); + } + + /** + * @notice Removes an element from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to remove. + */ + function remove(LinkedList.List storage list, address key) public { + list.remove(toBytes(key)); + } + + /** + * @notice Updates an element in the list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @param previousKey The key of the element that comes before the updated element. + * @param nextKey The key of the element that comes after the updated element. + */ + function update( + LinkedList.List storage list, + address key, + address previousKey, + address nextKey + ) public { + list.update(toBytes(key), toBytes(previousKey), toBytes(nextKey)); + } + + /** + * @notice Returns whether or not a particular key is present in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return Whether or not the key is in the sorted list. + */ + function contains(LinkedList.List storage list, address key) public view returns (bool) { + return list.elements[toBytes(key)].exists; + } + + /** + * @notice Returns the N greatest elements of the list. + * @param list A storage pointer to the underlying list. + * @param n The number of elements to return. + * @return The keys of the greatest elements. + * @dev Reverts if n is greater than the number of elements in the list. + */ + function headN(LinkedList.List storage list, uint256 n) public view returns (address[] memory) { + bytes32[] memory byteKeys = list.headN(n); + address[] memory keys = new address[](n); + for (uint256 i = 0; i < n; i = i.add(1)) { + keys[i] = toAddress(byteKeys[i]); + } + return keys; + } + + /** + * @notice Gets all element keys from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return All element keys from head to tail. + */ + function getKeys(LinkedList.List storage list) public view returns (address[] memory) { + return headN(list, list.numElements); + } + + function toBytes(address a) public pure returns (bytes32) { + return bytes32(uint256(uint160(a)) << 96); + } + + function toAddress(bytes32 b) public pure returns (address) { + return address(uint160(uint256(b) >> 96)); + } +} diff --git a/packages/protocol/contracts-0.8/common/linkedlists/IntegerSortedLinkedList.sol b/packages/protocol/contracts-0.8/common/linkedlists/IntegerSortedLinkedList.sol index 25afaeb8c49..baf51f3c303 100644 --- a/packages/protocol/contracts-0.8/common/linkedlists/IntegerSortedLinkedList.sol +++ b/packages/protocol/contracts-0.8/common/linkedlists/IntegerSortedLinkedList.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.8.0 <0.8.20; import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; diff --git a/packages/protocol/contracts-0.8/common/linkedlists/LinkedList.sol b/packages/protocol/contracts-0.8/common/linkedlists/LinkedList.sol index a8aef19478a..de386a113ea 100644 --- a/packages/protocol/contracts-0.8/common/linkedlists/LinkedList.sol +++ b/packages/protocol/contracts-0.8/common/linkedlists/LinkedList.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.8.0 <0.8.20; import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; diff --git a/packages/protocol/contracts-0.8/common/linkedlists/SortedLinkedList.sol b/packages/protocol/contracts-0.8/common/linkedlists/SortedLinkedList.sol index c030435cb3a..4ce7b23b846 100644 --- a/packages/protocol/contracts-0.8/common/linkedlists/SortedLinkedList.sol +++ b/packages/protocol/contracts-0.8/common/linkedlists/SortedLinkedList.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.8.0 <0.8.20; import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; diff --git a/packages/protocol/contracts-0.8/common/linkedlists/SortedLinkedListWithMedian.sol b/packages/protocol/contracts-0.8/common/linkedlists/SortedLinkedListWithMedian.sol index cded44b9d5d..6093b47466d 100644 --- a/packages/protocol/contracts-0.8/common/linkedlists/SortedLinkedListWithMedian.sol +++ b/packages/protocol/contracts-0.8/common/linkedlists/SortedLinkedListWithMedian.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.8.0 <0.8.20; import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; diff --git a/packages/protocol/contracts-0.8/common/mocks/EpochManager_WithMocks.sol b/packages/protocol/contracts-0.8/common/mocks/EpochManager_WithMocks.sol new file mode 100644 index 00000000000..9ddda4aba92 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/mocks/EpochManager_WithMocks.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "../EpochManager.sol"; + +contract EpochManager_WithMocks is EpochManager(true) { + function _setPaymentAllocation(address validator, uint256 amount) external { + validatorPendingPayments[validator] = amount; + } +} diff --git a/packages/protocol/contracts-0.8/common/mocks/MockAccounts.sol b/packages/protocol/contracts-0.8/common/mocks/MockAccounts.sol new file mode 100644 index 00000000000..61a08312b20 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/mocks/MockAccounts.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "../../../contracts/common/FixidityLib.sol"; + +contract MockAccounts { + using FixidityLib for FixidityLib.Fraction; + + struct PaymentDelegation { + // Address that should receive a fraction of validator payments. + address beneficiary; + // Fraction of payment to delegate to `beneficiary`. + FixidityLib.Fraction fraction; + } + + mapping(address => PaymentDelegation) delegations; + mapping(address => address) accountToSigner; + + function setValidatorSigner(address account, address signer) external { + accountToSigner[account] = signer; + } + + function getValidatorSigner(address account) external view returns (address) { + return accountToSigner[account]; + } + + function getPaymentDelegation(address account) external view returns (address, uint256) { + PaymentDelegation storage delegation = delegations[account]; + return (delegation.beneficiary, delegation.fraction.unwrap()); + } + + function setPaymentDelegationFor( + address validator, + address beneficiary, + uint256 fraction + ) public { + delegations[validator] = PaymentDelegation(beneficiary, FixidityLib.wrap(fraction)); + } + + function deletePaymentDelegationFor(address validator) public { + delete delegations[validator]; + } +} diff --git a/packages/protocol/contracts-0.8/common/test/MockCeloToken.sol b/packages/protocol/contracts-0.8/common/test/MockCeloToken.sol new file mode 100644 index 00000000000..0c85c62cf80 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/test/MockCeloToken.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0 <0.9.0; +// solhint-disable no-unused-vars + +/** + * @title A mock StableToken for testing. This contract can be deprecated once GoldToken gets migrated to 0.8 + */ +contract MockCeloToken08 { + uint256 public totalSupply_; + uint8 public constant decimals = 18; + mapping(address => uint256) balances; + + function setTotalSupply(uint256 value) external { + totalSupply_ = value; + } + + function transfer(address to, uint256 amount) external returns (bool) { + return _transfer(msg.sender, to, amount); + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + return _transfer(from, to, amount); + } + + function setBalanceOf(address a, uint256 value) external { + balances[a] = value; + } + + function balanceOf(address a) public view returns (uint256) { + return balances[a]; + } + + function totalSupply() public view returns (uint256) { + return totalSupply_; + } + + function _transfer(address from, address to, uint256 amount) internal returns (bool) { + if (balances[from] < amount) { + return false; + } + balances[from] -= amount; + balances[to] += amount; + return true; + } +} diff --git a/packages/protocol/contracts-0.8/common/test/MockCeloUnreleasedTreasury.sol b/packages/protocol/contracts-0.8/common/test/MockCeloUnreleasedTreasury.sol new file mode 100644 index 00000000000..0adbec9dd61 --- /dev/null +++ b/packages/protocol/contracts-0.8/common/test/MockCeloUnreleasedTreasury.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0 <0.9.0; +// solhint-disable no-unused-vars + +import "../../../contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; +import "../UsingRegistry.sol"; + +/** + * @title A mock CeloUnreleasedTreasury for testing. + */ +contract MockCeloUnreleasedTreasury is ICeloUnreleasedTreasury, UsingRegistry { + bool internal hasAlreadyReleased; + uint256 internal remainingTreasure; + function release(address to, uint256 amount) external { + if (!hasAlreadyReleased) { + remainingTreasure = address(this).balance; + hasAlreadyReleased = true; + } + + require(remainingTreasure >= amount, "Insufficient balance."); + require(getCeloToken().transfer(to, amount), "CELO transfer failed."); + remainingTreasure -= amount; + } + + function getRemainingBalanceToRelease() external view returns (uint256) { + remainingTreasure; + } + + function setRemainingTreasure(uint256 _amount) public { + remainingTreasure = _amount; + } + + function setFirstRelease(bool _hasAlreadyReleased) public { + hasAlreadyReleased = _hasAlreadyReleased; + } +} diff --git a/packages/protocol/contracts-0.8/common/test/MockRegistry.sol b/packages/protocol/contracts-0.8/common/test/MockRegistry.sol new file mode 100644 index 00000000000..7b50f86119d --- /dev/null +++ b/packages/protocol/contracts-0.8/common/test/MockRegistry.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts8/access/Ownable.sol"; + +import "../../../contracts/common/interfaces/IRegistry.sol"; +import "../../../contracts/common/interfaces/IRegistryInitializer.sol"; +import "../../../contracts/common/Initializable.sol"; + +/** + * @title Routes identifiers to addresses. + */ +contract MockRegistry is IRegistry, IRegistryInitializer, Ownable, Initializable { + using SafeMath for uint256; + + mapping(bytes32 => address) public registry; + + event RegistryUpdated(string identifier, bytes32 indexed identifierHash, address indexed addr); + + /** + * @notice Sets initialized == true on implementation contracts + * @param test Set to true to skip implementation initialization + */ + constructor(bool test) Initializable(test) {} + + /** + * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. + */ + function initialize() external initializer { + _transferOwnership(msg.sender); + } + + /** + * @notice Associates the given address with the given identifier. + * @param identifier Identifier of contract whose address we want to set. + * @param addr Address of contract. + */ + function setAddressFor(string calldata identifier, address addr) external onlyOwner { + bytes32 identifierHash = keccak256(abi.encodePacked(identifier)); + registry[identifierHash] = addr; + emit RegistryUpdated(identifier, identifierHash, addr); + } + + /** + * @notice Gets address associated with the given identifierHash. + * @param identifierHash Identifier hash of contract whose address we want to look up. + * @dev Throws if address not set. + */ + function getAddressForOrDie(bytes32 identifierHash) external view returns (address) { + require(registry[identifierHash] != address(0), "identifier has no registry entry"); + return registry[identifierHash]; + } + + /** + * @notice Gets address associated with the given identifierHash. + * @param identifierHash Identifier hash of contract whose address we want to look up. + */ + function getAddressFor(bytes32 identifierHash) external view returns (address) { + return registry[identifierHash]; + } + + /** + * @notice Gets address associated with the given identifier. + * @param identifier Identifier of contract whose address we want to look up. + * @dev Throws if address not set. + */ + function getAddressForStringOrDie(string calldata identifier) external view returns (address) { + bytes32 identifierHash = keccak256(abi.encodePacked(identifier)); + require(registry[identifierHash] != address(0), "identifier has no registry entry"); + return registry[identifierHash]; + } + + /** + * @notice Gets address associated with the given identifier. + * @param identifier Identifier of contract whose address we want to look up. + */ + function getAddressForString(string calldata identifier) external view returns (address) { + bytes32 identifierHash = keccak256(abi.encodePacked(identifier)); + return registry[identifierHash]; + } + + /** + * @notice Iterates over provided array of identifiers, getting the address for each. + * Returns true if `sender` matches the address of one of the provided identifiers. + * @param identifierHashes Array of hashes of approved identifiers. + * @param sender Address in question to verify membership. + * @return True if `sender` corresponds to the address of any of `identifiers` + * registry entries. + */ + function isOneOf( + bytes32[] calldata identifierHashes, + address sender + ) external view returns (bool) { + for (uint256 i = 0; i < identifierHashes.length; i = i.add(1)) { + if (registry[identifierHashes[i]] == sender) { + return true; + } + } + return false; + } +} diff --git a/packages/protocol/contracts/governance/Validators.sol b/packages/protocol/contracts-0.8/governance/Validators.sol similarity index 86% rename from packages/protocol/contracts/governance/Validators.sol rename to packages/protocol/contracts-0.8/governance/Validators.sol index 4bce9ac487e..7a744edf9ee 100644 --- a/packages/protocol/contracts/governance/Validators.sol +++ b/packages/protocol/contracts-0.8/governance/Validators.sol @@ -1,21 +1,24 @@ -pragma solidity ^0.5.13; +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; -import "openzeppelin-solidity/contracts/math/Math.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import "solidity-bytes-utils/contracts/BytesLib.sol"; +import "@openzeppelin/contracts8/access/Ownable.sol"; +import "@openzeppelin/contracts8/utils/math/Math.sol"; +import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; +import "solidity-bytes-utils-8/contracts/BytesLib.sol"; -import "./interfaces/IValidators.sol"; +import "../../contracts/governance/interfaces/IValidators.sol"; -import "../common/CalledByVm.sol"; -import "../common/Initializable.sol"; -import "../common/FixidityLib.sol"; +import "../../contracts/common/CalledByVm.sol"; +import "../../contracts/common/Initializable.sol"; +import "../../contracts/common/FixidityLib.sol"; import "../common/linkedlists/AddressLinkedList.sol"; import "../common/UsingRegistry.sol"; -import "../common/UsingPrecompiles.sol"; -import "../common/interfaces/ICeloVersionedContract.sol"; -import "../common/libraries/ReentrancyGuard.sol"; -import "../../contracts-0.8/common/IsL2Check.sol"; +import "../common/PrecompilesOverride.sol"; +import "../../contracts/common/interfaces/ICeloVersionedContract.sol"; +import "../../contracts/common/libraries/ReentrancyGuard.sol"; +import "../common/interfaces/IStableToken.sol"; + +import "../../contracts/common/interfaces/IAccounts.sol"; /** * @title A contract for registering and electing Validator Groups and Validators. @@ -27,9 +30,8 @@ contract Validators is ReentrancyGuard, Initializable, UsingRegistry, - UsingPrecompiles, - CalledByVm, - IsL2Check + PrecompilesOverride, + CalledByVm { using FixidityLib for FixidityLib.Fraction; using AddressLinkedList for LinkedList.List; @@ -110,6 +112,12 @@ contract Validators is FixidityLib.Fraction adjustmentSpeed; } + struct InitParams { + // The number of blocks to delay a ValidatorGroup's commission + uint256 commissionUpdateDelay; + uint256 downtimeGracePeriod; + } + mapping(address => ValidatorGroup) private groups; mapping(address => Validator) private validators; address[] private registeredGroups; @@ -164,7 +172,7 @@ contract Validators is * @notice Sets initialized == true on implementation contracts * @param test Set to true to skip implementation initialization */ - constructor(bool test) public Initializable(test) {} + constructor(bool test) Initializable(test) {} /** * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. @@ -177,7 +185,6 @@ contract Validators is * @param validatorScoreAdjustmentSpeed The speed at which validator scores are adjusted. * @param _membershipHistoryLength The max number of entries for validator membership history. * @param _maxGroupSize The maximum group size. - * @param _commissionUpdateDelay The number of blocks to delay a ValidatorGroup's commission * update. * @dev Should be called only once. */ @@ -192,8 +199,7 @@ contract Validators is uint256 _membershipHistoryLength, uint256 _slashingMultiplierResetPeriod, uint256 _maxGroupSize, - uint256 _commissionUpdateDelay, - uint256 _downtimeGracePeriod + InitParams calldata initParams ) external initializer { _transferOwnership(msg.sender); setRegistry(registryAddress); @@ -201,20 +207,21 @@ contract Validators is setValidatorLockedGoldRequirements(validatorRequirementValue, validatorRequirementDuration); setValidatorScoreParameters(validatorScoreExponent, validatorScoreAdjustmentSpeed); setMaxGroupSize(_maxGroupSize); - setCommissionUpdateDelay(_commissionUpdateDelay); + setCommissionUpdateDelay(initParams.commissionUpdateDelay); setMembershipHistoryLength(_membershipHistoryLength); setSlashingMultiplierResetPeriod(_slashingMultiplierResetPeriod); - setDowntimeGracePeriod(_downtimeGracePeriod); + setDowntimeGracePeriod(initParams.downtimeGracePeriod); } /** * @notice Updates a validator's score based on its uptime for the epoch. * @param signer The validator signer of the validator account whose score needs updating. * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. - * @return True upon success. */ - function updateValidatorScoreFromSigner(address signer, uint256 uptime) external onlyVm { - allowOnlyL1(); + function updateValidatorScoreFromSigner( + address signer, + uint256 uptime + ) external virtual onlyVm onlyL1 { _updateValidatorScoreFromSigner(signer, uptime); } @@ -223,13 +230,12 @@ contract Validators is * @param signer The validator signer of the account to distribute the epoch payment to. * @param maxPayment The maximum payment to the validator. Actual payment is based on score and * group commission. - * @return The total payment paid to the validator and their group. + * @return distributeEpochPaymentsFromSigner The total payment paid to the validator and their group. */ function distributeEpochPaymentsFromSigner( address signer, uint256 maxPayment - ) external onlyVm returns (uint256) { - allowOnlyL1(); + ) external virtual onlyVm onlyL1 returns (uint256) { return _distributeEpochPaymentsFromSigner(signer, maxPayment); } @@ -244,13 +250,13 @@ contract Validators is * @return True upon success. * @dev Fails if the account is already a validator or validator group. * @dev Fails if the account does not have sufficient Locked Gold. + * @dev Fails on L2. Use registerValidatorNoBls instead. */ function registerValidator( bytes calldata ecdsaPublicKey, bytes calldata blsPublicKey, bytes calldata blsPop - ) external nonReentrant returns (bool) { - allowOnlyL1(); + ) external nonReentrant onlyL1 returns (bool) { address account = getAccounts().validatorSignerToAccount(msg.sender); _isRegistrationAllowed(account); require(!isValidator(account) && !isValidatorGroup(account), "Already registered"); @@ -273,6 +279,34 @@ contract Validators is return true; } + /** + * @notice Registers a validator unaffiliated with any validator group. + * @param ecdsaPublicKey The ECDSA public key that the validator is using for consensus, should + * match the validator signer. 64 bytes. + * @return True upon success. + * @dev Fails if the account is already a validator or validator group. + * @dev Fails if the account does not have sufficient Locked Gold. + */ + function registerValidatorNoBls( + bytes calldata ecdsaPublicKey + ) external nonReentrant onlyL2 returns (bool) { + address account = getAccounts().validatorSignerToAccount(msg.sender); + _isRegistrationAllowed(account); + require(!isValidator(account) && !isValidatorGroup(account), "Already registered"); + uint256 lockedGoldBalance = getLockedGold().getAccountTotalLockedGold(account); + require(lockedGoldBalance >= validatorLockedGoldRequirements.value, "Deposit too small"); + Validator storage validator = validators[account]; + address signer = getAccounts().getValidatorSigner(account); + require( + _updateEcdsaPublicKey(validator, account, signer, ecdsaPublicKey), + "Error updating ECDSA public key" + ); + registeredValidators.push(account); + updateMembershipHistory(account, address(0)); + emit ValidatorRegistered(account); + return true; + } + /** * @notice De-registers a validator. * @param index The index of this validator in the list of all registered validators. @@ -296,7 +330,7 @@ contract Validators is uint256 requirementEndTime = validator.membershipHistory.lastRemovedFromGroupTimestamp.add( validatorLockedGoldRequirements.duration ); - require(requirementEndTime < now, "Not yet requirement end time"); + require(requirementEndTime < block.timestamp, "Not yet requirement end time"); // Remove the validator. deleteElement(registeredValidators, account, index); @@ -312,7 +346,6 @@ contract Validators is * @dev De-affiliates with the previously affiliated group if present. */ function affiliate(address group) external nonReentrant returns (bool) { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidator(account), "Not a validator"); require(isValidatorGroup(group), "Not a validator group"); @@ -352,8 +385,7 @@ contract Validators is function updateBlsPublicKey( bytes calldata blsPublicKey, bytes calldata blsPop - ) external returns (bool) { - allowOnlyL1(); + ) external onlyL1 returns (bool) { address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidator(account), "Not a validator"); Validator storage validator = validators[account]; @@ -376,7 +408,6 @@ contract Validators is address signer, bytes calldata ecdsaPublicKey ) external onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) returns (bool) { - allowOnlyL1(); require(isValidator(account), "Not a validator"); Validator storage validator = validators[account]; require( @@ -402,7 +433,7 @@ contract Validators is uint256[] storage sizeHistory = groups[account].sizeHistory; if (sizeHistory.length > 1) { require( - sizeHistory[1].add(groupLockedGoldRequirements.duration) < now, + sizeHistory[1].add(groupLockedGoldRequirements.duration) < block.timestamp, "Hasn't been empty for long enough" ); } @@ -429,8 +460,7 @@ contract Validators is bytes calldata ecdsaPublicKey, bytes calldata blsPublicKey, bytes calldata blsPop - ) external onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) returns (bool) { - allowOnlyL1(); + ) external onlyL1 onlyRegisteredContract(ACCOUNTS_REGISTRY_ID) returns (bool) { require(isValidator(account), "Not a validator"); Validator storage validator = validators[account]; require( @@ -453,7 +483,6 @@ contract Validators is * @dev Fails if the account does not have sufficient weight. */ function registerValidatorGroup(uint256 commission) external nonReentrant returns (bool) { - allowOnlyL1(); require(commission <= FixidityLib.fixed1().unwrap(), "Commission can't be greater than 100%"); address account = getAccounts().validatorSignerToAccount(msg.sender); _isRegistrationAllowed(account); @@ -478,14 +507,13 @@ contract Validators is * @dev Fails if the group has zero members. */ function addMember(address validator) external nonReentrant returns (bool) { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(groups[account].members.numElements > 0, "Validator group empty"); return _addMember(account, validator, address(0), address(0)); } /** - * @notice Adds the first member to a group's list of members and marks it eligible for election. + * @notice Adds the first member to a group's list of members and marks the group eligible for election. * @param validator The validator to add to the group * @param lesser The address of the group that has received fewer votes than this group. * @param greater The address of the group that has received more votes than this group. @@ -498,7 +526,6 @@ contract Validators is address lesser, address greater ) external nonReentrant returns (bool) { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(groups[account].members.numElements == 0, "Validator group not empty"); return _addMember(account, validator, lesser, greater); @@ -531,7 +558,6 @@ contract Validators is address lesserMember, address greaterMember ) external nonReentrant returns (bool) { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidatorGroup(account), "Not a group"); require(isValidator(validator), "Not a validator"); @@ -549,7 +575,6 @@ contract Validators is * payments made to its members. Must be in the range [0, 1.0]. */ function setNextCommissionUpdate(uint256 commission) external { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidatorGroup(account), "Not a validator group"); ValidatorGroup storage group = groups[account]; @@ -560,15 +585,17 @@ contract Validators is group.nextCommissionBlock = block.number.add(commissionUpdateDelay); emit ValidatorGroupCommissionUpdateQueued(account, commission, group.nextCommissionBlock); } + /** * @notice Updates a validator group's commission based on the previously queued update */ function updateCommission() external { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidatorGroup(account), "Not a validator group"); ValidatorGroup storage group = groups[account]; + _sendValidatorGroupPaymentsIfNecessary(group); + require(group.nextCommissionBlock != 0, "No commission update queued"); require(group.nextCommissionBlock <= block.number, "Can't apply commission update yet"); @@ -583,7 +610,6 @@ contract Validators is * @param validatorAccount The validator to deaffiliate from their affiliated validator group. */ function forceDeaffiliateIfValidator(address validatorAccount) external nonReentrant onlySlasher { - allowOnlyL1(); if (isValidator(validatorAccount)) { Validator storage validator = validators[validatorAccount]; if (validator.affiliation != address(0)) { @@ -597,12 +623,11 @@ contract Validators is * the last time the group was slashed. */ function resetSlashingMultiplier() external nonReentrant { - allowOnlyL1(); address account = getAccounts().validatorSignerToAccount(msg.sender); require(isValidatorGroup(account), "Not a validator group"); ValidatorGroup storage group = groups[account]; require( - now >= group.slashInfo.lastSlashed.add(slashingMultiplierResetPeriod), + block.timestamp >= group.slashInfo.lastSlashed.add(slashingMultiplierResetPeriod), "`resetSlashingMultiplier` called before resetPeriod expired" ); group.slashInfo.multiplier = FixidityLib.fixed1(); @@ -613,17 +638,31 @@ contract Validators is * @param account The group being slashed. */ function halveSlashingMultiplier(address account) external nonReentrant onlySlasher { - allowOnlyL1(); require(isValidatorGroup(account), "Not a validator group"); ValidatorGroup storage group = groups[account]; group.slashInfo.multiplier = FixidityLib.wrap(group.slashInfo.multiplier.unwrap().div(2)); - group.slashInfo.lastSlashed = now; + group.slashInfo.lastSlashed = block.timestamp; + } + + // TODO: Move this function's logic to `EpochManager` once Mento updates stable token + // to allow `EpochManager` to mint. + /** + * @notice Allows the EpochManager contract to mint stable token for itself. + * @param amount The amount of stableToken to be minted. + */ + function mintStableToEpochManager( + uint256 amount + ) external onlyL2 nonReentrant onlyRegisteredContract(EPOCH_MANAGER_REGISTRY_ID) { + require( + IStableToken(getStableToken()).mint(msg.sender, amount), + "mint failed to epoch manager" + ); } /** * @notice Returns the validator BLS key. * @param signer The account that registered the validator or its authorized signing address. - * @return The validator BLS key. + * @return blsPublicKey The validator BLS key. */ function getValidatorBlsPublicKeyFromSigner( address signer @@ -633,6 +672,10 @@ contract Validators is return validators[account].publicKeys.bls; } + function getMembershipHistoryLength() external view returns (uint256) { + return membershipHistoryLength; + } + /** * @notice Returns validator group information. * @param account The account that registered the validator group. @@ -668,7 +711,8 @@ contract Validators is * @notice Returns the top n group members for a particular group. * @param account The address of the validator group. * @param n The number of members to return. - * @return The top n group members for a particular group. + * @return The signers of the top n group members for a particular group. + * @dev Returns the account instead of signer on L2. */ function getTopGroupValidators( address account, @@ -676,12 +720,29 @@ contract Validators is ) external view returns (address[] memory) { address[] memory topAccounts = groups[account].members.headN(n); address[] memory topValidators = new address[](n); + + IAccounts accounts = getAccounts(); + for (uint256 i = 0; i < n; i = i.add(1)) { - topValidators[i] = getAccounts().getValidatorSigner(topAccounts[i]); + topValidators[i] = accounts.getValidatorSigner(topAccounts[i]); } return topValidators; } + /** + * @notice Retreives the top validator accounts of the specified group. + * @param account The address of the validator group. + * @param n The number of members to return. + * @return The accounts of the top n group members for a particular group. + */ + function getTopGroupValidatorsAccounts( + address account, + uint256 n + ) external view returns (address[] memory) { + address[] memory topAccounts = groups[account].members.headN(n); + return topAccounts; + } + /** * @notice Returns the number of members in the provided validator groups. * @param accounts The addresses of the validator groups. @@ -723,6 +784,19 @@ contract Validators is return (groupLockedGoldRequirements.value, groupLockedGoldRequirements.duration); } + /** + * @notice Returns the list of signers for the registered validator accounts. + * @return The list of signers for registered validator accounts. + */ + function getRegisteredValidatorSigners() external view returns (address[] memory) { + IAccounts accounts = getAccounts(); + address[] memory signers = new address[](registeredValidators.length); + for (uint256 i = 0; i < signers.length; i = i.add(1)) { + signers[i] = accounts.getValidatorSigner(registeredValidators[i]); + } + return signers; + } + /** * @notice Returns the list of registered validator accounts. * @return The list of registered validator accounts. @@ -755,7 +829,6 @@ contract Validators is * @param account The group to fetch slashing multiplier for. */ function getValidatorGroupSlashingMultiplier(address account) external view returns (uint256) { - allowOnlyL1(); require(isValidatorGroup(account), "Not a validator group"); ValidatorGroup storage group = groups[account]; return group.slashInfo.multiplier.unwrap(); @@ -773,7 +846,6 @@ contract Validators is uint256 epochNumber, uint256 index ) external view returns (address) { - allowOnlyL1(); require(isValidator(account), "Not a validator"); require(epochNumber <= getEpochNumber(), "Epoch cannot be larger than current"); MembershipHistory storage history = validators[account].membershipHistory; @@ -837,6 +909,14 @@ contract Validators is return sum.divide(FixidityLib.newFixed(uptimes.length)).unwrap(); } + /** + * @notice Returns the maximum number of members a group can add. + * @return The maximum number of members a group can add. + */ + function getMaxGroupSize() external view returns (uint256) { + return maxGroupSize; + } + /** * @notice Returns the block delay for a ValidatorGroup's commission udpdate. * @return The block delay for a ValidatorGroup's commission udpdate. @@ -845,6 +925,39 @@ contract Validators is return commissionUpdateDelay; } + /** + * @notice Computes epoch payments to the account + * @param account The validator account of the validator to distribute the epoch payment to. + * @param maxPayment The maximum payment to the validator. Actual payment is based on score and + * group commission. + * @return The total payment paid to the validator and their group. + */ + function computeEpochReward( + address account, + uint256 score, + uint256 maxPayment + ) external view virtual returns (uint256) { + require(isValidator(account), "Not a validator"); + FixidityLib.Fraction memory scoreFraction = FixidityLib.wrap(score); + require(scoreFraction.lte(FixidityLib.fixed1()), "Score must be <= 1"); + + // The group that should be paid is the group that the validator was a member of at the + // time it was elected. + address group = getMembershipInLastEpoch(account); + require(group != address(0), "Validator not registered with a group"); + // Both the validator and the group must maintain the minimum locked gold balance in order to + // receive epoch payments. + if (meetsAccountLockedGoldRequirements(account) && meetsAccountLockedGoldRequirements(group)) { + FixidityLib.Fraction memory totalPayment = FixidityLib + .newFixed(maxPayment) + .multiply(scoreFraction) + .multiply(groups[group].slashInfo.multiplier); + return totalPayment.fromFixed(); + } else { + return 0; + } + } + /** * @notice Returns the storage, major, minor, and patch version of the contract. * @return Storage version of the contract. @@ -861,7 +974,6 @@ contract Validators is * @param delay Number of blocks to delay the update */ function setCommissionUpdateDelay(uint256 delay) public onlyOwner { - allowOnlyL1(); require(delay != commissionUpdateDelay, "commission update delay not changed"); commissionUpdateDelay = delay; emit CommissionUpdateDelaySet(delay); @@ -873,7 +985,6 @@ contract Validators is * @return True upon success. */ function setMaxGroupSize(uint256 size) public onlyOwner returns (bool) { - allowOnlyL1(); require(0 < size, "Max group size cannot be zero"); require(size != maxGroupSize, "Max group size not changed"); maxGroupSize = size; @@ -887,7 +998,6 @@ contract Validators is * @return True upon success. */ function setMembershipHistoryLength(uint256 length) public onlyOwner returns (bool) { - allowOnlyL1(); require(0 < length, "Membership history length cannot be zero"); require(length != membershipHistoryLength, "Membership history length not changed"); membershipHistoryLength = length; @@ -904,8 +1014,7 @@ contract Validators is function setValidatorScoreParameters( uint256 exponent, uint256 adjustmentSpeed - ) public onlyOwner returns (bool) { - allowOnlyL1(); + ) public onlyOwner onlyL1 returns (bool) { require( adjustmentSpeed <= FixidityLib.fixed1().unwrap(), "Adjustment speed cannot be larger than 1" @@ -968,7 +1077,6 @@ contract Validators is * @param value New reset period for slashing multiplier. */ function setSlashingMultiplierResetPeriod(uint256 value) public nonReentrant onlyOwner { - allowOnlyL1(); slashingMultiplierResetPeriod = value; } @@ -976,8 +1084,7 @@ contract Validators is * @notice Sets the downtimeGracePeriod property if called by owner. * @param value New downtime grace period for calculating epoch scores. */ - function setDowntimeGracePeriod(uint256 value) public nonReentrant onlyOwner { - allowOnlyL1(); + function setDowntimeGracePeriod(uint256 value) public nonReentrant onlyOwner onlyL1 { downtimeGracePeriod = value; } @@ -994,7 +1101,7 @@ contract Validators is uint256[] storage sizeHistory = groups[account].sizeHistory; if (sizeHistory.length > 0) { for (uint256 i = sizeHistory.length.sub(1); i > 0; i = i.sub(1)) { - if (sizeHistory[i].add(groupLockedGoldRequirements.duration) >= now) { + if (sizeHistory[i].add(groupLockedGoldRequirements.duration) >= block.timestamp) { multiplier = Math.max(i, multiplier); break; } @@ -1011,8 +1118,8 @@ contract Validators is * @return The group that `account` was a member of at the end of the last epoch. */ function getMembershipInLastEpoch(address account) public view returns (address) { - allowOnlyL1(); uint256 epochNumber = getEpochNumber(); + MembershipHistory storage history = validators[account].membershipHistory; uint256 head = history.numEntries == 0 ? 0 : history.tail.add(history.numEntries.sub(1)); // If the most recent entry in the membership history is for the current epoch number, we need @@ -1031,7 +1138,7 @@ contract Validators is * @dev epoch_score = uptime ** exponent * @return Fixidity representation of the epoch score between 0 and 1. */ - function calculateEpochScore(uint256 uptime) public view returns (uint256) { + function calculateEpochScore(uint256 uptime) public view onlyL1 returns (uint256) { require(uptime <= FixidityLib.fixed1().unwrap(), "Uptime cannot be larger than one"); uint256 numerator; uint256 denominator; @@ -1062,7 +1169,11 @@ contract Validators is /** * @notice Returns validator information. * @param account The account that registered the validator. - * @return The unpacked validator struct. + * @return ecdsaPublicKey The ECDSA public key. + * @return blsPublicKey The BLS public key. + * @return affiliation The address of the validator group the validator is a member of. + * @return score The validator's score. + * @return signer The address of the validator's signer. */ function getValidator( address account @@ -1088,6 +1199,17 @@ contract Validators is ); } + /** + * @notice Returns affiliated group to validator. + * @param account The account that registered the validator. + * @return group The validator group. + */ + function getValidatorsGroup(address account) public view returns (address group) { + require(isValidator(account), "Not a validator"); + Validator storage validator = validators[account]; + return validator.affiliation; + } + /** * @notice Returns the number of members in a validator group. * @param account The address of the validator group. @@ -1113,7 +1235,7 @@ contract Validators is * @return Whether a particular address is a registered validator. */ function isValidator(address account) public view returns (bool) { - return validators[account].publicKeys.bls.length > 0; + return validators[account].publicKeys.ecdsa.length > 0; } /** @@ -1147,7 +1269,7 @@ contract Validators is (address beneficiary, uint256 fraction) = getAccounts().getPaymentDelegation(account); uint256 delegatedPayment = remainingPayment.multiply(FixidityLib.wrap(fraction)).fromFixed(); uint256 validatorPayment = remainingPayment.fromFixed().sub(delegatedPayment); - IStableToken stableToken = getStableToken(); + IStableToken stableToken = IStableToken(getStableToken()); require(stableToken.mint(group, groupPayment), "mint failed to validator group"); require(stableToken.mint(account, validatorPayment), "mint failed to validator account"); if (fraction != 0) { @@ -1165,7 +1287,6 @@ contract Validators is * @param signer The validator signer of the validator whose score needs updating. * @param uptime The Fixidity representation of the validator's uptime, between 0 and 1. * @dev new_score = uptime ** exponent * adjustmentSpeed + old_score * (1 - adjustmentSpeed) - * @return True upon success. */ function _updateValidatorScoreFromSigner(address signer, uint256 uptime) internal { address account = getAccounts().signerToAccount(signer); @@ -1186,7 +1307,7 @@ contract Validators is emit ValidatorScoreUpdated(account, validators[account].score.unwrap(), epochScore.unwrap()); } - function _isRegistrationAllowed(address account) private returns (bool) { + function _isRegistrationAllowed(address account) private { require( !getElection().allowedToVoteOverMaxNumberOfGroups(account), "Cannot vote for more than max number of groups" @@ -1292,7 +1413,7 @@ contract Validators is uint256 lastIndex = list.length.sub(1); list[index] = list[lastIndex]; delete list[lastIndex]; - list.length = lastIndex; + list.pop(); } /** @@ -1330,10 +1451,11 @@ contract Validators is function updateMembershipHistory(address account, address group) private returns (bool) { MembershipHistory storage history = validators[account].membershipHistory; uint256 epochNumber = getEpochNumber(); + uint256 head = history.numEntries == 0 ? 0 : history.tail.add(history.numEntries.sub(1)); if (history.numEntries > 0 && group == address(0)) { - history.lastRemovedFromGroupTimestamp = now; + history.lastRemovedFromGroupTimestamp = block.timestamp; } if (history.numEntries > 0 && history.entries[head].epochNumber == epochNumber) { @@ -1372,9 +1494,9 @@ contract Validators is function updateSizeHistory(address group, uint256 size) private { uint256[] storage sizeHistory = groups[group].sizeHistory; if (size == sizeHistory.length) { - sizeHistory.push(now); + sizeHistory.push(block.timestamp); } else if (size < sizeHistory.length) { - sizeHistory[size] = now; + sizeHistory[size] = block.timestamp; } else { require(false, "Unable to update size history"); } @@ -1390,6 +1512,7 @@ contract Validators is Validator storage validator, address validatorAccount ) private returns (bool) { + _sendValidatorPaymentIfNecessary(validatorAccount); address affiliation = validator.affiliation; ValidatorGroup storage group = groups[affiliation]; if (group.members.contains(validatorAccount)) { @@ -1399,4 +1522,17 @@ contract Validators is emit ValidatorDeaffiliated(validatorAccount, affiliation); return true; } + + function _sendValidatorPaymentIfNecessary(address validator) private { + if (isL2()) { + getEpochManager().sendValidatorPayment(validator); + } + } + + function _sendValidatorGroupPaymentsIfNecessary(ValidatorGroup storage group) private { + address[] memory members = group.members.getKeys(); + for (uint256 i = 0; i < members.length; i++) { + _sendValidatorPaymentIfNecessary(members[i]); + } + } } diff --git a/packages/protocol/contracts-0.8/governance/test/EpochRewardsMock.sol b/packages/protocol/contracts-0.8/governance/test/EpochRewardsMock.sol new file mode 100644 index 00000000000..df4e8e592a8 --- /dev/null +++ b/packages/protocol/contracts-0.8/governance/test/EpochRewardsMock.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "../../../contracts/governance/interfaces/IEpochRewards.sol"; + +/** + * @title A wrapper around EpochRewards that exposes internal functions for testing. + */ +contract EpochRewardsMock08 is IEpochRewards { + uint256 private numValidatorsInCurrentSet; + address public carbonOffsettingPartner; + + uint256 public perValidatorReward = 5; + uint256 public totalRewardsVoter = 6; + uint256 public totalRewardsCommunity = 7; + uint256 public totalRewardsCarbonFund = 8; + + function setNumberValidatorsInCurrentSet(uint256 value) external { + numValidatorsInCurrentSet = value; + } + + function updateTargetVotingYield() external {} + + function getRewardsMultiplier(uint256) external pure returns (uint256) { + return 0; + } + + function isReserveLow() external pure returns (bool) { + return false; + } + function calculateTargetEpochRewards() + external + view + returns (uint256, uint256, uint256, uint256) + { + return (perValidatorReward, totalRewardsVoter, totalRewardsCommunity, totalRewardsCarbonFund); + } + function getTargetVotingYieldParameters() external pure returns (uint256, uint256, uint256) { + return (0, 0, 0); + } + function getRewardsMultiplierParameters() external pure returns (uint256, uint256, uint256) { + return (0, 0, 0); + } + function getCommunityRewardFraction() external pure returns (uint256) { + return 0; + } + function getCarbonOffsettingFraction() external pure returns (uint256) { + return 0; + } + function getTargetVotingGoldFraction() external pure returns (uint256) { + return 0; + } + function getRewardsMultiplier() external pure returns (uint256) { + return 0; + } + + // mocks the precompile + function numberValidatorsInCurrentSet() public view returns (uint256) { + return numValidatorsInCurrentSet; + } + + function setCarbonOffsettingPartner(address partner) external { + carbonOffsettingPartner = partner; + } +} diff --git a/packages/protocol/contracts-0.8/governance/test/IMockValidators.sol b/packages/protocol/contracts-0.8/governance/test/IMockValidators.sol new file mode 100644 index 00000000000..9dd67b7857f --- /dev/null +++ b/packages/protocol/contracts-0.8/governance/test/IMockValidators.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.7 <0.8.20; + +interface IMockValidators { + function isValidator(address) external returns (bool); + function isValidatorGroup(address) external returns (bool); + + function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool); + + function updatePublicKeys( + address, + address, + bytes calldata, + bytes calldata, + bytes calldata + ) external returns (bool); + + function setValidator(address) external; + + function setValidatorGroup(address group) external; + + function affiliate(address group) external returns (bool); + + function setDoesNotMeetAccountLockedGoldRequirements(address account) external; + + function setNumRegisteredValidators(uint256 value) external; + + function setMembers(address group, address[] calldata _members) external; + + function setCommission(address group, uint256 commission) external; + + function setAccountLockedGoldRequirement(address account, uint256 value) external; + + function halveSlashingMultiplier(address) external; + + function forceDeaffiliateIfValidator(address validator) external; + + function getTopGroupValidators(address group, uint256 n) external view returns (address[] memory); + + function getValidatorGroup( + address + ) + external + view + returns (address[] memory, uint256, uint256, uint256, uint256[] memory, uint256, uint256); + + function getValidatorGroupSlashingMultiplier(address) external view returns (uint256); + + function meetsAccountLockedGoldRequirements(address account) external view returns (bool); + + function getNumRegisteredValidators() external view returns (uint256); + + function getAccountLockedGoldRequirement(address account) external view returns (uint256); + + function calculateGroupEpochScore(uint256[] calldata uptimes) external view returns (uint256); + + function getGroupsNumMembers(address[] calldata groups) external view returns (uint256[] memory); + + function groupMembershipInEpoch(address addr, uint256, uint256) external view returns (address); + + function getGroupNumMembers(address group) external view returns (uint256); + + function setEpochRewards(address account, uint256 reward) external; + + function mintedStable() external view returns (uint256); +} diff --git a/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol b/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol index 642d1603579..4db6fbc5d66 100644 --- a/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol +++ b/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol @@ -31,7 +31,7 @@ contract FeeCurrencyAdapter is Initializable, CalledByVm, IFeeCurrencyAdapter { * @notice Sets initialized == true on implementation contracts * @param test Set to true to skip implementation initialization */ - constructor(bool test) public Initializable(test) {} + constructor(bool test) Initializable(test) {} /** * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. diff --git a/packages/protocol/contracts-0.8/stability/interfaces/IDecimals.sol b/packages/protocol/contracts-0.8/stability/interfaces/IDecimals.sol index 2b68b9f8060..27d86e9740a 100644 --- a/packages/protocol/contracts-0.8/stability/interfaces/IDecimals.sol +++ b/packages/protocol/contracts-0.8/stability/interfaces/IDecimals.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.13; interface IDecimals { diff --git a/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrency.sol b/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrency.sol index e8c800a3c93..7dac6e50932 100644 --- a/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrency.sol +++ b/packages/protocol/contracts-0.8/stability/interfaces/IFeeCurrency.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.8.13; import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; diff --git a/packages/protocol/contracts-0.8/stability/test/MockReserve.sol b/packages/protocol/contracts-0.8/stability/test/MockReserve.sol new file mode 100644 index 00000000000..09b64ab4385 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/test/MockReserve.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +// solhint-disable no-unused-vars + +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; + +/** + * @title A mock Reserve for testing. + */ +contract MockReserve08 { + mapping(address => bool) public tokens; + + IERC20 public goldToken; + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + function setGoldToken(address goldTokenAddress) external { + goldToken = IERC20(goldTokenAddress); + } + + function transferGold(address to, uint256 value) external returns (bool) { + require(goldToken.transfer(to, value), "gold token transfer failed"); + return true; + } + + function transferExchangeGold(address to, uint256 value) external returns (bool) { + require(goldToken.transfer(to, value), "gold token transfer failed"); + return true; + } + + function addToken(address token) external returns (bool) { + tokens[token] = true; + return true; + } + + function getUnfrozenReserveGoldBalance() external view returns (uint256) { + return address(this).balance; + } + + function burnToken(address) external pure returns (bool) { + return true; + } + + function getReserveGoldBalance() public view returns (uint256) { + return address(this).balance; + } +} diff --git a/packages/protocol/contracts-0.8/stability/test/MockStableToken.sol b/packages/protocol/contracts-0.8/stability/test/MockStableToken.sol new file mode 100644 index 00000000000..579abfeec43 --- /dev/null +++ b/packages/protocol/contracts-0.8/stability/test/MockStableToken.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0 <0.9.0; +// solhint-disable no-unused-vars + +import "@openzeppelin/contracts8/utils/math/SafeMath.sol"; + +import "../../../contracts/common/FixidityLib.sol"; + +/** + * @title A mock StableToken for testing. + */ +contract MockStableToken08 { + using FixidityLib for FixidityLib.Fraction; + using SafeMath for uint256; + + uint8 public constant decimals = 18; + uint256 public _totalSupply; + FixidityLib.Fraction public inflationFactor; + + // Stored as units. Value can be found using unitsToValue(). + mapping(address => uint256) public balances; + + constructor() { + setInflationFactor(FixidityLib.fixed1().unwrap()); + } + + function setTotalSupply(uint256 value) external { + _totalSupply = value; + } + + function mint(address to, uint256 value) external returns (bool) { + require(to != address(0), "0 is a reserved address"); + balances[to] = balances[to].add(valueToUnits(value)); + _totalSupply = _totalSupply.add(value); + return true; + } + + function burn(uint256 value) external returns (bool) { + balances[msg.sender] = balances[msg.sender].sub(valueToUnits(value)); + _totalSupply = _totalSupply.sub(value); + return true; + } + + function transfer(address to, uint256 value) external returns (bool) { + return _transfer(msg.sender, to, value); + } + + function transferFrom(address from, address to, uint256 value) external returns (bool) { + return _transfer(from, to, value); + } + + function totalSupply() external view returns (uint256) { + return _totalSupply; + } + + function setInflationFactor(uint256 newInflationFactor) public { + inflationFactor = FixidityLib.wrap(newInflationFactor); + } + + function balanceOf(address account) public view returns (uint256) { + return unitsToValue(balances[account]); + } + + function unitsToValue(uint256 units) public view returns (uint256) { + return FixidityLib.newFixed(units).divide(inflationFactor).fromFixed(); + } + + function valueToUnits(uint256 value) public view returns (uint256) { + return inflationFactor.multiply(FixidityLib.newFixed(value)).fromFixed(); + } + + function _transfer(address from, address to, uint256 value) internal returns (bool) { + uint256 balanceValue = balanceOf(from); + if (balanceValue < value) { + return false; + } + uint256 units = valueToUnits(value); + balances[from] = balances[from].sub(units); + balances[to] = balances[to].add(units); + return true; + } +} diff --git a/packages/protocol/contracts/CompileExchange.sol b/packages/protocol/contracts/CompileExchange.sol index 27c4d2cef80..fbcb78aa10d 100644 --- a/packages/protocol/contracts/CompileExchange.sol +++ b/packages/protocol/contracts/CompileExchange.sol @@ -312,7 +312,7 @@ contract CompileExchange is goldBucket = goldBucket.add(sellAmount); stableBucket = stableBucket.sub(buyAmount); require( - getGoldToken().transferFrom(msg.sender, address(reserve), sellAmount), + getCeloToken().transferFrom(msg.sender, address(reserve), sellAmount), "Transfer of sell token failed" ); require(IStableToken(stable).mint(msg.sender, buyAmount), "Mint of stable token failed"); diff --git a/packages/protocol/contracts/SolidityPrecompiles.sol b/packages/protocol/contracts/SolidityPrecompiles.sol deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/protocol/contracts/common/Accounts.sol b/packages/protocol/contracts/common/Accounts.sol index af5a6885dcf..edd7a47624d 100644 --- a/packages/protocol/contracts/common/Accounts.sol +++ b/packages/protocol/contracts/common/Accounts.sol @@ -132,6 +132,9 @@ contract Accounts is _setEip712DomainSeparator(); } + /** + * @notice Sets the EIP712 domain separator for the Celo Accounts abstraction. + */ function setEip712DomainSeparator() external { _setEip712DomainSeparator(); } @@ -495,7 +498,7 @@ contract Accounts is * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 5, 0); + return (1, 1, 4, 2); } /** @@ -567,7 +570,7 @@ contract Accounts is * be greater than 1. * @dev Use `deletePaymentDelegation` to unset the payment delegation. */ - function setPaymentDelegation(address beneficiary, uint256 fraction) public onlyL1 { + function setPaymentDelegation(address beneficiary, uint256 fraction) public { require(isAccount(msg.sender), "Must first register address with Account.createAccount"); require(beneficiary != address(0), "Beneficiary cannot be address 0x0"); FixidityLib.Fraction memory f = FixidityLib.wrap(fraction); diff --git a/packages/protocol/contracts/common/Blockable.sol b/packages/protocol/contracts/common/Blockable.sol new file mode 100644 index 00000000000..90b0617ea1a --- /dev/null +++ b/packages/protocol/contracts/common/Blockable.sol @@ -0,0 +1,69 @@ +pragma solidity >=0.5.13 <0.9.0; + +import "./interfaces/IBlockable.sol"; +import "./interfaces/IBlocker.sol"; + +/** + * @title Blockable Contract + * @notice This contract allows certain actions to be blocked based on the logic of another contract implementing the IBlocker interface. + * @dev This contract uses an external IBlocker contract to determine if it is blocked. The owner can set the blocking contract. + **/ +contract Blockable is IBlockable { + // using directly memory slot so contracts can inherit from this contract withtout breaking storage layout + bytes32 private constant BLOCKEDBY_POSITION = + bytes32(uint256(keccak256("blocked_by_position")) - 1); + + event BlockedBySet(address indexed _blockedBy); + + /** + * @notice Modifier to ensure the function is only executed when the contract is not blocked. + * @dev Reverts with an error if the contract is blocked. + */ + modifier onlyWhenNotBlocked() { + require(!_isBlocked(), "Contract is blocked from performing this action"); + _; + } + + /** + * @notice Checks if the contract is currently blocked. + * @return Returns true if the contract is blocked, otherwise false. + * @dev The function returns false if no blocking contract has been set. + */ + function isBlocked() external view returns (bool) { + return _isBlocked(); + } + + /** + * @notice Returns the address of the contract imposing the block. + */ + function getBlockedByContract() public view returns (address blockedBy) { + bytes32 blockedByPosition = BLOCKEDBY_POSITION; + assembly { + blockedBy := sload(blockedByPosition) + } + return blockedBy; + } + + /** + * @notice Sets the address of the contract allowed to impose a block. + * @param _blockedBy The address of the contract that will impose a block. + */ + function _setBlockedBy(address _blockedBy) internal { + bytes32 blockedByPosition = BLOCKEDBY_POSITION; + assembly { + sstore(blockedByPosition, _blockedBy) + } + + emit BlockedBySet(_blockedBy); + } + + /** + * @notice Checks if the contract is currently blocked. + */ + function _isBlocked() internal view returns (bool) { + if (getBlockedByContract() == address(0)) { + return false; + } + return IBlocker(getBlockedByContract()).isBlocked(); + } +} diff --git a/packages/protocol/contracts/common/Create2.sol b/packages/protocol/contracts/common/Create2.sol index 282bba887ca..77d100390bc 100644 --- a/packages/protocol/contracts/common/Create2.sol +++ b/packages/protocol/contracts/common/Create2.sol @@ -1,6 +1,15 @@ pragma solidity ^0.5.13; +/** + * @title Used for deploying contracts using the CREATE2 opcode. + */ library Create2 { + /** + * @notice Deploys a contract with CREATE2. + * @param salt The CREATE2 salt. + * @param initCode The contract init code to use for deployment. + * @return Address of the deployed contract. + */ function deploy(bytes32 salt, bytes memory initCode) internal returns (address) { address deployedAddress; assembly { @@ -12,6 +21,13 @@ library Create2 { return deployedAddress; } + /** + * @notice Computes the CREATE2 address for given inputs. + * @param deployer Address of the deployer. + * @param salt The CREATE2 salt. + * @param initCodeHash Hash of the init code used for deployment. + * @return The address at which such a contract would be deployed. + */ function computeAddress( address deployer, bytes32 salt, diff --git a/packages/protocol/contracts/common/FeeCurrencyWhitelist.sol b/packages/protocol/contracts/common/FeeCurrencyWhitelist.sol index b0dc16ef56d..2f2adb88aaa 100644 --- a/packages/protocol/contracts/common/FeeCurrencyWhitelist.sol +++ b/packages/protocol/contracts/common/FeeCurrencyWhitelist.sol @@ -3,10 +3,9 @@ pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./interfaces/IFeeCurrencyWhitelist.sol"; - import "../common/Initializable.sol"; - import "../common/interfaces/ICeloVersionedContract.sol"; +import "../../contracts-0.8/common/IsL2Check.sol"; /** * @title Holds a whitelist of the ERC20+ tokens that can be used to pay for gas @@ -16,10 +15,11 @@ contract FeeCurrencyWhitelist is IFeeCurrencyWhitelist, Ownable, Initializable, - ICeloVersionedContract + ICeloVersionedContract, + IsL2Check { // Array of all the tokens enabled - address[] public whitelist; + address[] private deprecated_whitelist; event FeeCurrencyWhitelisted(address token); @@ -42,16 +42,28 @@ contract FeeCurrencyWhitelist is * @dev Add a token to the whitelist * @param tokenAddress The address of the token to add. */ - function addToken(address tokenAddress) external onlyOwner { - whitelist.push(tokenAddress); + function addToken(address tokenAddress) external onlyOwner onlyL1 { + deprecated_whitelist.push(tokenAddress); emit FeeCurrencyWhitelisted(tokenAddress); } /** * @return a list of all tokens enabled as gas fee currency. + * @dev Once Celo becomes an L2, use the FeeCurrencyDirectory contract + * instead. + */ + function getWhitelist() external view onlyL1 returns (address[] memory) { + return deprecated_whitelist; + } + + /** + * @notice Gets the whitelist item at the specified index. + * @return Address of a token in the whitelist. + * @dev Once Celo becomes an L2, use the FeeCurrencyDirectory contract + * instead. */ - function getWhitelist() external view returns (address[] memory) { - return whitelist; + function whitelist(uint256 index) external view onlyL1 returns (address) { + return deprecated_whitelist[index]; } /** @@ -62,7 +74,7 @@ contract FeeCurrencyWhitelist is * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 1, 0); + return (1, 1, 2, 0); } /** @@ -71,11 +83,11 @@ contract FeeCurrencyWhitelist is * @param tokenAddress The address of the token to remove. * @param index The index of the token in the whitelist array. */ - function removeToken(address tokenAddress, uint256 index) public onlyOwner { - require(whitelist[index] == tokenAddress, "Index does not match"); - uint256 length = whitelist.length; - whitelist[index] = whitelist[length - 1]; - whitelist.pop(); + function removeToken(address tokenAddress, uint256 index) public onlyOwner onlyL1 { + require(deprecated_whitelist[index] == tokenAddress, "Index does not match"); + uint256 length = deprecated_whitelist.length; + deprecated_whitelist[index] = deprecated_whitelist[length - 1]; + deprecated_whitelist.pop(); emit FeeCurrencyWhitelistRemoved(tokenAddress); } } diff --git a/packages/protocol/contracts/common/FeeHandler.sol b/packages/protocol/contracts/common/FeeHandler.sol index 57f69d1c5bf..29ccb2b8d8a 100644 --- a/packages/protocol/contracts/common/FeeHandler.sol +++ b/packages/protocol/contracts/common/FeeHandler.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; @@ -18,7 +19,6 @@ import "../common/interfaces/IFeeHandlerSeller.sol"; import "./interfaces/IStableTokenMento.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; import "../common/interfaces/ICeloToken.sol"; -import "../stability/interfaces/ISortedOracles.sol"; // Using the minimal required signatures in the interfaces so more contracts could be compatible import "../common/libraries/ReentrancyGuard.sol"; @@ -48,28 +48,44 @@ contract FeeHandler is uint256 toDistribute; // Historical amounts burned by this contract uint256 pastBurn; + uint256 lastLimitDay; + uint256 toBurn; } - uint256 public constant FIXED1_UINT = 1000000000000000000000000; // TODO move to FIX and add check + struct Beneficiary { + FixidityLib.Fraction fraction; + string name; + bool exists; + } + + uint256 public constant FIXED1_UINT = 1000000000000000000000000; // Min units that can be burned uint256 public constant MIN_BURN = 200; // last day the daily limits were updated - uint256 public lastLimitDay; + uint256 private deprecated_lastLimitDay; // deprecated - FixidityLib.Fraction public burnFraction; // 80% + // reason it's inverse it's because it used to be burnFraction and was migrated + // ignoreRenaming_ prefix allows the tooling to ignore the variable renaming + FixidityLib.Fraction private ignoreRenaming_inverseCarbonFraction; // 80% - address public feeBeneficiary; + address public ignoreRenaming_carbonFeeBeneficiary; - uint256 public celoToBeBurned; + uint256 private deprecated_celoToBeBurned; // deprecated // This mapping can not be public because it contains a FixidityLib.Fraction // and that'd be only supported with experimental features in this // compiler version mapping(address => TokenState) private tokenStates; + // Celo not included in this list EnumerableSet.AddressSet private activeTokens; + // does not include carbon fund + FixidityLib.Fraction private totalFractionOfOtherBeneficiaries; + + mapping(address => Beneficiary) private otherBeneficiaries; + EnumerableSet.AddressSet private otherBeneficiariesAddresses; event SoldAndBurnedToken(address token, uint256 value); event DailyLimitSet(address tokenAddress, uint256 newLimit); @@ -80,6 +96,12 @@ contract FeeHandler is event BurnFractionSet(uint256 fraction); event TokenAdded(address tokenAddress, address handlerAddress); event TokenRemoved(address tokenAddress); + event DistributionAmountSet(address tokenAddress, uint256 amount); + event CarbonFractionSet(uint256 fraction); + event BeneficiaryAdded(address beneficiary); + event BeneficiaryNameSet(address beneficiary, string name); + event BeneficiaryFractionSet(address beneficiary, uint256 fraction); + event BeneficiaryRemoved(address beneficiary); /** * @notice Sets initialized == true on implementation contracts. @@ -93,7 +115,7 @@ contract FeeHandler is function initialize( address _registryAddress, address newFeeBeneficiary, - uint256 newBurnFraction, + uint256 newCarbonFraction, address[] calldata tokens, address[] calldata handlers, uint256[] calldata newLimits, @@ -108,8 +130,8 @@ contract FeeHandler is _transferOwnership(msg.sender); setRegistry(_registryAddress); - _setFeeBeneficiary(newFeeBeneficiary); - _setBurnFraction(newBurnFraction); + _setCarbonFeeBeneficiary(newFeeBeneficiary); + _setCarbonFraction(newCarbonFraction); for (uint256 i = 0; i < tokens.length; i++) { _addToken(tokens[i], handlers[i]); @@ -121,20 +143,66 @@ contract FeeHandler is // Without this the contract cant receive Celo as native transfer function() external payable {} - /** - @dev Sets the fee beneficiary address to the specified address. - @param beneficiary The address to set as the fee beneficiary. - */ - function setFeeBeneficiary(address beneficiary) external onlyOwner { - return _setFeeBeneficiary(beneficiary); + function setCarbonFraction(uint256 newFraction) external onlyOwner { + _setCarbonFraction(newFraction); + } + + function setDistributionAndBurnAmounts(address tokenAddress) external { + return _setDistributionAndBurnAmounts(tokenStates[tokenAddress], IERC20(tokenAddress)); + } + + function changeOtherBeneficiaryAllocation( + address beneficiary, + uint256 _newFraction + ) external onlyOwner { + FixidityLib.Fraction memory newFraction = FixidityLib.wrap(_newFraction); + _setBeneficiaryFraction(beneficiary, newFraction); + } + + function addOtherBeneficiary( + address beneficiary, + uint256 _newFraction, + string calldata name + ) external onlyOwner { + require(otherBeneficiaries[beneficiary].exists == false, "Beneficiary already exists"); + FixidityLib.Fraction memory newFraction = FixidityLib.wrap(_newFraction); + + otherBeneficiaries[beneficiary].exists = true; + _setBeneficiaryFraction(beneficiary, newFraction); + _setBeneficiaryName(beneficiary, name); + otherBeneficiariesAddresses.add(beneficiary); + + emit BeneficiaryAdded(beneficiary); + } + + function removeOtherBeneficiary(address beneficiary) external onlyOwner { + require(otherBeneficiaries[beneficiary].exists, "Beneficiary not found"); + totalFractionOfOtherBeneficiaries = totalFractionOfOtherBeneficiaries.subtract( + otherBeneficiaries[beneficiary].fraction + ); + otherBeneficiariesAddresses.remove(beneficiary); + delete otherBeneficiaries[beneficiary]; + emit BeneficiaryRemoved(beneficiary); + } + + function setBeneficiaryFraction( + address beneficiaryAddress, + uint256 _newFraction + ) external onlyOwner { + FixidityLib.Fraction memory newFraction = FixidityLib.wrap(_newFraction); + _setBeneficiaryFraction(beneficiaryAddress, newFraction); + } + + function setBeneficiaryName(address beneficiary, string calldata name) external onlyOwner { + _setBeneficiaryName(beneficiary, name); } /** - @dev Sets the burn fraction to the specified value. - @param fraction The value to set as the burn fraction. + @dev Sets the fee beneficiary address to the specified address. + @param beneficiary The address to set as the fee beneficiary. */ - function setBurnFraction(uint256 fraction) external onlyOwner { - return _setBurnFraction(fraction); + function setCarbonFeeBeneficiary(address beneficiary) external onlyOwner { + return _setCarbonFeeBeneficiary(beneficiary); } /** @@ -188,6 +256,8 @@ contract FeeHandler is @param tokenAddress The address of the token for which to distribute the available tokens. */ function distribute(address tokenAddress) external { + TokenState storage tokenState = tokenStates[tokenAddress]; + _setDistributionAndBurnAmounts(tokenState, IERC20(tokenAddress)); return _distribute(tokenAddress); } @@ -217,22 +287,19 @@ contract FeeHandler is } /** - @dev Distributes the available tokens for all registered tokens to the feeBeneficiary. + @dev Distributes the available tokens for all registered tokens to the feeBeneficiaries. */ function distributeAll() external { return _distributeAll(); } /** - @dev Distributes the available tokens for all registered tokens to the feeBeneficiary. + @dev Handles all the registered tokens. */ function handleAll() external { return _handleAll(); } - /** - @dev Distributes the the token for to the feeBeneficiary. - */ function handle(address tokenAddress) external { return _handle(tokenAddress); } @@ -240,6 +307,8 @@ contract FeeHandler is /** * @notice Allows owner to transfer tokens of this contract. It's meant for governance to trigger use cases not contemplated in this contract. + CAUTION: this function does not update internal accounting, so it may leave the contract + inconsistent. It should be used as last resort. @param token The address of the token to transfer. @param recipient The address of the recipient to transfer the tokens to. @param value The amount of tokens to transfer. @@ -253,6 +322,10 @@ contract FeeHandler is return IERC20(token).transfer(recipient, value); } + function getCeloToBeBurned() external view returns (uint256) { + return getCeloTokenState().toBurn; + } + /** * @param token The address of the token to query. * @return The amount burned for a token. @@ -261,6 +334,10 @@ contract FeeHandler is return tokenStates[token].pastBurn; } + function carbonFeeBeneficiary() external view returns (address) { + return ignoreRenaming_carbonFeeBeneficiary; + } + /** @dev Returns the handler address for the specified token. @param tokenAddress The address of the token for which to return the handler. @@ -276,7 +353,7 @@ contract FeeHandler is @return A boolean representing the active status of the specified token. */ function getTokenActive(address tokenAddress) external view returns (bool) { - return activeTokens.contains(tokenAddress); + return _getTokenActive(tokenAddress); } /** @@ -318,6 +395,34 @@ contract FeeHandler is return tokenStates[tokenAddress].toDistribute; } + function getTokenToBurn(address tokenAddress) external view returns (uint256) { + return tokenStates[tokenAddress].toBurn; + } + + function getCarbonFraction() external view returns (uint256) { + return getCarbonFractionFixidity().unwrap(); + } + + function getBurnFraction() external view returns (uint256) { + return getBurnFractionFixidity().unwrap(); + } + + function getOtherBeneficiariesInfo( + address beneficiary + ) external view returns (uint256, string memory, bool) { + Beneficiary storage otherBeneficiary = otherBeneficiaries[beneficiary]; + require(otherBeneficiary.exists, "Beneficiary not found"); + return (otherBeneficiary.fraction.unwrap(), otherBeneficiary.name, otherBeneficiary.exists); + } + + function getTotalFractionOfOtherBeneficiariesAndCarbon() external view returns (uint256) { + return getTotalFractionOfOtherBeneficiariesAndCarbonFixidity().unwrap(); + } + + function getOtherBeneficiariesAddresses() external view returns (address[] memory) { + return otherBeneficiariesAddresses.values; + } + /** * @notice Returns the storage, major, minor, and patch version of the contract. * @return Storage version of the contract. @@ -326,7 +431,7 @@ contract FeeHandler is * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 0, 0); + return (1, 2, 0, 0); } /** @@ -344,8 +449,8 @@ contract FeeHandler is uint256 currentDay = now / 1 days; // Pattern borrowed from Reserve.sol - if (currentDay > lastLimitDay) { - lastLimitDay = currentDay; + if (currentDay > tokenState.lastLimitDay) { + tokenState.lastLimitDay = currentDay; tokenState.currentDaySellLimit = tokenState.dailySellLimit; } @@ -356,19 +461,146 @@ contract FeeHandler is return activeTokens.values; } - function _setFeeBeneficiary(address beneficiary) private { - feeBeneficiary = beneficiary; - emit FeeBeneficiarySet(beneficiary); + function _getBurnFraction() internal view returns (uint256) { + return getBurnFractionFixidity().unwrap(); } - function _setBurnFraction(uint256 newFraction) private { - FixidityLib.Fraction memory fraction = FixidityLib.wrap(newFraction); + function _setDistributionAndBurnAmounts(TokenState storage tokenState, IERC20 token) internal { + uint256 balanceOfToken = token.balanceOf(address(this)); + uint256 balanceToProcess = balanceOfToken.sub(tokenState.toDistribute).sub(tokenState.toBurn); + _setDistributeAfterBurn(tokenState, balanceToProcess); + + emit DistributionAmountSet(address(token), tokenState.toDistribute); + } + + function _executePayment(address tokenAddress, address beneficiary, uint256 amount) internal { + require( + _getTokenActive(tokenAddress) || + tokenAddress == registry.getAddressForOrDie(CELO_TOKEN_REGISTRY_ID), + "Token needs to be active" + ); + + uint256 balanceToDistribute = amount; + + if (balanceToDistribute == 0) { + // don't distribute with zero balance + return; + } + + IERC20 token = IERC20(tokenAddress); + token.transfer(beneficiary, balanceToDistribute); + } + + function _setDistributeAfterBurn( + TokenState storage tokenState, + uint256 balanceToProcess + ) internal returns (uint256) { + uint256 balanceToBurn = FixidityLib + .newFixed(balanceToProcess) + .multiply(getBurnFractionFixidity()) + .fromFixed(); + tokenState.toBurn = tokenState.toBurn.add(balanceToBurn); + tokenState.toDistribute = tokenState.toDistribute.add(balanceToProcess.sub(balanceToBurn)); + return tokenState.toBurn; + } + + function shouldBurn() public view returns (bool) { + return _getBurnFraction() != 0; + } + + function checkTotalBeneficiary() internal view { require( - FixidityLib.lte(fraction, FixidityLib.fixed1()), - "Burn fraction must be less than or equal to 1" + getTotalFractionOfOtherBeneficiariesAndCarbonFixidity().lte(FixidityLib.fixed1()), + "Total beneficiaries fraction must be less than 1" ); - burnFraction = fraction; - emit BurnFractionSet(newFraction); + } + + function _setCarbonFraction(uint256 _newFraction) internal { + FixidityLib.Fraction memory newFraction = FixidityLib.wrap(_newFraction); + require(newFraction.lte(FixidityLib.fixed1()), "New cargon fraction can't be greather than 1"); + ignoreRenaming_inverseCarbonFraction = FixidityLib.fixed1().subtract(newFraction); + checkTotalBeneficiary(); + emit CarbonFractionSet(_newFraction); + } + + function _setBeneficiaryFraction( + address beneficiaryAddress, + FixidityLib.Fraction memory newFraction + ) internal { + Beneficiary storage beneficiary = otherBeneficiaries[beneficiaryAddress]; + require(beneficiary.exists, "Beneficiary not found"); + totalFractionOfOtherBeneficiaries = totalFractionOfOtherBeneficiaries.add(newFraction); + checkTotalBeneficiary(); + beneficiary.fraction = newFraction; + emit BeneficiaryFractionSet(beneficiaryAddress, newFraction.unwrap()); + } + + function _setBeneficiaryName(address beneficiary, string memory name) internal { + require(otherBeneficiaries[beneficiary].exists, "Beneficiary not found"); + otherBeneficiaries[beneficiary].name = name; + emit BeneficiaryNameSet(beneficiary, name); + } + + function getTotalFractionOfOtherBeneficiariesAndCarbonFixidity() + internal + view + returns (FixidityLib.Fraction memory) + { + return totalFractionOfOtherBeneficiaries.add(getCarbonFractionFixidity()); + } + + // avoid using UsingRegistry contract + function getCeloTokenAddress() internal view returns (address) { + return registry.getAddressForOrDie(CELO_TOKEN_REGISTRY_ID); + } + + function getCarbonFractionFixidity() internal view returns (FixidityLib.Fraction memory) { + return FixidityLib.fixed1().subtract(ignoreRenaming_inverseCarbonFraction); + } + + function getBurnFractionFixidity() internal view returns (FixidityLib.Fraction memory) { + return FixidityLib.fixed1().subtract(getTotalFractionOfOtherBeneficiariesAndCarbonFixidity()); + } + + function _getTokenActive(address tokenAddress) internal view returns (bool) { + return activeTokens.contains(tokenAddress); + } + + /** + * @notice Burns all the Celo balance of this contract. + */ + function _burnCelo() private { + address celoTokenAddress = getCeloTokenAddress(); + TokenState storage tokenState = tokenStates[celoTokenAddress]; + _setDistributionAndBurnAmounts(tokenState, IERC20(celoTokenAddress)); + + if (tokenState.toBurn == 0) { + return; + } + + ICeloToken(celoTokenAddress).burn(tokenState.toBurn); + tokenState.toBurn = 0; + } + + /** + * @notice Updates the current day limit for a token. + * @param token The address of the token to query. + * @param amountBurned the amount of the token that was burned. + */ + function updateLimits(address token, uint256 amountBurned) private { + TokenState storage tokenState = tokenStates[token]; + + if (tokenState.dailySellLimit == 0) { + // if no limit set, assume uncapped + return; + } + tokenState.currentDaySellLimit = tokenState.currentDaySellLimit.sub(amountBurned); + emit DailySellLimitUpdated(amountBurned); + } + + function _setCarbonFeeBeneficiary(address beneficiary) private { + ignoreRenaming_carbonFeeBeneficiary = beneficiary; + emit FeeBeneficiarySet(beneficiary); } function _addToken(address tokenAddress, address handlerAddress) private { @@ -383,8 +615,7 @@ contract FeeHandler is function _activateToken(address tokenAddress) private { TokenState storage tokenState = tokenStates[tokenAddress]; require( - tokenState.handler != address(0) || - tokenAddress == registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID), + tokenState.handler != address(0) || tokenAddress == getCeloTokenAddress(), "Handler has to be set to activate token" ); activeTokens.add(tokenAddress); @@ -408,28 +639,21 @@ contract FeeHandler is } function _sell(address tokenAddress) private onlyWhenNotFrozen nonReentrant { + if (!shouldBurn()) { + return; + } IERC20 token = IERC20(tokenAddress); TokenState storage tokenState = tokenStates[tokenAddress]; - require(tokenState.handler != address(0), "Handler has to be set to sell token"); + require(_getTokenActive(tokenAddress), "Token needs to be active to sell"); require( FixidityLib.unwrap(tokenState.maxSlippage) != 0, "Max slippage has to be set to sell token" ); - FixidityLib.Fraction memory balanceToProcess = FixidityLib.newFixed( - token.balanceOf(address(this)).sub(tokenState.toDistribute) - ); - uint256 balanceToBurn = (burnFraction.multiply(balanceToProcess).fromFixed()); + _setDistributionAndBurnAmounts(tokenState, token); - tokenState.toDistribute = tokenState.toDistribute.add( - balanceToProcess.fromFixed().sub(balanceToBurn) - ); - - // small numbers cause rounding errors and zero case should be skipped - if (balanceToBurn < MIN_BURN) { - return; - } // eso debería estar antes de quemar el storage + uint256 balanceToBurn = tokenState.toBurn; if (dailySellLimitHit(tokenAddress, balanceToBurn)) { // in case the limit is hit, burn the max possible @@ -437,45 +661,78 @@ contract FeeHandler is emit DailyLimitHit(tokenAddress, balanceToBurn); } + // small numbers cause rounding errors and zero case should be skipped + if (balanceToBurn < MIN_BURN) { + return; + } + token.transfer(tokenState.handler, balanceToBurn); IFeeHandlerSeller handler = IFeeHandlerSeller(tokenState.handler); uint256 celoReceived = handler.sell( tokenAddress, - registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID), + getCeloTokenAddress(), balanceToBurn, FixidityLib.unwrap(tokenState.maxSlippage) ); - celoToBeBurned = celoToBeBurned.add(celoReceived); + // substract from toBurn only the amount that was burned + tokenState.toBurn = tokenState.toBurn.sub(balanceToBurn); + getCeloTokenState().toBurn = getCeloTokenState().toBurn.add(celoReceived); tokenState.pastBurn = tokenState.pastBurn.add(balanceToBurn); updateLimits(tokenAddress, balanceToBurn); emit SoldAndBurnedToken(tokenAddress, balanceToBurn); } - function _distribute(address tokenAddress) private onlyWhenNotFrozen nonReentrant { - require(feeBeneficiary != address(0), "Can't distribute to the zero address"); - IERC20 token = IERC20(tokenAddress); - uint256 tokenBalance = token.balanceOf(address(this)); + function _calculateDistributeAmounts( + FixidityLib.Fraction memory thisTokenFraction, + FixidityLib.Fraction memory totalFractionOfOtherBeneficiariesAndCarbonFixidity, + uint256 toDistribute + ) private pure returns (uint256) { + FixidityLib.Fraction memory proportionOfThisToken = thisTokenFraction.divide( + totalFractionOfOtherBeneficiariesAndCarbonFixidity + ); - TokenState storage tokenState = tokenStates[tokenAddress]; + FixidityLib.Fraction memory toDistributeFraction = FixidityLib.newFixed(toDistribute).multiply( + proportionOfThisToken + ); + + return toDistributeFraction.fromFixed(); + } + + function _distribute(address tokenAddress) private onlyWhenNotFrozen nonReentrant { require( - tokenState.handler != address(0) || - tokenAddress == registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID), - "Handler has to be set to sell token" + ignoreRenaming_carbonFeeBeneficiary != address(0), + "Can't distribute to the zero address" ); + TokenState storage tokenState = tokenStates[tokenAddress]; - // safty check to avoid a revert due balance - uint256 balanceToDistribute = Math.min(tokenBalance, tokenState.toDistribute); + FixidityLib.Fraction + memory totalFractionOfOtherBeneficiariesAndCarbonFixidity = getTotalFractionOfOtherBeneficiariesAndCarbonFixidity(); - if (balanceToDistribute == 0) { - // don't distribute with zero balance - return; + uint256 carbonFundAmount = _calculateDistributeAmounts( + getCarbonFractionFixidity(), + totalFractionOfOtherBeneficiariesAndCarbonFixidity, + tokenState.toDistribute + ); + + _executePayment(tokenAddress, ignoreRenaming_carbonFeeBeneficiary, carbonFundAmount); + + for (uint256 i = 0; i < EnumerableSet.length(otherBeneficiariesAddresses); i++) { + address beneficiary = otherBeneficiariesAddresses.get(i); + Beneficiary storage otherBeneficiary = otherBeneficiaries[beneficiary]; + + uint256 amount = _calculateDistributeAmounts( + otherBeneficiary.fraction, + totalFractionOfOtherBeneficiariesAndCarbonFixidity, + tokenState.toDistribute + ); + + _executePayment(tokenAddress, beneficiary, amount); } - token.transfer(feeBeneficiary, balanceToDistribute); - tokenState.toDistribute = tokenState.toDistribute.sub(balanceToDistribute); + tokenState.toDistribute = 0; } function _setMaxSplippage(address token, uint256 newMax) private { @@ -501,68 +758,42 @@ contract FeeHandler is _distribute(token); } // distribute Celo - _distribute(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); + _distribute(getCeloTokenAddress()); } function _handleAll() private { - for (uint256 i = 0; i < EnumerableSet.length(activeTokens); i++) { - // calling _handle would trigger may burn Celo and distributions - // that can be just batched at the end - address token = activeTokens.get(i); - _sell(token); - } - _distributeAll(); // distributes Celo as well + _handle(activeTokens.values); + } + + function _handleCelo() private { _burnCelo(); + address celoToken = getCeloTokenAddress(); + _distribute(celoToken); } + // tokenAddress can be Celo function _handle(address tokenAddress) private { - // Celo doesn't have to be exchanged for anything - if (tokenAddress != registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)) { + address celoToken = getCeloTokenAddress(); + if (tokenAddress != celoToken) { _sell(tokenAddress); + _distribute(tokenAddress); } - _burnCelo(); - _distribute(tokenAddress); - _distribute(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); // function distribute Celo + _handleCelo(); } - /** - * @notice Burns all the Celo balance of this contract. - */ - function _burnCelo() private { - TokenState storage tokenState = tokenStates[ - registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID) - ]; - ICeloToken celo = ICeloToken(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); - - uint256 balanceOfCelo = address(this).balance; - - uint256 balanceToProcess = balanceOfCelo.sub(tokenState.toDistribute).sub(celoToBeBurned); - uint256 currentBalanceToBurn = FixidityLib - .newFixed(balanceToProcess) - .multiply(burnFraction) - .fromFixed(); - uint256 totalBalanceToBurn = currentBalanceToBurn.add(celoToBeBurned); - celo.burn(totalBalanceToBurn); + // tokenAddresses should not contain the Celo address + function _handle(address[] memory tokenAddresses) private { + // Celo doesn't have to be exchanged for anything + for (uint256 i = 0; i < tokenAddresses.length; i++) { + address token = tokenAddresses[i]; + _sell(token); + _distribute(token); + } - celoToBeBurned = 0; - tokenState.toDistribute = tokenState.toDistribute.add( - balanceToProcess.sub(currentBalanceToBurn) - ); + _handleCelo(); } - /** - * @notice Updates the current day limit for a token. - * @param token The address of the token to query. - * @param amountBurned the amount of the token that was burned. - */ - function updateLimits(address token, uint256 amountBurned) private { - TokenState storage tokenState = tokenStates[token]; - - if (tokenState.dailySellLimit == 0) { - // if no limit set, assume uncapped - return; - } - tokenState.currentDaySellLimit = tokenState.currentDaySellLimit.sub(amountBurned); - emit DailySellLimitUpdated(amountBurned); + function getCeloTokenState() private view returns (TokenState storage) { + return tokenStates[getCeloTokenAddress()]; } } diff --git a/packages/protocol/contracts/common/FeeHandlerSeller.sol b/packages/protocol/contracts/common/FeeHandlerSeller.sol index 8ccd03b08ef..24d703c1d02 100644 --- a/packages/protocol/contracts/common/FeeHandlerSeller.sol +++ b/packages/protocol/contracts/common/FeeHandlerSeller.sol @@ -1,24 +1,28 @@ pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "../common/FixidityLib.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; + import "./UsingRegistry.sol"; +import "../common/FixidityLib.sol"; import "../common/Initializable.sol"; +import "../common/interfaces/ICeloVersionedContract.sol"; // Abstract class for a FeeHandlerSeller, as defined in CIP-52 // https://github.com/celo-org/celo-proposals/blob/master/CIPs/cip-0052.md -contract FeeHandlerSeller is Ownable, Initializable, UsingRegistry { +contract FeeHandlerSeller is Ownable, Initializable, UsingRegistry, ICeloVersionedContract { using SafeMath for uint256; using FixidityLib for FixidityLib.Fraction; // Address of the token // Minimal number of reports in SortedOracles contract mapping(address => uint256) public minimumReports; + mapping(address => address) public oracleAddresses; event MinimumReportsSet(address tokenAddress, uint256 minimumReports); event TokenSold(address soldTokenAddress, address boughtTokenAddress, uint256 amount); + event OracleAddressSet(address _token, address _oracle); function initialize( address _registryAddress, @@ -87,4 +91,18 @@ contract FeeHandlerSeller is Ownable, Initializable, UsingRegistry { minimumReports[tokenAddress] = newMininumReports; emit MinimumReportsSet(tokenAddress, newMininumReports); } + + function setOracleAddress(address _tokenAddress, address _oracleAddress) external onlyOwner { + oracleAddresses[_tokenAddress] = _oracleAddress; + emit OracleAddressSet(_tokenAddress, _oracleAddress); + } + + function getOracleAddress(address _tokenAddress) public view returns (address) { + address oracleAddress = oracleAddresses[_tokenAddress]; + if (oracleAddress != address(0)) { + return oracleAddress; + } + + return address(getSortedOracles()); + } } diff --git a/packages/protocol/contracts/common/GoldToken.sol b/packages/protocol/contracts/common/GoldToken.sol index f17862f439c..f3a9d307958 100644 --- a/packages/protocol/contracts/common/GoldToken.sol +++ b/packages/protocol/contracts/common/GoldToken.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; @@ -8,16 +9,26 @@ import "./UsingRegistry.sol"; import "./CalledByVm.sol"; import "./Initializable.sol"; import "./interfaces/ICeloToken.sol"; +import "./interfaces/ICeloTokenInitializer.sol"; import "./interfaces/ICeloVersionedContract.sol"; -import "./interfaces/IMintGoldSchedule.sol"; import "../../contracts-0.8/common/IsL2Check.sol"; +/** + * @title ERC20 interface for the CELO token. + * @notice The native token was initially called "Celo Gold", but soon after + * mainnet launch, the community voted to change the name to "CELO". For legacy + * reasons, the contract itself is still called GoldToken. + * @dev Note that this is not a wrapper token like WETH. Thanks to the + * `transfer` precompile, this contract provides an ERC20 interface *directly* + * to the native token. + */ contract GoldToken is Initializable, CalledByVm, UsingRegistry, IERC20, ICeloToken, + ICeloTokenInitializer, ICeloVersionedContract, IsL2Check { @@ -29,6 +40,7 @@ contract GoldToken is string constant NAME = "Celo native asset"; string constant SYMBOL = "CELO"; uint8 constant DECIMALS = 18; + uint256 constant CELO_SUPPLY_CAP = 1000000000 ether; // 1 billion CELO uint256 internal totalSupply_; // solhint-enable state-visibility @@ -37,25 +49,12 @@ contract GoldToken is // Burn address is 0xdEaD because truffle is having buggy behaviour with the zero address address constant BURN_ADDRESS = address(0x000000000000000000000000000000000000dEaD); - IMintGoldSchedule public goldTokenMintingSchedule; - event Transfer(address indexed from, address indexed to, uint256 value); event TransferComment(string comment); event Approval(address indexed owner, address indexed spender, uint256 value); - event SetGoldTokenMintingScheduleAddress(address indexed newScheduleAddress); - - modifier onlySchedule() { - if (isL2()) { - require(msg.sender == address(goldTokenMintingSchedule), "Only MintGoldSchedule can call."); - } else { - require(msg.sender == address(0), "Only VM can call."); - } - _; - } - /** * @notice Sets initialized == true on implementation contracts * @param test Set to true to skip implementation initialization @@ -72,23 +71,6 @@ contract GoldToken is setRegistry(registryAddress); } - /** - * @notice Used set the address of the MintGoldSchedule contract. - * @param goldTokenMintingScheduleAddress The address of the MintGoldSchedule contract. - */ - function setGoldTokenMintingScheduleAddress( - address goldTokenMintingScheduleAddress - ) external onlyOwner { - require( - goldTokenMintingScheduleAddress != address(0) || - goldTokenMintingScheduleAddress != address(goldTokenMintingSchedule), - "Invalid address." - ); - goldTokenMintingSchedule = IMintGoldSchedule(goldTokenMintingScheduleAddress); - - emit SetGoldTokenMintingScheduleAddress(goldTokenMintingScheduleAddress); - } - /** * @notice Transfers CELO from one address to another. * @param to The address to transfer CELO to. @@ -198,8 +180,9 @@ contract GoldToken is * @notice Mints new CELO and gives it to 'to'. * @param to The account for which to mint tokens. * @param value The amount of CELO to mint. + * @dev This function will be deprecated in L2. */ - function mint(address to, uint256 value) external onlySchedule returns (bool) { + function mint(address to, uint256 value) external onlyL1 onlyVm returns (bool) { if (value == 0) { return true; } @@ -246,18 +229,11 @@ contract GoldToken is return DECIMALS; } - /** - * @return The total amount of CELO in existence, including what the burn address holds. - */ - function totalSupply() external view returns (uint256) { - return totalSupply_; - } - /** * @return The total amount of CELO in existence, not including what the burn address holds. */ function circulatingSupply() external view returns (uint256) { - return totalSupply_.sub(getBurnedAmount()).sub(balanceOf(address(0))); + return allocatedSupply().sub(getBurnedAmount()).sub(balanceOf(address(0))); } /** @@ -283,7 +259,7 @@ contract GoldToken is /** * @notice Gets the amount of CELO that has been burned. - * @return The total amount of Celo that has been sent to the burn address. + * @return The total amount of CELO that has been sent to the burn address. */ function getBurnedAmount() public view returns (uint256) { return balanceOf(BURN_ADDRESS); @@ -298,6 +274,28 @@ contract GoldToken is return _owner.balance; } + /** + * @return The total amount of allocated CELO. + */ + function allocatedSupply() public view returns (uint256) { + if (isL2()) { + return CELO_SUPPLY_CAP - getCeloUnreleasedTreasury().getRemainingBalanceToRelease(); + } else { + return totalSupply(); + } + } + + /** + * @return The total amount of CELO in existence, including what the burn address holds. + */ + function totalSupply() public view returns (uint256) { + if (isL2()) { + return CELO_SUPPLY_CAP; + } else { + return totalSupply_; + } + } + /** * @notice internal CELO transfer from one address to another. * @param to The address to transfer CELO to. diff --git a/packages/protocol/contracts/common/Initializable.sol b/packages/protocol/contracts/common/Initializable.sol index 1a4939f87c3..dba886d1e31 100644 --- a/packages/protocol/contracts/common/Initializable.sol +++ b/packages/protocol/contracts/common/Initializable.sol @@ -1,15 +1,30 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.5.13 <0.9.0; +/** + * @title Used with proxied contracts that have an `initialize` function. + * @notice Ensures the `initialize` function: + * - gets called only once + * - cannot be called on the logic contract. + */ contract Initializable { bool public initialized; + /** + * @notice Ensures the initializer function cannot be called more than once. + */ modifier initializer() { require(!initialized, "contract already initialized"); initialized = true; _; } + /** + * @notice By default, ensures that the `initialize` function cannot be called + * on the logic contract. + * @param testingDeployment When set to true, allows the `initialize` function + * to be called, which is useful in testing when not setting up with a Proxy. + */ constructor(bool testingDeployment) public { if (!testingDeployment) { initialized = true; diff --git a/packages/protocol/contracts/common/MentoFeeHandlerSeller.sol b/packages/protocol/contracts/common/MentoFeeHandlerSeller.sol index 6545459df71..c462c2ff637 100644 --- a/packages/protocol/contracts/common/MentoFeeHandlerSeller.sol +++ b/packages/protocol/contracts/common/MentoFeeHandlerSeller.sol @@ -30,6 +30,7 @@ contract MentoFeeHandlerSeller is IFeeHandlerSeller, FeeHandlerSeller { // without this line the contract can't receive native Celo transfers function() external payable {} + // Note: current version of Mento is not compatible with this Seller function sell( address sellTokenAddress, address buyTokenAddress, @@ -37,7 +38,7 @@ contract MentoFeeHandlerSeller is IFeeHandlerSeller, FeeHandlerSeller { uint256 maxSlippage // as fraction, ) external returns (uint256) { require( - buyTokenAddress == registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID), + buyTokenAddress == registry.getAddressForOrDie(CELO_TOKEN_REGISTRY_ID), "Buy token can only be gold token" ); @@ -64,7 +65,7 @@ contract MentoFeeHandlerSeller is IFeeHandlerSeller, FeeHandlerSeller { stableToken.approve(exchangeAddress, amount); exchange.sell(amount, minAmount, false); - IERC20 goldToken = getGoldToken(); + IERC20 goldToken = getCeloToken(); uint256 celoAmount = goldToken.balanceOf(address(this)); goldToken.transfer(msg.sender, celoAmount); @@ -80,6 +81,6 @@ contract MentoFeeHandlerSeller is IFeeHandlerSeller, FeeHandlerSeller { * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 0, 0); + return (1, 1, 1, 0); } } diff --git a/packages/protocol/contracts/common/PrecompilesOverride.sol b/packages/protocol/contracts/common/PrecompilesOverride.sol new file mode 100644 index 00000000000..5be5a83ce29 --- /dev/null +++ b/packages/protocol/contracts/common/PrecompilesOverride.sol @@ -0,0 +1,69 @@ +pragma solidity ^0.5.13; + +import "./interfaces/ICeloVersionedContract.sol"; +import "../../contracts-0.8/common/IsL2Check.sol"; +import "./UsingRegistry.sol"; + +import "./UsingPrecompiles.sol"; + +/** + * @title PrecompilesOverride Contract + * @notice This contract allows for a smoother transition from L1 to L2 + * by abstracting away the usingPrecompile contract, and taking care of the L1 to L2 switching logic. + **/ +contract PrecompilesOverride is UsingPrecompiles, UsingRegistry { + /** + * @notice Returns the epoch number at a block. + * @param blockNumber Block number where epoch number is calculated. + * @return Epoch number. + */ + function getEpochNumberOfBlock(uint256 blockNumber) public view returns (uint256) { + if (isL2()) { + return getEpochManager().getEpochNumberOfBlock(blockNumber); + } else { + return epochNumberOfBlock(blockNumber, getEpochSize()); + } + } + + /** + * @notice Returns the epoch number at a block. + * @return Current epoch number. + */ + function getEpochNumber() public view returns (uint256) { + return getEpochNumberOfBlock(block.number); + } + + /** + * @notice Gets a validator signer address from the current validator set. + * @param index Index of requested validator in the validator set. + * @return Address of validator signer at the requested index. + */ + function validatorSignerAddressFromCurrentSet(uint256 index) public view returns (address) { + if (isL2()) { + return getEpochManager().getElectedSignerByIndex(index); + } else { + super.validatorSignerAddressFromCurrentSet(index); + } + } + + /** + * @notice Gets a validator address from the current validator set. + * @param index Index of requested validator in the validator set. + * @return Address of validator at the requested index. + */ + function validatorAddressFromCurrentSet(uint256 index) public view onlyL2 returns (address) { + return getEpochManager().getElectedAccountByIndex(index); + } + + /** + * @notice Gets the size of the current elected validator set. + * @return Size of the current elected validator set. + */ + function numberValidatorsInCurrentSet() public view returns (uint256) { + if (isL2()) { + return getEpochManager().numberOfElectedInCurrentSet(); + } else { + return super.numberValidatorsInCurrentSet(); + } + } +} diff --git a/packages/protocol/contracts/common/PrecompilesOverrideV2.sol b/packages/protocol/contracts/common/PrecompilesOverrideV2.sol new file mode 100644 index 00000000000..c3f9ffb85b8 --- /dev/null +++ b/packages/protocol/contracts/common/PrecompilesOverrideV2.sol @@ -0,0 +1,71 @@ +pragma solidity ^0.5.13; + +import "./interfaces/ICeloVersionedContract.sol"; +import "../../contracts-0.8/common/IsL2Check.sol"; +import "./UsingRegistryV2.sol"; + +import "./UsingPrecompiles.sol"; + +/** + * @title PrecompilesOverride Contract + * @notice This contract allows for a smoother transition from L1 to L2 + * by abstracting away the usingPrecompile contract, and taking care of the L1 to L2 switching logic. + * @dev This is a version of the contract that uses UsingRegistryV2, i.e. it + * uses a hardcoded constant for the Registry address. + **/ +contract PrecompilesOverrideV2 is UsingPrecompiles, UsingRegistryV2 { + /** + * @notice Returns the epoch number at a block. + * @param blockNumber Block number where epoch number is calculated. + * @return Epoch number. + */ + function getEpochNumberOfBlock(uint256 blockNumber) public view returns (uint256) { + if (isL2()) { + return getEpochManager().getEpochNumberOfBlock(blockNumber); + } else { + return epochNumberOfBlock(blockNumber, getEpochSize()); + } + } + + /** + * @notice Returns the epoch number at a block. + * @return Current epoch number. + */ + function getEpochNumber() public view returns (uint256) { + return getEpochNumberOfBlock(block.number); + } + + /** + * @notice Gets a validator signer address from the current validator set. + * @param index Index of requested validator in the validator set. + * @return Address of validator signer at the requested index. + */ + function validatorSignerAddressFromCurrentSet(uint256 index) public view returns (address) { + if (isL2()) { + return getEpochManager().getElectedSignerByIndex(index); + } else { + super.validatorSignerAddressFromCurrentSet(index); + } + } + + /** + * @notice Gets a validator address from the current validator set. + * @param index Index of requested validator in the validator set. + * @return Address of validator at the requested index. + */ + function validatorAddressFromCurrentSet(uint256 index) public view onlyL2 returns (address) { + return getEpochManager().getElectedAccountByIndex(index); + } + + /** + * @notice Gets the size of the current elected validator set. + * @return Size of the current elected validator set. + */ + function numberValidatorsInCurrentSet() public view returns (uint256) { + if (isL2()) { + return getEpochManager().numberOfElectedInCurrentSet(); + } else { + return super.numberValidatorsInCurrentSet(); + } + } +} diff --git a/packages/protocol/contracts/common/Proxy.sol b/packages/protocol/contracts/common/Proxy.sol index 9bbfaeefde6..61f573bcbdf 100644 --- a/packages/protocol/contracts/common/Proxy.sol +++ b/packages/protocol/contracts/common/Proxy.sol @@ -1,10 +1,7 @@ -// TODO make Iproxy - pragma solidity ^0.5.13; /* solhint-disable no-inline-assembly, no-complex-fallback, avoid-low-level-calls */ import "openzeppelin-solidity/contracts/utils/Address.sol"; -// import "forge-std/console.sol"; /** * @title A Proxy utilizing the Unstructured Storage pattern. diff --git a/packages/protocol/contracts/common/ProxyFactory.sol b/packages/protocol/contracts/common/ProxyFactory.sol index cfb1f01b64c..1e2f5d7e828 100644 --- a/packages/protocol/contracts/common/ProxyFactory.sol +++ b/packages/protocol/contracts/common/ProxyFactory.sol @@ -1,22 +1,28 @@ pragma solidity ^0.5.13; -// import "@celo-contracts/common/interfaces/IProxy.sol"; import "./Proxy.sol"; import "./interfaces/IProxyFactory.sol"; -// import "forge-std/console.sol"; +/** + * @title Used for deploying Proxy contracts. + */ contract ProxyFactory is IProxyFactory { - function deployProxy(address owner) external returns (address) { - return _deployProxy(owner); - } - + /** + * @notice Deploys a new proxy contract and transfers ownership to the sender + * @return Address of the deployed proxy. + */ function deployProxy() external returns (address) { - return _deployProxy(msg.sender); + return deployProxy(msg.sender); } - function _deployProxy(address owner) private returns (address) { + /** + * @notice Deploys a new proxy contract and transfers ownership to the provided address + * @param owner The address to transfer ownership to. + * @return Address of the deployed proxy. + */ + function deployProxy(address owner) public returns (address) { Proxy proxy = new Proxy(); - proxy._transferOwnership(msg.sender); + proxy._transferOwnership(owner); return address(proxy); } } diff --git a/packages/protocol/contracts/common/Registry.sol b/packages/protocol/contracts/common/Registry.sol index c5a79ed3580..9ebe7970caa 100644 --- a/packages/protocol/contracts/common/Registry.sol +++ b/packages/protocol/contracts/common/Registry.sol @@ -4,12 +4,13 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./interfaces/IRegistry.sol"; +import "./interfaces/IRegistryInitializer.sol"; import "./Initializable.sol"; /** * @title Routes identifiers to addresses. */ -contract Registry is IRegistry, Ownable, Initializable { +contract Registry is IRegistry, IRegistryInitializer, Ownable, Initializable { using SafeMath for uint256; mapping(bytes32 => address) public registry; diff --git a/packages/protocol/contracts/common/UniswapFeeHandlerSeller.sol b/packages/protocol/contracts/common/UniswapFeeHandlerSeller.sol index e475413e147..72feb565126 100644 --- a/packages/protocol/contracts/common/UniswapFeeHandlerSeller.sol +++ b/packages/protocol/contracts/common/UniswapFeeHandlerSeller.sol @@ -10,6 +10,7 @@ import "./UsingRegistry.sol"; import "../common/interfaces/IFeeHandlerSeller.sol"; import "../stability/interfaces/ISortedOracles.sol"; +import "../../contracts-0.8/common/interfaces/IOracle.sol"; import "../common/FixidityLib.sol"; import "../common/Initializable.sol"; import "./FeeHandlerSeller.sol"; @@ -70,8 +71,8 @@ contract UniswapFeeHandlerSeller is IFeeHandlerSeller, FeeHandlerSeller { uint256 maxSlippage // as fraction, ) external returns (uint256) { require( - buyTokenAddress == registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID), - "Buy token can only be gold token" + buyTokenAddress == registry.getAddressForOrDie(CELO_TOKEN_REGISTRY_ID), + "Buy token can only be CELO token" ); require( @@ -83,7 +84,7 @@ contract UniswapFeeHandlerSeller is IFeeHandlerSeller, FeeHandlerSeller { // and if it generates a better outcome that the ones enabled that gets used // and the user gets a reward - IERC20 celoToken = getGoldToken(); + IERC20 celoToken = getCeloToken(); IUniswapV2RouterMin bestRouter; uint256 bestRouterQuote = 0; @@ -147,7 +148,7 @@ contract UniswapFeeHandlerSeller is IFeeHandlerSeller, FeeHandlerSeller { * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 0, 0); + return (2, 0, 0, 0); } function _setRouter(address token, address router) private { @@ -175,18 +176,22 @@ contract UniswapFeeHandlerSeller is IFeeHandlerSeller, FeeHandlerSeller { uint256 amount, IUniswapV2RouterMin bestRouter ) private view returns (uint256) { - ISortedOracles sortedOracles = getSortedOracles(); + address _oracleAddress = getOracleAddress(sellTokenAddress); + uint256 minReports = minimumReports[sellTokenAddress]; - require( - sortedOracles.numRates(sellTokenAddress) >= minReports, - "Number of reports for token not enough" - ); + IOracle oracle = IOracle(_oracleAddress); uint256 minimalSortedOracles = 0; // if minimumReports for this token is zero, assume the check is not needed if (minReports > 0) { - (uint256 rateNumerator, uint256 rateDenominator) = sortedOracles.medianRate(sellTokenAddress); + ISortedOracles sortedOracles = ISortedOracles(_oracleAddress); + require( + sortedOracles.numRates(sellTokenAddress) >= minReports, + "Number of reports for token not enough" + ); + + (uint256 rateNumerator, uint256 rateDenominator) = oracle.getExchangeRate(sellTokenAddress); minimalSortedOracles = calculateMinAmount( rateNumerator, @@ -196,7 +201,7 @@ contract UniswapFeeHandlerSeller is IFeeHandlerSeller, FeeHandlerSeller { ); } - IERC20 celoToken = getGoldToken(); + IERC20 celoToken = getCeloToken(); address pair = IUniswapV2FactoryMin(bestRouter.factory()).getPair( sellTokenAddress, address(celoToken) diff --git a/packages/protocol/contracts/common/UsingPrecompiles.sol b/packages/protocol/contracts/common/UsingPrecompiles.sol index a09eea3410b..62c9d72ea86 100644 --- a/packages/protocol/contracts/common/UsingPrecompiles.sol +++ b/packages/protocol/contracts/common/UsingPrecompiles.sol @@ -2,8 +2,10 @@ pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; +import "../common/interfaces/IEpochManager.sol"; +import "../../contracts-0.8/common/IsL2Check.sol"; -contract UsingPrecompiles { +contract UsingPrecompiles is IsL2Check { using SafeMath for uint256; address constant TRANSFER = address(0xff - 2); @@ -16,6 +18,7 @@ contract UsingPrecompiles { address constant HASH_HEADER = address(0xff - 9); address constant GET_PARENT_SEAL_BITMAP = address(0xff - 10); address constant GET_VERIFIED_SEAL_BITMAP = address(0xff - 11); + uint256 constant DAY = 86400; /** * @notice calculate a * b^x for fractions a, b to `decimals` precision @@ -27,6 +30,7 @@ contract UsingPrecompiles { * @param _decimals precision * @return Numerator of the computed quantity (not reduced). * @return Denominator of the computed quantity (not reduced). + * @dev This function will be deprecated in L2. */ function fractionMulExp( uint256 aNumerator, @@ -35,7 +39,7 @@ contract UsingPrecompiles { uint256 bDenominator, uint256 exponent, uint256 _decimals - ) public view returns (uint256, uint256) { + ) public view onlyL1 returns (uint256, uint256) { require(aDenominator != 0 && bDenominator != 0, "a denominator is zero"); uint256 returnNumerator; uint256 returnDenominator; @@ -53,8 +57,9 @@ contract UsingPrecompiles { /** * @notice Returns the current epoch size in blocks. * @return The current epoch size in blocks. + * @dev This function will be deprecated in L2. */ - function getEpochSize() public view returns (uint256) { + function getEpochSize() public view onlyL1 returns (uint256) { bytes memory out; bool success; (success, out) = EPOCH_SIZE.staticcall(abi.encodePacked(true)); @@ -66,54 +71,61 @@ contract UsingPrecompiles { * @notice Returns the epoch number at a block. * @param blockNumber Block number where epoch number is calculated. * @return Epoch number. + * @dev This function will be deprecated in L2. */ - function getEpochNumberOfBlock(uint256 blockNumber) public view returns (uint256) { + function getEpochNumberOfBlock(uint256 blockNumber) public view onlyL1 returns (uint256) { return epochNumberOfBlock(blockNumber, getEpochSize()); } /** * @notice Returns the epoch number at a block. * @return Current epoch number. + * @dev This function will be deprecated in L2. */ - function getEpochNumber() public view returns (uint256) { + function getEpochNumber() public view onlyL1 returns (uint256) { return getEpochNumberOfBlock(block.number); } /** - * @notice Gets a validator address from the current validator set. + * @notice Gets a validator signer address from the current validator set. * @param index Index of requested validator in the validator set. - * @return Address of validator at the requested index. + * @return Address of validator signer at the requested index. + * @dev This function will be deprecated in L2. */ - function validatorSignerAddressFromCurrentSet(uint256 index) public view returns (address) { + function validatorSignerAddressFromCurrentSet( + uint256 index + ) public view onlyL1 returns (address) { bytes memory out; bool success; (success, out) = GET_VALIDATOR.staticcall(abi.encodePacked(index, uint256(block.number))); require(success, "error calling validatorSignerAddressFromCurrentSet precompile"); - return address(getUint256FromBytes(out, 0)); + return address(uint160(getUint256FromBytes(out, 0))); } /** - * @notice Gets a validator address from the validator set at the given block number. + * @notice Gets a validator signer address from the validator set at the given block number. * @param index Index of requested validator in the validator set. * @param blockNumber Block number to retrieve the validator set from. - * @return Address of validator at the requested index. + * @return Address of validator signer at the requested index. + * @dev This function will be deprecated in L2. */ function validatorSignerAddressFromSet( uint256 index, uint256 blockNumber - ) public view returns (address) { + ) public view onlyL1 returns (address) { bytes memory out; bool success; (success, out) = GET_VALIDATOR.staticcall(abi.encodePacked(index, blockNumber)); require(success, "error calling validatorSignerAddressFromSet precompile"); - return address(getUint256FromBytes(out, 0)); + return address(uint160(getUint256FromBytes(out, 0))); } /** * @notice Gets the size of the current elected validator set. * @return Size of the current elected validator set. + * @dev This function will be deprecated in L2. */ - function numberValidatorsInCurrentSet() public view returns (uint256) { + function numberValidatorsInCurrentSet() public view onlyL1 returns (uint256) { bytes memory out; bool success; (success, out) = NUMBER_VALIDATORS.staticcall(abi.encodePacked(uint256(block.number))); @@ -125,8 +137,9 @@ contract UsingPrecompiles { * @notice Gets the size of the validator set that must sign the given block number. * @param blockNumber Block number to retrieve the validator set from. * @return Size of the validator set. + * @dev This function will be deprecated in L2. */ - function numberValidatorsInSet(uint256 blockNumber) public view returns (uint256) { + function numberValidatorsInSet(uint256 blockNumber) public view onlyL1 returns (uint256) { bytes memory out; bool success; (success, out) = NUMBER_VALIDATORS.staticcall(abi.encodePacked(blockNumber)); @@ -142,12 +155,13 @@ contract UsingPrecompiles { * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the * account address. 96 bytes. * @return True upon success. + * @dev This function will be deprecated in L2. */ function checkProofOfPossession( address sender, bytes memory blsKey, bytes memory blsPop - ) public view returns (bool) { + ) public view onlyL1 returns (bool) { bool success; (success, ) = PROOF_OF_POSSESSION.staticcall(abi.encodePacked(sender, blsKey, blsPop)); return success; @@ -157,8 +171,9 @@ contract UsingPrecompiles { * @notice Parses block number out of header. * @param header RLP encoded header * @return Block number. + * @dev This function will be deprecated in L2. */ - function getBlockNumberFromHeader(bytes memory header) public view returns (uint256) { + function getBlockNumberFromHeader(bytes memory header) public view onlyL1 returns (uint256) { bytes memory out; bool success; (success, out) = BLOCK_NUMBER_FROM_HEADER.staticcall(abi.encodePacked(header)); @@ -170,8 +185,9 @@ contract UsingPrecompiles { * @notice Computes hash of header. * @param header RLP encoded header * @return Header hash. + * @dev This function will be deprecated in L2. */ - function hashHeader(bytes memory header) public view returns (bytes32) { + function hashHeader(bytes memory header) public view onlyL1 returns (bytes32) { bytes memory out; bool success; (success, out) = HASH_HEADER.staticcall(abi.encodePacked(header)); @@ -183,8 +199,9 @@ contract UsingPrecompiles { * @notice Gets the parent seal bitmap from the header at the given block number. * @param blockNumber Block number to retrieve. Must be within 4 epochs of the current number. * @return Bitmap parent seal with set bits at indices corresponding to signing validators. + * @dev This function will be deprecated in L2. */ - function getParentSealBitmap(uint256 blockNumber) public view returns (bytes32) { + function getParentSealBitmap(uint256 blockNumber) public view onlyL1 returns (bytes32) { bytes memory out; bool success; (success, out) = GET_PARENT_SEAL_BITMAP.staticcall(abi.encodePacked(blockNumber)); @@ -198,8 +215,11 @@ contract UsingPrecompiles { * header. If the parent hash is not in the blockchain, verification fails. * @param header RLP encoded header * @return Bitmap parent seal with set bits at indices correspoinding to signing validators. + * @dev This function will be deprecated in L2. */ - function getVerifiedSealBitmapFromHeader(bytes memory header) public view returns (bytes32) { + function getVerifiedSealBitmapFromHeader( + bytes memory header + ) public view onlyL1 returns (bytes32) { bytes memory out; bool success; (success, out) = GET_VERIFIED_SEAL_BITMAP.staticcall(abi.encodePacked(header)); @@ -210,16 +230,18 @@ contract UsingPrecompiles { /** * @notice Returns the minimum number of required signers for a given block number. * @dev Computed in celo-blockchain as int(math.Ceil(float64(2*valSet.Size()) / 3)) + * @dev This function will be deprecated in L2. */ - function minQuorumSize(uint256 blockNumber) public view returns (uint256) { + function minQuorumSize(uint256 blockNumber) public view onlyL1 returns (uint256) { return numberValidatorsInSet(blockNumber).mul(2).add(2).div(3); } /** * @notice Computes byzantine quorum from current validator set size * @return Byzantine quorum of validators. + * @dev This function will be deprecated in L2. */ - function minQuorumSizeInCurrentSet() public view returns (uint256) { + function minQuorumSizeInCurrentSet() public view onlyL1 returns (uint256) { return minQuorumSize(block.number); } diff --git a/packages/protocol/contracts/common/UsingRegistry.sol b/packages/protocol/contracts/common/UsingRegistry.sol index a9a753822c5..d7d71a93e4e 100644 --- a/packages/protocol/contracts/common/UsingRegistry.sol +++ b/packages/protocol/contracts/common/UsingRegistry.sol @@ -5,13 +5,17 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import "./interfaces/IAccounts.sol"; +import "./interfaces/IEpochManager.sol"; import "./interfaces/IFeeCurrencyWhitelist.sol"; import "./interfaces/IFreezer.sol"; import "./interfaces/IRegistry.sol"; +import "./interfaces/ICeloUnreleasedTreasury.sol"; import "../governance/interfaces/IElection.sol"; +import "../governance/interfaces/IEpochRewards.sol"; import "../governance/interfaces/IGovernance.sol"; import "../governance/interfaces/ILockedGold.sol"; +import "../governance/interfaces/ILockedCelo.sol"; import "../governance/interfaces/IValidators.sol"; import "../identity/interfaces/IRandom.sol"; @@ -44,6 +48,15 @@ contract UsingRegistry is Ownable { bytes32 constant SORTED_ORACLES_REGISTRY_ID = keccak256(abi.encodePacked("SortedOracles")); bytes32 constant STABLE_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("StableToken")); bytes32 constant VALIDATORS_REGISTRY_ID = keccak256(abi.encodePacked("Validators")); + + bytes32 constant CELO_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("CeloToken")); + bytes32 constant LOCKED_CELO_REGISTRY_ID = keccak256(abi.encodePacked("LockedCelo")); + bytes32 constant CELO_UNRELEASED_TREASURY_REGISTRY_ID = + keccak256(abi.encodePacked("CeloUnreleasedTreasury")); + bytes32 constant EPOCH_REWARDS_REGISTRY_ID = keccak256(abi.encodePacked("EpochRewards")); + bytes32 constant EPOCH_MANAGER_ENABLER_REGISTRY_ID = + keccak256(abi.encodePacked("EpochManagerEnabler")); + bytes32 constant EPOCH_MANAGER_REGISTRY_ID = keccak256(abi.encodePacked("EpochManager")); // solhint-enable state-visibility IRegistry public registry; @@ -97,6 +110,9 @@ contract UsingRegistry is Ownable { function getGoldToken() internal view returns (IERC20) { return IERC20(registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); } + function getCeloToken() internal view returns (IERC20) { + return IERC20(registry.getAddressForOrDie(CELO_TOKEN_REGISTRY_ID)); + } function getGovernance() internal view returns (IGovernance) { return IGovernance(registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID)); @@ -105,6 +121,9 @@ contract UsingRegistry is Ownable { function getLockedGold() internal view returns (ILockedGold) { return ILockedGold(registry.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); } + function getLockedCelo() internal view returns (ILockedCelo) { + return ILockedCelo(registry.getAddressForOrDie(LOCKED_CELO_REGISTRY_ID)); + } function getRandom() internal view returns (IRandom) { return IRandom(registry.getAddressForOrDie(RANDOM_REGISTRY_ID)); @@ -125,4 +144,17 @@ contract UsingRegistry is Ownable { function getValidators() internal view returns (IValidators) { return IValidators(registry.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); } + + function getCeloUnreleasedTreasury() internal view returns (ICeloUnreleasedTreasury) { + return + ICeloUnreleasedTreasury(registry.getAddressForOrDie(CELO_UNRELEASED_TREASURY_REGISTRY_ID)); + } + + function getEpochRewards() internal view returns (IEpochRewards) { + return IEpochRewards(registry.getAddressForOrDie(EPOCH_REWARDS_REGISTRY_ID)); + } + + function getEpochManager() internal view returns (IEpochManager) { + return IEpochManager(registry.getAddressForOrDie(EPOCH_MANAGER_REGISTRY_ID)); + } } diff --git a/packages/protocol/contracts/common/UsingRegistryV2.sol b/packages/protocol/contracts/common/UsingRegistryV2.sol index 0be3c5909d3..463ea48af1d 100644 --- a/packages/protocol/contracts/common/UsingRegistryV2.sol +++ b/packages/protocol/contracts/common/UsingRegistryV2.sol @@ -4,13 +4,17 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import "./interfaces/IAccounts.sol"; +import "./interfaces/IEpochManager.sol"; import "./interfaces/IFeeCurrencyWhitelist.sol"; import "./interfaces/IFreezer.sol"; import "./interfaces/IRegistry.sol"; +import "./interfaces/ICeloUnreleasedTreasury.sol"; import "../governance/interfaces/IElection.sol"; +import "../governance/interfaces/IEpochRewards.sol"; import "../governance/interfaces/IGovernance.sol"; import "../governance/interfaces/ILockedGold.sol"; +import "../governance/interfaces/ILockedCelo.sol"; import "../governance/interfaces/IValidators.sol"; import "../identity/interfaces/IRandom.sol"; @@ -57,6 +61,15 @@ contract UsingRegistryV2 { bytes32 internal constant STABLE_REAL_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("StableTokenBRL")); bytes32 internal constant VALIDATORS_REGISTRY_ID = keccak256(abi.encodePacked("Validators")); + bytes32 internal constant CELO_UNRELEASED_TREASURY_REGISTRY_ID = + keccak256(abi.encodePacked("CeloUnreleasedTreasury")); + + bytes32 internal constant CELO_TOKEN_REGISTRY_ID = keccak256(abi.encodePacked("CeloToken")); + bytes32 internal constant LOCKED_CELO_REGISTRY_ID = keccak256(abi.encodePacked("LockedCelo")); + bytes32 internal constant EPOCH_REWARDS_REGISTRY_ID = keccak256(abi.encodePacked("EpochRewards")); + bytes32 internal constant EPOCH_MANAGER_ENABLER_REGISTRY_ID = + keccak256(abi.encodePacked("EpochManagerEnabler")); + bytes32 internal constant EPOCH_MANAGER_REGISTRY_ID = keccak256(abi.encodePacked("EpochManager")); modifier onlyRegisteredContract(bytes32 identifierHash) { require( @@ -121,6 +134,10 @@ contract UsingRegistryV2 { return IERC20(registryContract.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID)); } + function getCeloToken() internal view returns (IERC20) { + return IERC20(registryContract.getAddressForOrDie(CELO_TOKEN_REGISTRY_ID)); + } + function getGovernance() internal view returns (IGovernance) { return IGovernance(registryContract.getAddressForOrDie(GOVERNANCE_REGISTRY_ID)); } @@ -129,6 +146,10 @@ contract UsingRegistryV2 { return ILockedGold(registryContract.getAddressForOrDie(LOCKED_GOLD_REGISTRY_ID)); } + function getLockedCelo() internal view returns (ILockedCelo) { + return ILockedCelo(registryContract.getAddressForOrDie(LOCKED_CELO_REGISTRY_ID)); + } + function getRandom() internal view returns (IRandom) { return IRandom(registryContract.getAddressForOrDie(RANDOM_REGISTRY_ID)); } @@ -160,4 +181,19 @@ contract UsingRegistryV2 { function getValidators() internal view returns (IValidators) { return IValidators(registryContract.getAddressForOrDie(VALIDATORS_REGISTRY_ID)); } + + function getCeloUnreleasedTreasury() internal view returns (ICeloUnreleasedTreasury) { + return + ICeloUnreleasedTreasury( + registryContract.getAddressForOrDie(CELO_UNRELEASED_TREASURY_REGISTRY_ID) + ); + } + + function getEpochRewards() internal view returns (IEpochRewards) { + return IEpochRewards(registryContract.getAddressForOrDie(EPOCH_REWARDS_REGISTRY_ID)); + } + + function getEpochManager() internal view returns (IEpochManager) { + return IEpochManager(registryContract.getAddressForOrDie(EPOCH_MANAGER_REGISTRY_ID)); + } } diff --git a/packages/protocol/contracts/common/interfaces/IBlockable.sol b/packages/protocol/contracts/common/interfaces/IBlockable.sol new file mode 100644 index 00000000000..0f2ad5516e5 --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IBlockable.sol @@ -0,0 +1,7 @@ +pragma solidity >=0.5.13 <0.9.0; + +interface IBlockable { + function setBlockedByContract(address _blockedBy) external; + function isBlocked() external view returns (bool); + function getBlockedByContract() external view returns (address); +} diff --git a/packages/protocol/contracts/common/interfaces/IBlocker.sol b/packages/protocol/contracts/common/interfaces/IBlocker.sol new file mode 100644 index 00000000000..e711c71597a --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IBlocker.sol @@ -0,0 +1,5 @@ +pragma solidity >=0.5.13 <0.9.0; + +interface IBlocker { + function isBlocked() external view returns (bool); +} diff --git a/packages/protocol/contracts/common/interfaces/ICeloToken.sol b/packages/protocol/contracts/common/interfaces/ICeloToken.sol index 8e2986adc53..5f7107395db 100644 --- a/packages/protocol/contracts/common/interfaces/ICeloToken.sol +++ b/packages/protocol/contracts/common/interfaces/ICeloToken.sol @@ -9,7 +9,9 @@ interface ICeloToken { function initialize(address) external; function transferWithComment(address, uint256, string calldata) external returns (bool); function burn(uint256 value) external returns (bool); + function mint(address to, uint256 value) external returns (bool); function name() external view returns (string memory); function symbol() external view returns (string memory); function decimals() external view returns (uint8); + function allocatedSupply() external view returns (uint256); } diff --git a/packages/protocol/contracts/common/interfaces/ICeloTokenInitializer.sol b/packages/protocol/contracts/common/interfaces/ICeloTokenInitializer.sol new file mode 100644 index 00000000000..2959abf7fdb --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/ICeloTokenInitializer.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +/** + * @title This interface describes the non- ERC20 shared interface for all Celo Tokens, and + * in the absence of interface inheritance is intended as a companion to IERC20.sol. + */ +interface ICeloTokenInitializer { + function initialize(address) external; +} diff --git a/packages/protocol/contracts/common/interfaces/ICeloUnreleasedTreasury.sol b/packages/protocol/contracts/common/interfaces/ICeloUnreleasedTreasury.sol new file mode 100644 index 00000000000..e14b9bf536c --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/ICeloUnreleasedTreasury.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface ICeloUnreleasedTreasury { + /** + * @notice Releases the Celo to the specified address. + * @param to The address to release the amount to. + * @param amount The amount to release. + */ + function release(address to, uint256 amount) external; + + function getRemainingBalanceToRelease() external view returns (uint256); +} diff --git a/packages/protocol/contracts/common/interfaces/IEpochManager.sol b/packages/protocol/contracts/common/interfaces/IEpochManager.sol new file mode 100644 index 00000000000..72c54f71814 --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IEpochManager.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochManager { + function initializeSystem( + uint256 firstEpochNumber, + uint256 firstEpochBlock, + address[] calldata firstElected + ) external; + function startNextEpochProcess() external; + function finishNextEpochProcess( + address[] calldata groups, + address[] calldata lessers, + address[] calldata greaters + ) external; + function setToProcessGroups() external; + function processGroup(address group, address lesser, address greater) external; + function sendValidatorPayment(address) external; + function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256); + function getEpochByNumber( + uint256 epochNumber + ) external view returns (uint256, uint256, uint256, uint256); + function getEpochByBlockNumber( + uint256 blockNumber + ) external view returns (uint256, uint256, uint256, uint256); + function getEpochNumberOfBlock(uint256) external view returns (uint256); + function getCurrentEpochNumber() external view returns (uint256); + function numberOfElectedInCurrentSet() external view returns (uint256); + function getElectedAccounts() external view returns (address[] memory); + function getElectedAccountByIndex(uint256 index) external view returns (address); + function getElectedSigners() external view returns (address[] memory); + function getElectedSignerByIndex(uint256 index) external view returns (address); + function epochDuration() external view returns (uint256); + function firstKnownEpoch() external view returns (uint256); + function getEpochProcessingState() + external + view + returns (uint256, uint256, uint256, uint256, uint256); + function systemAlreadyInitialized() external view returns (bool); + function isBlocked() external view returns (bool); + function isTimeForNextEpoch() external view returns (bool); + function isOnEpochProcess() external view returns (bool); + function getFirstBlockAtEpoch(uint256) external view returns (uint256); + function getLastBlockAtEpoch(uint256) external view returns (uint256); +} diff --git a/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol b/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol new file mode 100644 index 00000000000..8cf8ce591bd --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IEpochManagerEnabler.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochManagerEnabler { + function initEpochManager() external; + function captureEpochAndValidators() external; +} diff --git a/packages/protocol/contracts/common/interfaces/IFeeHandler.sol b/packages/protocol/contracts/common/interfaces/IFeeHandler.sol index 60ab4c78cb5..385998240fe 100644 --- a/packages/protocol/contracts/common/interfaces/IFeeHandler.sol +++ b/packages/protocol/contracts/common/interfaces/IFeeHandler.sol @@ -4,7 +4,7 @@ import "../FixidityLib.sol"; interface IFeeHandler { // sets the portion of the fee that should be burned. - function setBurnFraction(uint256 fraction) external; + // function setBurnFraction(uint256 fraction) external; function addToken(address tokenAddress, address handlerAddress) external; function removeToken(address tokenAddress) external; @@ -35,4 +35,5 @@ interface IFeeHandler { // in case some funds need to be returned or moved to another contract function transfer(address token, address recipient, uint256 value) external returns (bool); + function setCarbonFeeBeneficiary(address beneficiary) external; } diff --git a/packages/protocol/contracts/common/interfaces/IFeeHandlerSeller.sol b/packages/protocol/contracts/common/interfaces/IFeeHandlerSeller.sol index deb41a2ee15..fa3437c8222 100644 --- a/packages/protocol/contracts/common/interfaces/IFeeHandlerSeller.sol +++ b/packages/protocol/contracts/common/interfaces/IFeeHandlerSeller.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.5.13 <0.9.0; interface IFeeHandlerSeller { diff --git a/packages/protocol/contracts/common/interfaces/IFreezer.sol b/packages/protocol/contracts/common/interfaces/IFreezer.sol index 8fb68c906f5..5af49807fdc 100644 --- a/packages/protocol/contracts/common/interfaces/IFreezer.sol +++ b/packages/protocol/contracts/common/interfaces/IFreezer.sol @@ -2,7 +2,6 @@ pragma solidity >=0.5.13 <0.9.0; interface IFreezer { - function initialize() external; function freeze(address target) external; function unfreeze(address target) external; function isFrozen(address) external view returns (bool); diff --git a/packages/protocol/contracts/common/interfaces/IFreezerInitializer.sol b/packages/protocol/contracts/common/interfaces/IFreezerInitializer.sol new file mode 100644 index 00000000000..567671431a6 --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IFreezerInitializer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IFreezerInitializer { + function initialize() external; +} diff --git a/packages/protocol/contracts/common/interfaces/IMintGoldSchedule.sol b/packages/protocol/contracts/common/interfaces/IMintGoldSchedule.sol deleted file mode 100644 index 64f1eb2cae6..00000000000 --- a/packages/protocol/contracts/common/interfaces/IMintGoldSchedule.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity >=0.5.13 <0.9.0; - -interface IMintGoldSchedule { - /** - * @notice Mints CELO to the beneficiaries according to the predefined schedule. - */ - function mintAccordingToSchedule() external returns (bool); - - /** - * @return The currently mintable amount. - */ - function getMintableAmount() external returns (uint256); - - /** - * @notice Returns the target Gold supply according to the target schedule. - * @return The target Gold supply according to the target schedule. - */ - function getTargetGoldTotalSupply() external returns (uint256, uint256, uint256); -} diff --git a/packages/protocol/contracts/common/interfaces/IProxy.sol b/packages/protocol/contracts/common/interfaces/IProxy.sol index dd4f9dffb7e..bdcabc09d8f 100644 --- a/packages/protocol/contracts/common/interfaces/IProxy.sol +++ b/packages/protocol/contracts/common/interfaces/IProxy.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: LGPL-3.0-only pragma solidity >=0.5.13 <0.9.0; interface IProxy { diff --git a/packages/protocol/contracts/common/interfaces/IRegistry.sol b/packages/protocol/contracts/common/interfaces/IRegistry.sol index 6b877e43d0d..ce3cbd34f08 100644 --- a/packages/protocol/contracts/common/interfaces/IRegistry.sol +++ b/packages/protocol/contracts/common/interfaces/IRegistry.sol @@ -2,7 +2,6 @@ pragma solidity >=0.5.13 <0.9.0; interface IRegistry { - function initialize() external; // TODO should this be here or have a separate interface? function setAddressFor(string calldata, address) external; function getAddressForOrDie(bytes32) external view returns (address); function getAddressFor(bytes32) external view returns (address); diff --git a/packages/protocol/contracts/common/interfaces/IRegistryInitializer.sol b/packages/protocol/contracts/common/interfaces/IRegistryInitializer.sol new file mode 100644 index 00000000000..f5e2e757c13 --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IRegistryInitializer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IRegistryInitializer { + function initialize() external; +} diff --git a/packages/protocol/contracts/common/interfaces/IScoreManager.sol b/packages/protocol/contracts/common/interfaces/IScoreManager.sol new file mode 100644 index 00000000000..4eee7cd6871 --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IScoreManager.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IScoreManager { + function getValidatorScore(address validator) external view returns (uint256); + function getGroupScore(address validator) external view returns (uint256); +} diff --git a/packages/protocol/contracts/common/interfaces/IScoreManagerGovernance.sol b/packages/protocol/contracts/common/interfaces/IScoreManagerGovernance.sol new file mode 100644 index 00000000000..07768e53a49 --- /dev/null +++ b/packages/protocol/contracts/common/interfaces/IScoreManagerGovernance.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +// import "./IScoreManager.sol"; +// TODO make import when everything is ported to Solidity 0.8 + +interface IScoreManagerGovernance { + function setGroupScore(address group, uint256 score) external; + function setValidatorScore(address validator, uint256 score) external; + function setScoreManagerSetter(address) external; + function getScoreManagerSetter() external view returns (address); +} diff --git a/packages/protocol/contracts/common/libraries/ReentrancyGuard.sol b/packages/protocol/contracts/common/libraries/ReentrancyGuard.sol index d3394816bbf..5364754c495 100644 --- a/packages/protocol/contracts/common/libraries/ReentrancyGuard.sol +++ b/packages/protocol/contracts/common/libraries/ReentrancyGuard.sol @@ -1,4 +1,5 @@ -pragma solidity ^0.5.13; +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.8.20; /** * @title Helps contracts guard against reentrancy attacks. @@ -24,7 +25,7 @@ contract ReentrancyGuard { require(localCounter == _guardCounter, "reentrant call"); } - constructor() internal { + constructor() public { // The counter starts at one to prevent changing it from zero to a non-zero // value, which is a more expensive operation. _guardCounter = 1; diff --git a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol index e1b75eb95d2..2136ec1ad94 100644 --- a/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol +++ b/packages/protocol/contracts/common/linkedlists/SortedLinkedList.sol @@ -71,8 +71,22 @@ library SortedLinkedList { bytes32 lesserKey, bytes32 greaterKey ) internal { - remove(list, key); - insert(list, key, value, lesserKey, greaterKey); + require(contains(list, key), "key not in list"); + require(key != bytes32(0) && key != lesserKey && key != greaterKey, "invalid key"); + require( + (lesserKey != bytes32(0) || greaterKey != bytes32(0)) || list.list.numElements == 1, + "greater and lesser key zero" + ); + + bytes32 oldLesserKey = list.list.elements[key].previousKey; + bytes32 oldGreaterKey = list.list.elements[key].nextKey; + + if (isValueBetween(list, value, oldLesserKey, oldGreaterKey)) { + list.values[key] = value; + } else { + remove(list, key); + insert(list, key, value, lesserKey, greaterKey); + } } /** diff --git a/packages/protocol/contracts/common/proxies/CeloUnreleasedTreasuryProxy.sol b/packages/protocol/contracts/common/proxies/CeloUnreleasedTreasuryProxy.sol new file mode 100644 index 00000000000..553ff00a4b7 --- /dev/null +++ b/packages/protocol/contracts/common/proxies/CeloUnreleasedTreasuryProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../Proxy.sol"; + +/* solhint-disable-next-line no-empty-blocks */ +contract CeloUnreleasedTreasuryProxy is Proxy {} diff --git a/packages/protocol/contracts/common/proxies/EpochManagerEnablerProxy.sol b/packages/protocol/contracts/common/proxies/EpochManagerEnablerProxy.sol new file mode 100644 index 00000000000..ddd10f3cd43 --- /dev/null +++ b/packages/protocol/contracts/common/proxies/EpochManagerEnablerProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../Proxy.sol"; + +/* solhint-disable-next-line no-empty-blocks */ +contract EpochManagerEnablerProxy is Proxy {} diff --git a/packages/protocol/contracts/common/proxies/MintGoldScheduleProxy.sol b/packages/protocol/contracts/common/proxies/EpochManagerProxy.sol similarity index 69% rename from packages/protocol/contracts/common/proxies/MintGoldScheduleProxy.sol rename to packages/protocol/contracts/common/proxies/EpochManagerProxy.sol index d9be7c6df68..7f5fc943e37 100644 --- a/packages/protocol/contracts/common/proxies/MintGoldScheduleProxy.sol +++ b/packages/protocol/contracts/common/proxies/EpochManagerProxy.sol @@ -3,4 +3,4 @@ pragma solidity ^0.5.13; import "../Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract MintGoldScheduleProxy is Proxy {} +contract EpochManagerProxy is Proxy {} diff --git a/packages/protocol/contracts/common/proxies/ScoreManagerProxy.sol b/packages/protocol/contracts/common/proxies/ScoreManagerProxy.sol new file mode 100644 index 00000000000..d46446ee16b --- /dev/null +++ b/packages/protocol/contracts/common/proxies/ScoreManagerProxy.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.5.13; + +import "../Proxy.sol"; + +/* solhint-disable-next-line no-empty-blocks */ +contract ScoreManagerProxy is Proxy {} diff --git a/packages/protocol/contracts/common/test/SortedLinkedListMock.sol b/packages/protocol/contracts/common/test/SortedLinkedListMock.sol new file mode 100644 index 00000000000..8a82e71ff7d --- /dev/null +++ b/packages/protocol/contracts/common/test/SortedLinkedListMock.sol @@ -0,0 +1,41 @@ +pragma solidity ^0.5.13; + +import "../linkedlists/SortedLinkedList.sol"; + +contract SortedLinkedListMock { + using SortedLinkedList for SortedLinkedList.List; + + SortedLinkedList.List private list; + + function insert(bytes32 key, uint256 numerator, bytes32 lesserKey, bytes32 greaterKey) external { + list.insert(key, numerator, lesserKey, greaterKey); + } + + function update(bytes32 key, uint256 numerator, bytes32 lesserKey, bytes32 greaterKey) external { + list.update(key, numerator, lesserKey, greaterKey); + } + + function remove(bytes32 key) external { + list.remove(key); + } + + function contains(bytes32 key) external view returns (bool) { + return list.contains(key); + } + + function getNumElements() external view returns (uint256) { + return list.list.numElements; + } + + function getElements() external view returns (bytes32[] memory, uint256[] memory) { + return list.getElements(); + } + + function head() external view returns (bytes32) { + return list.list.head; + } + + function tail() external view returns (bytes32) { + return list.list.tail; + } +} diff --git a/packages/protocol/contracts/governance/BlockchainParameters.sol b/packages/protocol/contracts/governance/BlockchainParameters.sol index 8d358602344..45bebda1704 100644 --- a/packages/protocol/contracts/governance/BlockchainParameters.sol +++ b/packages/protocol/contracts/governance/BlockchainParameters.sol @@ -5,12 +5,10 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "../common/Initializable.sol"; import "../common/UsingPrecompiles.sol"; -import "../../contracts-0.8/common/IsL2Check.sol"; - /** * @title Contract for storing blockchain parameters that can be set by governance. */ -contract BlockchainParameters is Ownable, Initializable, UsingPrecompiles, IsL2Check { +contract BlockchainParameters is Ownable, Initializable, UsingPrecompiles { using SafeMath for uint256; // obsolete @@ -30,9 +28,9 @@ contract BlockchainParameters is Ownable, Initializable, UsingPrecompiles, IsL2C } ClientVersion private minimumClientVersion; // obsolete - uint256 public blockGasLimit; - uint256 public intrinsicGasForAlternativeFeeCurrency; - LookbackWindow public uptimeLookbackWindow; + uint256 private deprecated_blockGasLimit; + uint256 private deprecated_intrinsicGasForAlternativeFeeCurrency; + LookbackWindow private uptimeLookbackWindow; event IntrinsicGasForAlternativeFeeCurrencySet(uint256 gas); event BlockGasLimitSet(uint256 limit); @@ -77,7 +75,7 @@ contract BlockchainParameters is Ownable, Initializable, UsingPrecompiles, IsL2C * @param gasLimit New block gas limit. */ function setBlockGasLimit(uint256 gasLimit) public onlyOwner onlyL1 { - blockGasLimit = gasLimit; + deprecated_blockGasLimit = gasLimit; emit BlockGasLimitSet(gasLimit); } @@ -86,7 +84,7 @@ contract BlockchainParameters is Ownable, Initializable, UsingPrecompiles, IsL2C * @param gas Intrinsic gas for non-gold gas currencies. */ function setIntrinsicGasForAlternativeFeeCurrency(uint256 gas) public onlyOwner onlyL1 { - intrinsicGasForAlternativeFeeCurrency = gas; + deprecated_intrinsicGasForAlternativeFeeCurrency = gas; emit IntrinsicGasForAlternativeFeeCurrencySet(gas); } @@ -118,6 +116,26 @@ contract BlockchainParameters is Ownable, Initializable, UsingPrecompiles, IsL2C require(lookbackWindow != 0, "UptimeLookbackWindow is not initialized"); } + /** + * @notice Gets the Celo L1 block gas limit. + * @return The block gas limit. + * @dev Once Celo becomes an L2, query Optimism's L1 SystemConfig contract + * instead. + */ + function blockGasLimit() public view onlyL1 returns (uint256) { + return deprecated_blockGasLimit; + } + + /** + * @notice Gets the intrinsic gas paid for transactions using alternative fee + * currencies. + * @return The intrinsic gas for alternative fee currencies. + * @dev Once Celo becomes an L2, query the FeeCurrencyDirectory instead. + */ + function intrinsicGasForAlternativeFeeCurrency() public view onlyL1 returns (uint256) { + return deprecated_intrinsicGasForAlternativeFeeCurrency; + } + /** * @notice Gets the uptime lookback window. */ diff --git a/packages/protocol/contracts/governance/DoubleSigningSlasher.sol b/packages/protocol/contracts/governance/DoubleSigningSlasher.sol index b9aef3f737c..ecbf64f15a4 100644 --- a/packages/protocol/contracts/governance/DoubleSigningSlasher.sol +++ b/packages/protocol/contracts/governance/DoubleSigningSlasher.sol @@ -45,7 +45,7 @@ contract DoubleSigningSlasher is ICeloVersionedContract, SlasherUtil { * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 1, 2, 0); + return (1, 1, 1, 1); } /** @@ -116,7 +116,7 @@ contract DoubleSigningSlasher is ICeloVersionedContract, SlasherUtil { uint256 index, bytes memory headerA, bytes memory headerB - ) public view returns (uint256) { + ) public view onlyL1 returns (uint256) { require(hashHeader(headerA) != hashHeader(headerB), "Block hashes have to be different"); uint256 blockNumber = getBlockNumberFromHeader(headerA); require( diff --git a/packages/protocol/contracts/governance/DowntimeSlasher.sol b/packages/protocol/contracts/governance/DowntimeSlasher.sol index d9f4f68a0ab..88058a93c23 100644 --- a/packages/protocol/contracts/governance/DowntimeSlasher.sol +++ b/packages/protocol/contracts/governance/DowntimeSlasher.sol @@ -64,7 +64,7 @@ contract DowntimeSlasher is ICeloVersionedContract, SlasherUtil { * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (2, 0, 1, 0); + return (2, 0, 0, 1); } /** @@ -170,7 +170,7 @@ contract DowntimeSlasher is ICeloVersionedContract, SlasherUtil { function getBitmapForInterval( uint256 startBlock, uint256 endBlock - ) public view returns (bytes32) { + ) public view onlyL1 returns (bytes32) { require(endBlock >= startBlock, "endBlock must be greater or equal than startBlock"); // The signature bitmap for block N is stored in block N+1. // The latest block is `block.number - 1`, which stores the signature bitmap for @@ -215,7 +215,7 @@ contract DowntimeSlasher is ICeloVersionedContract, SlasherUtil { uint256 startBlock, uint256 endBlock, uint256 signerIndex - ) public view returns (bool) { + ) public view onlyL1 returns (bool) { require(signerIndex < numberValidatorsInSet(startBlock), "bad validator index at start block"); require( isBitmapSetForInterval(startBlock, endBlock), @@ -250,7 +250,7 @@ contract DowntimeSlasher is ICeloVersionedContract, SlasherUtil { uint256[] memory startBlocks, uint256[] memory endBlocks, uint256[] memory signerIndices - ) public view returns (bool) { + ) public view onlyL1 returns (bool) { require(startBlocks.length > 0, "requires at least one interval"); require( startBlocks.length == endBlocks.length, diff --git a/packages/protocol/contracts/governance/Election.sol b/packages/protocol/contracts/governance/Election.sol index 33c05c82ca6..3db4ec170ae 100644 --- a/packages/protocol/contracts/governance/Election.sol +++ b/packages/protocol/contracts/governance/Election.sol @@ -10,13 +10,16 @@ import "../common/CalledByVm.sol"; import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/AddressSortedLinkedList.sol"; -import "../common/UsingPrecompiles.sol"; import "../common/UsingRegistry.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; import "../common/libraries/Heap.sol"; import "../common/libraries/ReentrancyGuard.sol"; -import "../../contracts-0.8/common/IsL2Check.sol"; +import "../common/Blockable.sol"; +import "../common/PrecompilesOverride.sol"; +/** + * @title Manages the validator election process. + */ contract Election is IElection, ICeloVersionedContract, @@ -24,9 +27,9 @@ contract Election is ReentrancyGuard, Initializable, UsingRegistry, - UsingPrecompiles, + PrecompilesOverride, CalledByVm, - IsL2Check + Blockable { using AddressSortedLinkedList for SortedLinkedList.List; using FixidityLib for FixidityLib.Fraction; @@ -153,6 +156,20 @@ contract Election is ); event EpochRewardsDistributedToVoters(address indexed group, uint256 value); + /** + * @notice - On L1, ensures the function is called via the consensus client. + * - On L2, ensures the function is called by the permitted address. + * @param permittedAddress The address permitted to call permissioned + * functions on L2. + */ + modifier onlyVmOrPermitted(address permittedAddress) { + if (isL2()) require(msg.sender == permittedAddress, "Only permitted address can call"); + else { + require(msg.sender == address(0), "Only VM can call"); + } + _; + } + /** * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. * @param registryAddress The address of the registry core smart contract. @@ -198,7 +215,7 @@ contract Election is uint256 value, address lesser, address greater - ) external nonReentrant onlyL1 returns (bool) { + ) external nonReentrant onlyWhenNotBlocked returns (bool) { require(votes.total.eligible.contains(group), "Group not eligible"); require(0 < value, "Vote value cannot be zero"); require(canReceiveVotes(group, value), "Group cannot receive votes"); @@ -231,7 +248,7 @@ contract Election is * @return True upon success. * @dev Pending votes cannot be activated until an election has been held. */ - function activate(address group) external nonReentrant onlyL1 returns (bool) { + function activate(address group) external nonReentrant returns (bool) { address account = getAccounts().voteSignerToAccount(msg.sender); return _activate(group, account); } @@ -243,10 +260,7 @@ contract Election is * @return True upon success. * @dev Pending votes cannot be activated until an election has been held. */ - function activateForAccount( - address group, - address account - ) external nonReentrant onlyL1 returns (bool) { + function activateForAccount(address group, address account) external nonReentrant returns (bool) { return _activate(group, account); } @@ -343,7 +357,7 @@ contract Election is uint256 value, address lesser, address greater - ) external onlyVm onlyL1 { + ) external onlyVmOrPermitted(registry.getAddressFor(EPOCH_MANAGER_REGISTRY_ID)) { _distributeEpochRewards(group, value, lesser, greater); } @@ -369,7 +383,7 @@ contract Election is address group, address lesser, address greater - ) external onlyL1 onlyRegisteredContract(VALIDATORS_REGISTRY_ID) { + ) external onlyRegisteredContract(VALIDATORS_REGISTRY_ID) { uint256 value = getTotalVotesForGroup(group); votes.total.eligible.insert(group, value, lesser, greater); emit ValidatorGroupMarkedEligible(group); @@ -423,6 +437,15 @@ contract Election is return value; } + /** + * @notice Sets the address of the blocking contract. + * @param _blockedBy The address of the contract that will determine if this contract is blocked. + * @dev Can only be called by the owner of the contract. + */ + function setBlockedByContract(address _blockedBy) external onlyOwner { + _setBlockedBy(_blockedBy); + } + /** * @notice Returns the groups that `account` has voted for. * @param account The address of the account casting votes. @@ -474,14 +497,23 @@ contract Election is } /** - * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt + * @notice Returns a list of elected validator signers with seats allocated to groups via the D'Hondt * method. - * @return The list of elected validators. + * @return The list of elected validator signers. */ function electValidatorSigners() external view returns (address[] memory) { return electNValidatorSigners(electableValidators.min, electableValidators.max); } + /** + * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt + * method. + * @return The list of elected validators. + */ + function electValidatorAccounts() external view returns (address[] memory) { + return electNValidatorAccounts(electableValidators.min, electableValidators.max); + } + /** * @notice Returns the total number of votes cast by an account. * @param account The address of the account. @@ -544,7 +576,7 @@ contract Election is address group, uint256 totalEpochRewards, uint256[] calldata uptimes - ) external view returns (uint256) { + ) external view onlyL1 returns (uint256) { IValidators validators = getValidators(); // The group must meet the balance requirements for their voters to receive epoch rewards. if (!validators.meetsAccountLockedGoldRequirements(group) || votes.active.total <= 0) { @@ -570,6 +602,43 @@ contract Election is .fromFixed(); } + /** + * @notice Returns the amount of rewards that voters for `group` are due at the end of an epoch. + * @param group The group to calculate epoch rewards for. + * @param totalEpochRewards The total amount of rewards going to all voters. + * @param groupScore The score of the group. + * @return The amount of rewards that voters for `group` are due at the end of an epoch. + * @dev Eligible groups that have received their maximum number of votes cannot receive more. + */ + function getGroupEpochRewardsBasedOnScore( + address group, + uint256 totalEpochRewards, + uint256 groupScore + ) external view onlyL2 returns (uint256) { + IValidators validators = getValidators(); + // The group must meet the balance requirements for their voters to receive epoch rewards. + if (!validators.meetsAccountLockedGoldRequirements(group) || votes.active.total <= 0) { + return 0; + } + + FixidityLib.Fraction memory votePortion = FixidityLib.newFixedFraction( + votes.active.forGroup[group].total, + votes.active.total + ); + FixidityLib.Fraction memory slashingMultiplier = FixidityLib.wrap( + validators.getValidatorGroupSlashingMultiplier(group) + ); + + FixidityLib.Fraction memory score = FixidityLib.wrap(groupScore); + return + FixidityLib + .newFixed(totalEpochRewards) + .multiply(votePortion) + .multiply(score) + .multiply(slashingMultiplier) + .fromFixed(); + } + /** * @notice Returns whether or not an account's votes for the specified group can be activated. * @param account The account with pending votes. @@ -577,10 +646,7 @@ contract Election is * @return Whether or not `account` has activatable votes for `group`. * @dev Pending votes cannot be activated until an election has been held. */ - function hasActivatablePendingVotes( - address account, - address group - ) external view onlyL1 returns (bool) { + function hasActivatablePendingVotes(address account, address group) external view returns (bool) { PendingVote storage pendingVote = votes.pending.forGroup[group].byAccount[account]; return pendingVote.epoch < getEpochNumber() && pendingVote.value > 0; } @@ -619,7 +685,7 @@ contract Election is * @param max The maximum number of validators that can be elected. * @return True upon success. */ - function setElectableValidators(uint256 min, uint256 max) public onlyOwner onlyL1 returns (bool) { + function setElectableValidators(uint256 min, uint256 max) public onlyOwner returns (bool) { require(0 < min, "Minimum electable validators cannot be zero"); require(min <= max, "Maximum electable validators cannot be smaller than minimum"); require( @@ -636,9 +702,7 @@ contract Election is * @param _maxNumGroupsVotedFor The maximum number of groups an account can vote for. * @return True upon success. */ - function setMaxNumGroupsVotedFor( - uint256 _maxNumGroupsVotedFor - ) public onlyOwner onlyL1 returns (bool) { + function setMaxNumGroupsVotedFor(uint256 _maxNumGroupsVotedFor) public onlyOwner returns (bool) { require(_maxNumGroupsVotedFor != maxNumGroupsVotedFor, "Max groups voted for not changed"); maxNumGroupsVotedFor = _maxNumGroupsVotedFor; emit MaxNumGroupsVotedForSet(_maxNumGroupsVotedFor); @@ -650,7 +714,7 @@ contract Election is * @param threshold Electability threshold as unwrapped Fraction. * @return True upon success. */ - function setElectabilityThreshold(uint256 threshold) public onlyOwner onlyL1 returns (bool) { + function setElectabilityThreshold(uint256 threshold) public onlyOwner returns (bool) { electabilityThreshold = FixidityLib.wrap(threshold); require( electabilityThreshold.lt(FixidityLib.fixed1()), @@ -681,7 +745,7 @@ contract Election is * If not run, voting power of account will not reflect rewards awarded. * @param flag The on/off flag. */ - function setAllowedToVoteOverMaxNumberOfGroups(bool flag) public onlyL1 { + function setAllowedToVoteOverMaxNumberOfGroups(bool flag) public { address account = getAccounts().voteSignerToAccount(msg.sender); IValidators validators = getValidators(); require( @@ -742,16 +806,35 @@ contract Election is return votes.active.total; } + function electNValidatorSigners( + uint256 minElectableValidators, + uint256 maxElectableValidators + ) public view returns (address[] memory) { + bool accounts = false; + return + _electNValidatorSignerOrAccount(minElectableValidators, maxElectableValidators, accounts); + } + + function electNValidatorAccounts( + uint256 minElectableValidators, + uint256 maxElectableValidators + ) public view returns (address[] memory) { + bool accounts = true; + return + _electNValidatorSignerOrAccount(minElectableValidators, maxElectableValidators, accounts); + } + /** - * @notice Returns a list of elected validators with seats allocated to groups via the D'Hondt + * @notice Returns a list of elected validator with seats allocated to groups via the D'Hondt * method. - * @return The list of elected validators. + * @return The list of elected validator signers or accounts depending on input. * @dev See https://en.wikipedia.org/wiki/D%27Hondt_method#Allocation for more information. */ - function electNValidatorSigners( + function _electNValidatorSignerOrAccount( uint256 minElectableValidators, - uint256 maxElectableValidators - ) public view returns (address[] memory) { + uint256 maxElectableValidators, + bool accounts // accounts or signers + ) internal view returns (address[] memory) { // Groups must have at least `electabilityThreshold` proportion of the total votes to be // considered for the election. uint256 requiredVotes = electabilityThreshold @@ -763,6 +846,7 @@ contract Election is requiredVotes, maxElectableValidators ); + address[] memory electionGroups = votes.total.eligible.headN(numElectionGroups); uint256[] memory numMembers = getValidators().getGroupsNumMembers(electionGroups); // Holds the number of members elected for each of the eligible validator groups. @@ -804,12 +888,23 @@ contract Election is // Grab the top validators from each group that won seats. address[] memory electedValidators = new address[](totalNumMembersElected); totalNumMembersElected = 0; + + IValidators validators = getValidators(); + for (uint256 i = 0; i < electionGroups.length; i = i.add(1)) { // We use the validating delegate if one is set. - address[] memory electedGroupValidators = getValidators().getTopGroupValidators( - electionGroups[i], - numMembersElected[i] - ); + address[] memory electedGroupValidators; + if (accounts) { + electedGroupValidators = validators.getTopGroupValidatorsAccounts( + electionGroups[i], + numMembersElected[i] + ); + } else { + electedGroupValidators = validators.getTopGroupValidators( + electionGroups[i], + numMembersElected[i] + ); + } for (uint256 j = 0; j < electedGroupValidators.length; j = j.add(1)) { electedValidators[totalNumMembersElected] = electedGroupValidators[j]; totalNumMembersElected = totalNumMembersElected.add(1); @@ -822,7 +917,7 @@ contract Election is * @notice Returns get current validator signers using the precompiles. * @return List of current validator signers. */ - function getCurrentValidatorSigners() public view returns (address[] memory) { + function getCurrentValidatorSigners() public view onlyL1 returns (address[] memory) { uint256 n = numberValidatorsInCurrentSet(); address[] memory res = new address[](n); for (uint256 i = 0; i < n; i = i.add(1)) { @@ -910,7 +1005,7 @@ contract Election is uint256 value, address lesser, address greater - ) internal onlyL1 { + ) internal { if (votes.total.eligible.contains(group)) { uint256 newVoteTotal = votes.total.eligible.getValue(group).add(value); votes.total.eligible.update(group, newVoteTotal, lesser, greater); @@ -921,9 +1016,11 @@ contract Election is emit EpochRewardsDistributedToVoters(group, value); } - function _activate(address group, address account) internal returns (bool) { + function _activate(address group, address account) internal onlyWhenNotBlocked returns (bool) { PendingVote storage pendingVote = votes.pending.forGroup[group].byAccount[account]; + require(pendingVote.epoch < getEpochNumber(), "Pending vote epoch not passed"); + uint256 value = pendingVote.value; require(value > 0, "Vote value cannot be zero"); decrementPendingVotes(group, account, value); @@ -938,7 +1035,7 @@ contract Election is address lesser, address greater, uint256 index - ) internal returns (bool) { + ) internal onlyWhenNotBlocked returns (bool) { // TODO(asa): Dedup with revokePending. require(group != address(0), "Group address zero"); address account = getAccounts().voteSignerToAccount(msg.sender); @@ -979,7 +1076,7 @@ contract Election is address lesser, address greater, uint256 index - ) internal returns (uint256) { + ) internal onlyWhenNotBlocked returns (uint256) { uint256 remainingValue = maxValue; uint256 pendingVotes = getPendingVotesForGroupByAccount(group, account); if (pendingVotes > 0) { diff --git a/packages/protocol/contracts/governance/EpochRewards.sol b/packages/protocol/contracts/governance/EpochRewards.sol index fc9135e3f39..44e27f1b0ce 100644 --- a/packages/protocol/contracts/governance/EpochRewards.sol +++ b/packages/protocol/contracts/governance/EpochRewards.sol @@ -3,27 +3,26 @@ pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import "../common/CalledByVm.sol"; +import "./interfaces/IEpochRewards.sol"; import "../common/FixidityLib.sol"; import "../common/Freezable.sol"; import "../common/Initializable.sol"; import "../common/UsingRegistry.sol"; -import "../common/UsingPrecompiles.sol"; +import "../common/PrecompilesOverride.sol"; +import "../common/interfaces/ICeloToken.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; -import "../../contracts-0.8/common/IsL2Check.sol"; /** * @title Contract for calculating epoch rewards. */ contract EpochRewards is ICeloVersionedContract, + IEpochRewards, Ownable, Initializable, - UsingPrecompiles, UsingRegistry, - Freezable, - CalledByVm, - IsL2Check + PrecompilesOverride, + Freezable { using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; @@ -86,6 +85,14 @@ contract EpochRewards is event TargetVotingYieldUpdated(uint256 fraction); + modifier onlyVmOrPermitted(address permittedAddress) { + if (isL2()) require(msg.sender == permittedAddress, "Only permitted address can call"); + else { + require(msg.sender == address(0), "Only VM can call"); + } + _; + } + /** * @notice Sets initialized == true on implementation contracts * @param test Set to true to skip implementation initialization @@ -145,7 +152,11 @@ contract EpochRewards is * voting Gold fraction. * @dev Only called directly by the protocol. */ - function updateTargetVotingYield() external onlyVm onlyWhenNotFrozen onlyL1 { + function updateTargetVotingYield() + external + onlyVmOrPermitted(registry.getAddressFor(EPOCH_MANAGER_REGISTRY_ID)) + onlyWhenNotFrozen + { _updateTargetVotingYield(); } @@ -153,8 +164,11 @@ contract EpochRewards is * @notice Determines if the reserve is low enough to demand a diversion from * the community reward. Targets initial critical ratio of 2 with a linear * decline until 25 years have passed where the critical ratio will be 1. + * @dev This function is called by L1 celo-blockchain during epoch reward + * distribution. + * @dev TODO Remove this after the L2 transition. */ - function isReserveLow() external view returns (bool) { + function isReserveLow() external view onlyL1 returns (bool) { // critical reserve ratio = 2 - time in second / 25 years FixidityLib.Fraction memory timeSinceInitialization = FixidityLib.newFixed(now.sub(startTime)); FixidityLib.Fraction memory m = FixidityLib.newFixed(25 * 365 * 1 days); @@ -275,7 +289,7 @@ contract EpochRewards is * @param value The percentage of the total reward to be sent to the community funds. * @return True upon success. */ - function setCommunityRewardFraction(uint256 value) public onlyOwner onlyL1 returns (bool) { + function setCommunityRewardFraction(uint256 value) public onlyOwner returns (bool) { require( value != communityRewardFraction.unwrap() && value < FixidityLib.fixed1().unwrap(), "Value must be different from existing community reward fraction and less than 1" @@ -291,10 +305,7 @@ contract EpochRewards is * @param value The percentage of the total reward to be sent to the carbon offsetting partner. * @return True upon success. */ - function setCarbonOffsettingFund( - address partner, - uint256 value - ) public onlyOwner onlyL1 returns (bool) { + function setCarbonOffsettingFund(address partner, uint256 value) public onlyOwner returns (bool) { require( partner != carbonOffsettingPartner || value != carbonOffsettingFraction.unwrap(), "Partner and value must be different from existing carbon offsetting fund" @@ -311,7 +322,7 @@ contract EpochRewards is * @param value The percentage of floating Gold voting to target. * @return True upon success. */ - function setTargetVotingGoldFraction(uint256 value) public onlyOwner onlyL1 returns (bool) { + function setTargetVotingGoldFraction(uint256 value) public onlyOwner returns (bool) { require(value != targetVotingGoldFraction.unwrap(), "Target voting gold fraction unchanged"); require( value < FixidityLib.fixed1().unwrap(), @@ -327,7 +338,7 @@ contract EpochRewards is * @param value The value in Celo Dollars. * @return True upon success. */ - function setTargetValidatorEpochPayment(uint256 value) public onlyOwner onlyL1 returns (bool) { + function setTargetValidatorEpochPayment(uint256 value) public onlyOwner returns (bool) { require(value != targetValidatorEpochPayment, "Target validator epoch payment unchanged"); targetValidatorEpochPayment = value; emit TargetValidatorEpochPaymentSet(value); @@ -347,7 +358,7 @@ contract EpochRewards is uint256 max, uint256 underspendAdjustmentFactor, uint256 overspendAdjustmentFactor - ) public onlyOwner onlyL1 returns (bool) { + ) public onlyOwner returns (bool) { require( max != rewardsMultiplierParams.max.unwrap() || overspendAdjustmentFactor != rewardsMultiplierParams.adjustmentFactors.overspend.unwrap() || @@ -374,7 +385,7 @@ contract EpochRewards is function setTargetVotingYieldParameters( uint256 max, uint256 adjustmentFactor - ) public onlyOwner onlyL1 returns (bool) { + ) public onlyOwner returns (bool) { require( max != targetVotingYieldParams.max.unwrap() || adjustmentFactor != targetVotingYieldParams.adjustmentFactor.unwrap(), @@ -396,7 +407,7 @@ contract EpochRewards is * @param targetVotingYield The relative target block reward for voters. * @return True upon success. */ - function setTargetVotingYield(uint256 targetVotingYield) public onlyOwner onlyL1 returns (bool) { + function setTargetVotingYield(uint256 targetVotingYield) public onlyOwner returns (bool) { FixidityLib.Fraction memory target = FixidityLib.wrap(targetVotingYield); require( target.lte(targetVotingYieldParams.max), @@ -443,6 +454,14 @@ contract EpochRewards is function getTargetTotalEpochPaymentsInGold() public view returns (uint256) { address stableTokenAddress = registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID); (uint256 numerator, uint256 denominator) = getSortedOracles().medianRate(stableTokenAddress); + if (isL2()) { + return + getEpochManager() + .numberOfElectedInCurrentSet() + .mul(targetValidatorEpochPayment) + .mul(denominator) + .div(numerator); + } return numberValidatorsInCurrentSet().mul(targetValidatorEpochPayment).mul(denominator).div( numerator @@ -454,7 +473,9 @@ contract EpochRewards is * @return The fraction of floating Gold being used for voting in validator elections. */ function getVotingGoldFraction() public view returns (uint256) { - uint256 liquidGold = getGoldToken().totalSupply().sub(getReserve().getReserveGoldBalance()); + uint256 liquidGold = ICeloToken(address(getCeloToken())).allocatedSupply().sub( + getReserve().getReserveGoldBalance() + ); uint256 votingGold = getElection().getTotalVotes(); return FixidityLib.newFixed(votingGold).divide(FixidityLib.newFixed(liquidGold)).unwrap(); } @@ -503,8 +524,8 @@ contract EpochRewards is uint256 targetGoldSupplyIncrease ) internal view returns (FixidityLib.Fraction memory) { uint256 targetSupply = getTargetGoldTotalSupply(); - uint256 totalSupply = getGoldToken().totalSupply(); - uint256 remainingSupply = GOLD_SUPPLY_CAP.sub(totalSupply.add(targetGoldSupplyIncrease)); + uint256 allocatedSupply = ICeloToken(address(getCeloToken())).allocatedSupply(); + uint256 remainingSupply = GOLD_SUPPLY_CAP.sub(allocatedSupply.add(targetGoldSupplyIncrease)); uint256 targetRemainingSupply = GOLD_SUPPLY_CAP.sub(targetSupply); FixidityLib.Fraction memory remainingToTargetRatio = FixidityLib .newFixed(remainingSupply) diff --git a/packages/protocol/contracts/governance/Governance.sol b/packages/protocol/contracts/governance/Governance.sol index 7d3a50e81e8..2459183314c 100644 --- a/packages/protocol/contracts/governance/Governance.sol +++ b/packages/protocol/contracts/governance/Governance.sol @@ -13,7 +13,7 @@ import "../common/Initializable.sol"; import "../common/FixidityLib.sol"; import "../common/linkedlists/IntegerSortedLinkedList.sol"; import "../common/UsingRegistry.sol"; -import "../common/UsingPrecompiles.sol"; +import "../common/PrecompilesOverride.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; import "../common/libraries/ReentrancyGuard.sol"; @@ -27,7 +27,7 @@ contract Governance is Initializable, ReentrancyGuard, UsingRegistry, - UsingPrecompiles + PrecompilesOverride { using Proposals for Proposals.Proposal; using FixidityLib for FixidityLib.Fraction; @@ -74,8 +74,10 @@ contract Governance is struct HotfixRecord { bool executed; bool approved; - uint256 preparedEpoch; - mapping(address => bool) whitelisted; + uint256 deprecated_preparedEpoch; // obsolete + mapping(address => bool) deprecated_whitelisted; // obsolete + bool councilApproved; + uint256 executionTimeLimit; } // The baseline is updated as @@ -110,6 +112,8 @@ contract Governance is uint256[] public dequeued; uint256[] public emptyIndices; ParticipationParameters private participationParameters; + address public securityCouncil; + uint256 public hotfixExecutionTimeWindow; event ApproverSet(address indexed approver); @@ -191,27 +195,56 @@ contract Governance is event HotfixWhitelisted(bytes32 indexed hash, address whitelister); - event HotfixApproved(bytes32 indexed hash); + event HotfixApproved(bytes32 indexed hash, address approver); - event HotfixPrepared(bytes32 indexed hash, uint256 indexed epoch); + event HotfixPrepared(bytes32 indexed hash, uint256 indexed executionLimit); event HotfixExecuted(bytes32 indexed hash); + event SecurityCouncilSet(address indexed council); + + event HotfixExecutionTimeWindowSet(uint256 timeDelta); + + event HotfixRecordReset(bytes32 indexed hash); + + /** + * @notice Ensures a hotfix can be executed only once. + * @param hash Hash of the hotfix. + * @dev Reverts if the hotfix has already been executed. + */ modifier hotfixNotExecuted(bytes32 hash) { require(!hotfixes[hash].executed, "hotfix already executed"); _; } + /** + * @notice Ensures function can only be called by the approver address. + */ modifier onlyApprover() { require(msg.sender == approver, "msg.sender not approver"); _; } + /** + * @notice Ensures function can only be called by the LockedGold contract. + */ modifier onlyLockedGold() { require(msg.sender == address(getLockedGold()), "msg.sender not lockedGold"); _; } + /** + * @notice Ensures a hotfix cannot be executed after the time limit. + * @param hash Hash of the hotfix. + * @dev Reverts if the hotfix time limit has been reached, or the hotfix is + * not prepared yet. + */ + modifier hotfixTimedOut(bytes32 hash) { + require(hotfixes[hash].executionTimeLimit > 0, "hotfix not prepared"); + require(hotfixes[hash].executionTimeLimit < now, "hotfix execution time limit not reached"); + _; + } + /** * @notice Sets initialized == true on implementation contracts * @param test Set to true to skip implementation initialization @@ -298,6 +331,30 @@ contract Governance is emit ConstitutionSet(destination, functionId, threshold); } + /** + * @notice Updates the address that has permission to whitelist hotfix proposals. + * @param _council The address that has permission to whitelist hotfix proposals. + */ + function setSecurityCouncil(address _council) external onlyOwner { + require(_council != address(0), "Council cannot be address zero"); + require(_council != securityCouncil, "Council unchanged"); + require(_council != approver, "Council cannot be approver"); + + securityCouncil = _council; + emit SecurityCouncilSet(_council); + } + + /** + * @notice Sets the time window during which a hotfix has to be executed. + * @param timeWindow The time (in seconds) during which a hotfix can be + * executed after it has been prepared. + */ + function setHotfixExecutionTimeWindow(uint256 timeWindow) external onlyOwner { + require(timeWindow > 0, "Execution time window cannot be zero"); + hotfixExecutionTimeWindow = timeWindow; + emit HotfixExecutionTimeWindowSet(timeWindow); + } + /** * @notice Creates a new proposal and adds it to end of the queue with no upvotes. * @param values The values of CELO to be sent in the proposed transactions. @@ -615,30 +672,63 @@ contract Governance is * @notice Approves the hash of a hotfix transaction(s). * @param hash The abi encoded keccak256 hash of the hotfix transaction(s) to be approved. */ - function approveHotfix(bytes32 hash) external hotfixNotExecuted(hash) onlyApprover { - hotfixes[hash].approved = true; - emit HotfixApproved(hash); + function approveHotfix(bytes32 hash) external hotfixNotExecuted(hash) { + require(msg.sender != address(0), "msg.sender cannot be address zero"); + require( + msg.sender == approver || msg.sender == securityCouncil, + "msg.sender not approver or Security Council" + ); + + if (msg.sender == approver) { + hotfixes[hash].approved = true; + } else { + if (isL2()) { + hotfixes[hash].councilApproved = true; + } else { + revert("Hotfix approval by security council is not available on L1."); + } + } + emit HotfixApproved(hash, msg.sender); } /** * @notice Whitelists the hash of a hotfix transaction(s). * @param hash The abi encoded keccak256 hash of the hotfix transaction(s) to be whitelisted. */ - function whitelistHotfix(bytes32 hash) external hotfixNotExecuted(hash) { - hotfixes[hash].whitelisted[msg.sender] = true; + function whitelistHotfix(bytes32 hash) external hotfixNotExecuted(hash) onlyL1 { + hotfixes[hash].deprecated_whitelisted[msg.sender] = true; emit HotfixWhitelisted(hash, msg.sender); } /** - * @notice Gives hotfix a prepared epoch for execution. + * @notice Gives hotfix a prepared epoch for execution on L1. + * @notice Gives hotfix a time limit for execution on L2. * @param hash The hash of the hotfix to be prepared. */ function prepareHotfix(bytes32 hash) external hotfixNotExecuted(hash) { - require(isHotfixPassing(hash), "hotfix not whitelisted by 2f+1 validators"); - uint256 epoch = getEpochNumber(); - require(hotfixes[hash].preparedEpoch < epoch, "hotfix already prepared for this epoch"); - hotfixes[hash].preparedEpoch = epoch; - emit HotfixPrepared(hash, epoch); + HotfixRecord storage _currentHotfix = hotfixes[hash]; + if (isL2()) { + uint256 _currentTime = now; + require(hotfixExecutionTimeWindow > 0, "Hotfix execution time window not set"); + require( + _currentHotfix.executionTimeLimit == 0, + "Hotfix already prepared for this timeframe." + ); + require(_currentHotfix.approved, "Hotfix not approved by approvers."); + require(_currentHotfix.councilApproved, "Hotfix not approved by security council."); + + _currentHotfix.executionTimeLimit = _currentTime.add(hotfixExecutionTimeWindow); + emit HotfixPrepared(hash, _currentTime.add(hotfixExecutionTimeWindow)); + } else { + require(isHotfixPassing(hash), "hotfix not whitelisted by 2f+1 validators"); + uint256 epoch = getEpochNumber(); + require( + _currentHotfix.deprecated_preparedEpoch < epoch, + "hotfix already prepared for this epoch" + ); + _currentHotfix.deprecated_preparedEpoch = epoch; + emit HotfixPrepared(hash, epoch); + } } /** @@ -657,17 +747,36 @@ contract Governance is uint256[] calldata dataLengths, bytes32 salt ) external { - bytes32 hash = keccak256(abi.encode(values, destinations, data, dataLengths, salt)); + if (isL2()) { + bytes32 hash = keccak256(abi.encode(values, destinations, data, dataLengths, salt)); + + ( + bool approved, + bool councilApproved, + bool executed, + uint256 executionTimeLimit + ) = getL2HotfixRecord(hash); + require(!executed, "hotfix already executed"); + require(approved, "hotfix not approved"); + require(councilApproved, "hotfix not approved by security council"); + require(executionTimeLimit >= now, "Execution time limit has already been reached."); + Proposals.makeMem(values, destinations, data, dataLengths, msg.sender, 0).executeMem(); + + hotfixes[hash].executed = true; + emit HotfixExecuted(hash); + } else { + bytes32 hash = keccak256(abi.encode(values, destinations, data, dataLengths, salt)); - (bool approved, bool executed, uint256 preparedEpoch) = getHotfixRecord(hash); - require(!executed, "hotfix already executed"); - require(approved, "hotfix not approved"); - require(preparedEpoch == getEpochNumber(), "hotfix must be prepared for this epoch"); + (bool approved, bool executed, uint256 preparedEpoch) = getL1HotfixRecord(hash); + require(!executed, "hotfix already executed"); + require(approved, "hotfix not approved"); + require(preparedEpoch == getEpochNumber(), "hotfix must be prepared for this epoch"); - Proposals.makeMem(values, destinations, data, dataLengths, msg.sender, 0).executeMem(); + Proposals.makeMem(values, destinations, data, dataLengths, msg.sender, 0).executeMem(); - hotfixes[hash].executed = true; - emit HotfixExecuted(hash); + hotfixes[hash].executed = true; + emit HotfixExecuted(hash); + } } /** @@ -941,7 +1050,7 @@ contract Governance is * @return Patch version of the contract. */ function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { - return (1, 4, 1, 1); + return (1, 4, 2, 0); } /** @@ -969,6 +1078,7 @@ contract Governance is function setApprover(address _approver) public onlyOwner { require(_approver != address(0), "Approver cannot be 0"); require(_approver != approver, "Approver unchanged"); + require(_approver != securityCouncil, "Approver cannot be council"); approver = _approver; emit ApproverSet(_approver); } @@ -1169,12 +1279,23 @@ contract Governance is _removeVotesWhenRevokingDelegatedVotes(account, newVotingPower); } + /** + * @notice Resets the hotfix record when after the execution time limit has elapsed. + * @param hash The abi encoded keccak256 hash of the hotfix transaction. + */ + function resetHotFixRecord(bytes32 hash) public hotfixNotExecuted(hash) hotfixTimedOut(hash) { + hotfixes[hash].approved = false; + hotfixes[hash].councilApproved = false; + hotfixes[hash].executionTimeLimit = 0; + emit HotfixRecordReset(hash); + } + /** * @notice Returns number of validators from current set which have whitelisted the given hotfix. * @param hash The abi encoded keccak256 hash of the hotfix transaction. * @return Whitelist tally */ - function hotfixWhitelistValidatorTally(bytes32 hash) public view returns (uint256) { + function hotfixWhitelistValidatorTally(bytes32 hash) public view onlyL1 returns (uint256) { uint256 tally = 0; uint256 n = numberValidatorsInCurrentSet(); IAccounts accounts = getAccounts(); @@ -1196,19 +1317,53 @@ contract Governance is * @param hash The abi encoded keccak256 hash of the hotfix transaction. * @return Whether validator whitelist tally >= validator byzantine quorum */ - function isHotfixPassing(bytes32 hash) public view returns (bool) { + function isHotfixPassing(bytes32 hash) public view onlyL1 returns (bool) { return hotfixWhitelistValidatorTally(hash) >= minQuorumSizeInCurrentSet(); } /** - * @notice Gets information about a hotfix. + * @notice Gets information about a L1 hotfix. + * @param hash The abi encoded keccak256 hash of the hotfix transaction. + * @return Hotfix approved. + * @return Hotfix executed. + * @return Hotfix preparedEpoch. + */ + function getL1HotfixRecord(bytes32 hash) public view onlyL1 returns (bool, bool, uint256) { + return ( + hotfixes[hash].approved, + hotfixes[hash].executed, + hotfixes[hash].deprecated_preparedEpoch + ); + } + + /** + * @notice Gets information about a L1 hotfix. * @param hash The abi encoded keccak256 hash of the hotfix transaction. * @return Hotfix approved. * @return Hotfix executed. * @return Hotfix preparedEpoch. + * @dev Provided for API backwards compatibility. Prefer the explicitly named + * `getL1HotfixRecord`/`getL2HotfixRecord` functions. */ function getHotfixRecord(bytes32 hash) public view returns (bool, bool, uint256) { - return (hotfixes[hash].approved, hotfixes[hash].executed, hotfixes[hash].preparedEpoch); + return getL1HotfixRecord(hash); + } + + /** + * @notice Gets information about a L2 hotfix. + * @param hash The abi encoded keccak256 hash of the hotfix transaction. + * @return Hotfix approved by approver. + * @return Hotfix approved by SecurityCouncil. + * @return Hotfix executed. + * @return Hotfix exection time limit. + */ + function getL2HotfixRecord(bytes32 hash) public view onlyL2 returns (bool, bool, bool, uint256) { + return ( + hotfixes[hash].approved, + hotfixes[hash].councilApproved, + hotfixes[hash].executed, + hotfixes[hash].executionTimeLimit + ); } /** @@ -1226,8 +1381,11 @@ contract Governance is * @param hash The abi encoded keccak256 hash of the hotfix transaction(s) to be whitelisted. * @param whitelister Address to check whitelist status of. */ - function isHotfixWhitelistedBy(bytes32 hash, address whitelister) public view returns (bool) { - return hotfixes[hash].whitelisted[whitelister]; + function isHotfixWhitelistedBy( + bytes32 hash, + address whitelister + ) public view onlyL1 returns (bool) { + return hotfixes[hash].deprecated_whitelisted[whitelister]; } /** diff --git a/packages/protocol/contracts/governance/GovernanceSlasher.sol b/packages/protocol/contracts/governance/GovernanceSlasher.sol index 682e0551d3f..916a7d2499a 100644 --- a/packages/protocol/contracts/governance/GovernanceSlasher.sol +++ b/packages/protocol/contracts/governance/GovernanceSlasher.sol @@ -5,15 +5,35 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "../common/Initializable.sol"; import "../common/UsingRegistry.sol"; +import "./interfaces/IValidators.sol"; +import "../../contracts-0.8/common/IsL2Check.sol"; +import "../common/interfaces/ICeloVersionedContract.sol"; -contract GovernanceSlasher is Ownable, Initializable, UsingRegistry { +contract GovernanceSlasher is + Ownable, + Initializable, + UsingRegistry, + ICeloVersionedContract, + IsL2Check +{ using SafeMath for uint256; // Maps a slashed address to the amount to be slashed. // Note that there is no reward paid when slashing via governance. mapping(address => uint256) slashed; + address internal slasherExecuter; event SlashingApproved(address indexed account, uint256 amount); event GovernanceSlashPerformed(address indexed account, uint256 amount); + event GovernanceSlashL2Performed(address indexed account, address indexed group, uint256 amount); + event SlasherExecuterSet(address slasherExecuter); + + modifier onlyAuthorizedToSlash() { + require( + msg.sender == owner() || slasherExecuter == msg.sender, + "Sender not authorized to slash" + ); + _; + } /** * @notice Sets initialized == true on implementation contracts @@ -30,13 +50,29 @@ contract GovernanceSlasher is Ownable, Initializable, UsingRegistry { setRegistry(registryAddress); } + /** + * @notice Returns the storage, major, minor, and patch version of the contract. + * @return Storage version of the contract. + * @return Major version of the contract. + * @return Minor version of the contract. + * @return Patch version of the contract. + */ + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { + return (1, 1, 1, 0); + } + + function setSlasherExecuter(address _slasherExecuter) external onlyOwner { + slasherExecuter = _slasherExecuter; + emit SlasherExecuterSet(_slasherExecuter); + } + /** * @notice Sets account penalty. * @param account Address that is punished. * @param penalty Amount of penalty in wei. * @dev Only callable by governance. */ - function approveSlashing(address account, uint256 penalty) external onlyOwner { + function approveSlashing(address account, uint256 penalty) external onlyAuthorizedToSlash { slashed[account] = slashed[account].add(penalty); emit SlashingApproved(account, penalty); } @@ -53,7 +89,7 @@ contract GovernanceSlasher is Ownable, Initializable, UsingRegistry { address[] calldata electionLessers, address[] calldata electionGreaters, uint256[] calldata electionIndices - ) external returns (bool) { + ) external onlyL1 returns (bool) { uint256 penalty = slashed[account]; require(penalty > 0, "No penalty given by governance"); slashed[account] = 0; @@ -70,6 +106,55 @@ contract GovernanceSlasher is Ownable, Initializable, UsingRegistry { return true; } + /** + * @notice Calls `LockedGold.slash` on `account` if `account` has an entry in `slashed`. + * @param account Account to slash + * @param electionLessers Lesser pointers for slashing locked election gold. + * @param electionGreaters Greater pointers for slashing locked election gold. + * @param electionIndices Indices of groups voted by slashed account. + */ + function slashL2( + address account, + address group, + address[] calldata electionLessers, + address[] calldata electionGreaters, + uint256[] calldata electionIndices + ) external onlyL2 onlyAuthorizedToSlash returns (bool) { + uint256 penalty = slashed[account]; + require(penalty > 0, "No penalty given by governance"); + slashed[account] = 0; + + ILockedGold lockedGold = getLockedGold(); + + lockedGold.slash( + account, + penalty, + address(0), + 0, + electionLessers, + electionGreaters, + electionIndices + ); + + if (group != address(0)) { + lockedGold.slash( + group, + penalty, + address(0), + 0, + electionLessers, + electionGreaters, + electionIndices + ); + IValidators validators = getValidators(); + validators.forceDeaffiliateIfValidator(account); + validators.halveSlashingMultiplier(group); + } + + emit GovernanceSlashL2Performed(account, group, penalty); + return true; + } + /** * @notice Gets account penalty. * @param account Address that is punished. @@ -78,4 +163,8 @@ contract GovernanceSlasher is Ownable, Initializable, UsingRegistry { function getApprovedSlashing(address account) external view returns (uint256) { return slashed[account]; } + + function getSlasherExecuter() external view returns (address) { + return slasherExecuter; + } } diff --git a/packages/protocol/contracts/governance/LockedGold.sol b/packages/protocol/contracts/governance/LockedGold.sol index 4d2db3449b2..fd9390bc41e 100755 --- a/packages/protocol/contracts/governance/LockedGold.sol +++ b/packages/protocol/contracts/governance/LockedGold.sol @@ -7,6 +7,7 @@ import "openzeppelin-solidity/contracts/utils/Address.sol"; import "openzeppelin-solidity/contracts/utils/EnumerableSet.sol"; import "./interfaces/ILockedGold.sol"; +import "./interfaces/ILockedGoldInitializer.sol"; import "../common/FixidityLib.sol"; import "../common/Initializable.sol"; @@ -14,15 +15,21 @@ import "../common/Signatures.sol"; import "../common/UsingRegistry.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; import "../common/libraries/ReentrancyGuard.sol"; +import "../common/Blockable.sol"; +/** + * @title Manages the CELO locked for use in Celo governance, election, and + * staking systems. + */ contract LockedGold is ILockedGold, + ILockedGoldInitializer, ICeloVersionedContract, ReentrancyGuard, Initializable, - UsingRegistry + UsingRegistry, + Blockable { - // TODO add initializer using SafeMath for uint256; using Address for address payable; // prettier-ignore using FixidityLib for FixidityLib.Fraction; @@ -35,13 +42,13 @@ contract LockedGold is uint256 timestamp; } - // NOTE: This contract does not store an account's locked gold that is being used in electing + // NOTE: This contract does not store an account's locked CELO that is being used in electing // validators. struct Balances { - // The amount of locked gold that this account has that is not currently participating in + // The amount of locked CELO that this account has that is not currently participating in // validator elections. uint256 nonvoting; - // Gold that has been unlocked and will become available for withdrawal. + // CELO that has been unlocked and will become available for withdrawal. PendingWithdrawal[] pendingWithdrawals; } @@ -103,6 +110,9 @@ contract LockedGold is ); event MaxDelegateesCountSet(uint256 value); + /** + * @notice Ensures a function can only be called by an authorized slasher. + */ modifier onlySlasher() { require( registry.isOneOf(slashingWhitelist, msg.sender), @@ -130,7 +140,7 @@ contract LockedGold is } /** - * @notice Locks gold to be used for voting. + * @notice Locks CELO to be used for voting. */ function lock() external payable nonReentrant { require( @@ -169,8 +179,8 @@ contract LockedGold is } /** - * @notice Unlocks gold that becomes withdrawable after the unlocking period. - * @param value The amount of gold to unlock. + * @notice Unlocks CELO that becomes withdrawable after the unlocking period. + * @param value The amount of CELO to unlock. */ function unlock(uint256 value) external nonReentrant { require( @@ -180,7 +190,7 @@ contract LockedGold is Balances storage account = balances[msg.sender]; uint256 totalLockedGold = getAccountTotalLockedGold(msg.sender); - // Prevent unlocking gold when voting on governance proposals so that the gold cannot be + // Prevent unlocking CELO when voting on governance proposals so that the CELO cannot be // used to vote more than once. uint256 remainingLockedGold = totalLockedGold.sub(value); @@ -210,7 +220,7 @@ contract LockedGold is } /** - * @notice Relocks gold that has been unlocked but not withdrawn. + * @notice Relocks CELO that has been unlocked but not withdrawn. * @param index The index of the pending withdrawal to relock from. * @param value The value to relock from the specified pending withdrawal. */ @@ -234,7 +244,7 @@ contract LockedGold is } /** - * @notice Withdraws gold that has been unlocked after the unlocking period has passed. + * @notice Withdraws CELO that has been unlocked after the unlocking period has passed. * @param index The index of the pending withdrawal to withdraw. */ function withdraw(uint256 index) external nonReentrant { @@ -254,7 +264,7 @@ contract LockedGold is } /** - * Delegates CELO to delegatee. + * @notice Delegates CELO to delegatee. * @param delegatee The delegatee account. * @param delegateFraction Fraction of total CELO that will be delegated from delegatee. Fixidity fraction */ @@ -355,7 +365,7 @@ contract LockedGold is } /** - * Revokes delegated CELO. + * @notice Revokes delegated CELO. * @param delegatee The delegatee acount. * @param revokeFraction Fraction of total CELO that will be revoked from delegatee. Fixidity fraction */ @@ -445,11 +455,11 @@ contract LockedGold is } /** - * @notice Slashes `account` by reducing its nonvoting locked gold by `penalty`. - * If there is not enough nonvoting locked gold to slash, calls into - * `Election.slashVotes` to slash the remaining gold. If `account` does not have - * `penalty` worth of locked gold, slashes `account`'s total locked gold. - * Also sends `reward` gold to the reporter, and penalty-reward to the Community Fund. + * @notice Slashes `account` by reducing its nonvoting locked CELO by `penalty`. + * If there is not enough nonvoting locked CELO to slash, calls into + * `Election.slashVotes` to slash the remaining CELO. If `account` does not have + * `penalty` worth of locked CELO, slashes `account`'s total locked CELO. + * Also sends `reward` CELO to the reporter, and penalty-reward to the Community Fund. * @param account Address of account being slashed. * @param penalty Amount to slash account. * @param reporter Address of account reporting the slasher. @@ -459,7 +469,7 @@ contract LockedGold is * @param greaters The groups receiving more votes than the i'th group, or 0 if the i'th group * has the most votes of any validator group. * @param indices The indices of the i'th group in `account`'s voting list. - * @dev Fails if `reward` is greater than `account`'s total locked gold. + * @dev Fails if `reward` is greater than `account`'s total locked CELO. */ function slash( address account, @@ -469,7 +479,7 @@ contract LockedGold is address[] calldata lessers, address[] calldata greaters, uint256[] calldata indices - ) external onlySlasher { + ) external onlySlasher onlyWhenNotBlocked { uint256 maxSlash = Math.min(penalty, getAccountTotalLockedGold(account)); require(maxSlash >= reward, "reward cannot exceed penalty."); // `reporter` receives the reward in locked CELO, so it must be given to an account @@ -488,13 +498,16 @@ contract LockedGold is require( getElection().forceDecrementVotes(account, difference, lessers, greaters, indices) == difference, - "Cannot revoke enough voting gold." + "Cannot revoke enough voting CELO." ); } // forceDecrementVotes does not increment nonvoting account balance, so we can't double count _decrementNonvotingAccountBalance(account, maxSlash.sub(difference)); _incrementNonvotingAccountBalance(reporter, reward); } + + _updateDelegatedAmount(account); + address communityFund = registry.getAddressForOrDie(GOVERNANCE_REGISTRY_ID); address payable communityFundPayable = address(uint160(communityFund)); require(maxSlash.sub(reward) <= address(this).balance, "Inconsistent balance"); @@ -503,28 +516,42 @@ contract LockedGold is } /** - * @notice Returns the total amount of locked gold in the system. Note that this does not include - * gold that has been unlocked but not yet withdrawn. - * @return The total amount of locked gold in the system. + * @notice Sets the address of the blocking contract. + * @param _blockedBy The address of the contract that will determine if this contract is blocked. + * @dev Can only be called by the owner of the contract. + */ + function setBlockedByContract(address _blockedBy) external onlyOwner { + _setBlockedBy(_blockedBy); + } + + /** + * @notice Returns the total amount of locked CELO in the system. Note that this does not include + * CELO that has been unlocked but not yet withdrawn. + * @return The total amount of locked CELO in the system. */ function getTotalLockedGold() external view returns (uint256) { return totalNonvoting.add(getElection().getTotalVotes()); } /** - * @notice Returns the total amount of locked gold not being used to vote in elections. - * @return The total amount of locked gold not being used to vote in elections. + * @notice Returns the total amount of locked CELO not being used to vote in elections. + * @return The total amount of locked CELO not being used to vote in elections. */ function getNonvotingLockedGold() external view returns (uint256) { return totalNonvoting; } + /** + * @notice Queries if the given address is an approved slasher. + * @param slasher The address to check. + * @return Whether or not the address is an approved slasher. + */ function isSlasher(address slasher) external view returns (bool) { return (registry.isOneOf(slashingWhitelist, slasher)); } /** - * Return percentage and amount that delegator assigned to delegateee. + * @notice Return percentage and amount that delegator assigned to delegateee. * Please note that amount doesn't have to be up to date. * In such case please use `updateDelegatedBalance`. * @param delegator The delegator address. @@ -544,16 +571,16 @@ contract LockedGold is } /** - * @notice Returns the total amount of non-voting locked gold for an account. + * @notice Returns the total amount of non-voting locked CELO for an account. * @param account The account. - * @return The total amount of non-voting locked gold for an account. + * @return The total amount of non-voting locked CELO for an account. */ function getAccountNonvotingLockedGold(address account) external view returns (uint256) { return balances[account].nonvoting; } /** - * @notice Returns the total amount to withdraw from unlocked gold for an account. + * @notice Returns the total amount to withdraw from unlocked CELO for an account. * @param account The address of the account. * @return Total amount to withdraw. */ @@ -566,6 +593,10 @@ contract LockedGold is return pendingWithdrawalSum; } + /** + * @notice Returns the list of approved slashers. + * @return List of Registry identifiers for the approved slashers. + */ function getSlashingWhitelist() external view returns (bytes32[] memory) { return slashingWhitelist; } @@ -616,7 +647,7 @@ contract LockedGold is } /** - * @notice Sets the duration in seconds users must wait before withdrawing gold after unlocking. + * @notice Sets the duration in seconds users must wait before withdrawing CELO after unlocking. * @param value The unlocking period in seconds. */ function setUnlockingPeriod(uint256 value) public onlyOwner { @@ -635,8 +666,8 @@ contract LockedGold is } /** - * Updates real delegated amount to delegatee. - * There might be discrepancy because of validator rewards or extra locked gold. + * @notice Updates real delegated amount to delegatee. + * There might be discrepancy because of validator rewards or extra locked CELO. * Voting power will always be smaller or equal to what it is supposed to be. * @param delegator The delegator address. * @param delegatee The delegatee address. @@ -649,8 +680,8 @@ contract LockedGold is } /** - * Updates real delegated amount to all delegator's delegatees. - * There might be discrepancy because of validator rewards or extra locked gold. + * @notice Updates real delegated amount to all delegator's delegatees. + * There might be discrepancy because of validator rewards or extra locked CELO. * @param delegator The delegator address. */ function updateDelegatedAmount(address delegator) public { @@ -668,7 +699,7 @@ contract LockedGold is } /** - * Retuns all delegatees of delegator + * @notice Retuns all delegatees of delegator * @param delegator The delegator address. */ function getDelegateesOfDelegator(address delegator) public view returns (address[] memory) { @@ -708,7 +739,7 @@ contract LockedGold is } /** - * Returns how many percents of CELO is account delegating. + * @notice Returns how many percents of CELO is account delegating. * @param account The account address. */ function getAccountTotalDelegatedFraction(address account) public view returns (uint256) { @@ -717,9 +748,9 @@ contract LockedGold is } /** - * @notice Returns the total amount of locked gold for an account. + * @notice Returns the total amount of locked CELO for an account. * @param account The account. - * @return The total amount of locked gold for an account. + * @return The total amount of locked CELO for an account. */ function getAccountTotalLockedGold(address account) public view returns (uint256) { uint256 total = balances[account].nonvoting; @@ -727,9 +758,9 @@ contract LockedGold is } /** - * @notice Returns the total amount of locked gold + delegated gold for an account. + * @notice Returns the total amount of locked CELO + delegated CELO for an account. * @param account The account. - * @return The total amount of locked gold + delegated gold for an account. + * @return The total amount of locked CELO + delegated CELO for an account. */ function getAccountTotalGovernanceVotingPower(address account) public view returns (uint256) { FixidityLib.Fraction memory availableUndelegatedPercents = FixidityLib.fixed1().subtract( @@ -746,7 +777,7 @@ contract LockedGold is } /** - * Returns expected vs real delegated amount. + * @notice Returns expected and real delegated amount. * If there is a discrepancy it can be fixed by calling `updateDelegatedAmount` function. * @param delegator The delegator address. * @param delegatee The delegatee address. @@ -767,8 +798,8 @@ contract LockedGold is } /** - * Updates real delegated amount to delegatee. - * There might be discrepancy because of validator rewards or extra locked gold. + * @notice Updates real delegated amount to delegatee. + * There might be discrepancy because of validator rewards or extra locked CELO. * Voting power will always be smaller or equal to what it is supposed to be. * @param delegator The delegator address. * @param delegatee The delegatee address. @@ -819,7 +850,7 @@ contract LockedGold is } /** - * Revokes amount during unlocking. It will revoke votes from voted proposals if necessary. + * @notice Revokes amount during unlocking. It will revoke votes from voted proposals if necessary. * @param delegator The delegator account. * @param amountToRevoke The amount to revoke. */ @@ -847,7 +878,7 @@ contract LockedGold is } /** - * Decreases delegatee voting power when removing or unlocking delegated votes. + * @notice Decreases delegatee voting power when removing or unlocking delegated votes. * @param delegatee The delegatee. * @param amountToRevoke Amount to revoke. * @param delegateeInfo Delegatee info. @@ -890,7 +921,7 @@ contract LockedGold is } /** - * Returns expected vs real delegated amount. + * @notice Returns expected and real delegated amount. * If there is a discrepancy it can be fixed by calling `updateDelegatedAmount` function. * @param delegator The delegator address. * @param delegatee The delegatee address. diff --git a/packages/protocol/contracts/governance/Proposals.sol b/packages/protocol/contracts/governance/Proposals.sol index 873145b7ad2..b7ba52d01ce 100644 --- a/packages/protocol/contracts/governance/Proposals.sol +++ b/packages/protocol/contracts/governance/Proposals.sol @@ -169,6 +169,10 @@ library Proposals { executeTransactions(proposal.transactions); } + /** + * @notice Executes a list of transactions. + * @param transactions The transactions to execute. + */ function executeTransactions(Transaction[] memory transactions) internal { for (uint256 i = 0; i < transactions.length; i = i.add(1)) { require( @@ -219,6 +223,7 @@ library Proposals { * @param dataLengths The lengths of each transaction's data. * @param proposer The proposer. * @param deposit The proposal deposit. + * @return The constructed proposal struct. */ function makeMem( uint256[] memory values, @@ -275,6 +280,7 @@ library Proposals { * @return transaction Transaction count. * @return description Description url. * @return networkWeight Network weight. + * @return Approval status. */ function unpack( Proposal storage proposal diff --git a/packages/protocol/contracts/governance/ReleaseGold.sol b/packages/protocol/contracts/governance/ReleaseGold.sol index 469e93274b3..12ee7972681 100644 --- a/packages/protocol/contracts/governance/ReleaseGold.sol +++ b/packages/protocol/contracts/governance/ReleaseGold.sol @@ -268,7 +268,7 @@ contract ReleaseGold is UsingRegistry, ReentrancyGuard, IReleaseGold, Initializa uint256 value ) external onlyWhenInProperState { require( - erc20 != registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID), + erc20 != registry.getAddressForOrDie(CELO_TOKEN_REGISTRY_ID), "Transfer must not target celo balance" ); SafeERC20.safeTransfer(IERC20(erc20), to, value); diff --git a/packages/protocol/contracts/governance/SlasherUtil.sol b/packages/protocol/contracts/governance/SlasherUtil.sol index 066bed0dc6a..9884d15e082 100644 --- a/packages/protocol/contracts/governance/SlasherUtil.sol +++ b/packages/protocol/contracts/governance/SlasherUtil.sol @@ -7,9 +7,8 @@ import "../common/Initializable.sol"; import "../common/UsingRegistry.sol"; import "../common/UsingPrecompiles.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; -import "../../contracts-0.8/common/IsL2Check.sol"; -contract SlasherUtil is Ownable, Initializable, UsingRegistry, UsingPrecompiles, IsL2Check { +contract SlasherUtil is Ownable, Initializable, UsingRegistry, UsingPrecompiles { using SafeMath for uint256; struct SlashingIncentives { diff --git a/packages/protocol/contracts/governance/interfaces/IElection.sol b/packages/protocol/contracts/governance/interfaces/IElection.sol index 5ba6aee6502..42fd7a7225f 100644 --- a/packages/protocol/contracts/governance/interfaces/IElection.sol +++ b/packages/protocol/contracts/governance/interfaces/IElection.sol @@ -29,7 +29,9 @@ interface IElection { // view functions function electValidatorSigners() external view returns (address[] memory); + function electValidatorAccounts() external view returns (address[] memory); function electNValidatorSigners(uint256, uint256) external view returns (address[] memory); + function electNValidatorAccounts(uint256, uint256) external view returns (address[] memory); function getElectableValidators() external view returns (uint256, uint256); function getElectabilityThreshold() external view returns (uint256); function getNumVotesReceivable(address) external view returns (uint256); @@ -49,6 +51,11 @@ interface IElection { uint256, uint256[] calldata ) external view returns (uint256); + function getGroupEpochRewardsBasedOnScore( + address group, + uint256 totalEpochRewards, + uint256 groupScore + ) external view returns (uint256); function getGroupsVotedForByAccount(address) external view returns (address[] memory); function getEligibleValidatorGroups() external view returns (address[] memory); function getTotalVotesForEligibleValidatorGroups() @@ -60,4 +67,5 @@ interface IElection { function hasActivatablePendingVotes(address, address) external view returns (bool); function validatorSignerAddressFromCurrentSet(uint256 index) external view returns (address); function numberValidatorsInCurrentSet() external view returns (uint256); + function owner() external view returns (address); } diff --git a/packages/protocol/contracts/governance/interfaces/IEpochRewards.sol b/packages/protocol/contracts/governance/interfaces/IEpochRewards.sol new file mode 100644 index 00000000000..1c32d669933 --- /dev/null +++ b/packages/protocol/contracts/governance/interfaces/IEpochRewards.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface IEpochRewards { + function updateTargetVotingYield() external; + function isReserveLow() external view returns (bool); + function calculateTargetEpochRewards() external view returns (uint256, uint256, uint256, uint256); + function getTargetVotingYieldParameters() external view returns (uint256, uint256, uint256); + function getRewardsMultiplierParameters() external view returns (uint256, uint256, uint256); + function getCommunityRewardFraction() external view returns (uint256); + function getCarbonOffsettingFraction() external view returns (uint256); + function getTargetVotingGoldFraction() external view returns (uint256); + function getRewardsMultiplier() external view returns (uint256); + function carbonOffsettingPartner() external view returns (address); +} diff --git a/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol b/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol new file mode 100644 index 00000000000..cfa19772a8d --- /dev/null +++ b/packages/protocol/contracts/governance/interfaces/ILockedCelo.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface ILockedCelo { + function lock() external payable; + function incrementNonvotingAccountBalance(address, uint256) external; + function decrementNonvotingAccountBalance(address, uint256) external; + + function unlock(uint256) external; + function relock(uint256, uint256) external; + function withdraw(uint256) external; + function slash( + address account, + uint256 penalty, + address reporter, + uint256 reward, + address[] calldata lessers, + address[] calldata greaters, + uint256[] calldata indices + ) external; + function addSlasher(string calldata slasherIdentifier) external; + + function getAccountNonvotingLockedGold(address account) external view returns (uint256); + function getAccountTotalLockedCelo(address) external view returns (uint256); + function getTotalLockedCelo() external view returns (uint256); + function getPendingWithdrawals( + address + ) external view returns (uint256[] memory, uint256[] memory); + function getPendingWithdrawal( + address account, + uint256 index + ) external view returns (uint256, uint256); + function getTotalPendingWithdrawals(address) external view returns (uint256); + function isSlasher(address) external view returns (bool); + + function getAccountTotalDelegatedFraction(address account) external view returns (uint256); + + function getAccountTotalGovernanceVotingPower(address account) external view returns (uint256); + function unlockingPeriod() external view returns (uint256); + function getAccountNonvotingLockedCelo(address account) external view returns (uint256); +} diff --git a/packages/protocol/contracts/governance/interfaces/LockedGoldfunctionInitializer.sol b/packages/protocol/contracts/governance/interfaces/ILockedGoldInitializer.sol similarity index 100% rename from packages/protocol/contracts/governance/interfaces/LockedGoldfunctionInitializer.sol rename to packages/protocol/contracts/governance/interfaces/ILockedGoldInitializer.sol diff --git a/packages/protocol/contracts/governance/interfaces/IValidators.sol b/packages/protocol/contracts/governance/interfaces/IValidators.sol index 6d8a0f92e28..8d771efec6c 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidators.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidators.sol @@ -7,6 +7,7 @@ interface IValidators { bytes calldata, bytes calldata ) external returns (bool); + function registerValidatorNoBls(bytes calldata ecdsaPublicKey) external returns (bool); function deregisterValidator(uint256) external returns (bool); function affiliate(address) external returns (bool); function deaffiliate() external returns (bool); @@ -29,6 +30,7 @@ interface IValidators { function setGroupLockedGoldRequirements(uint256, uint256) external returns (bool); function setValidatorLockedGoldRequirements(uint256, uint256) external returns (bool); function setSlashingMultiplierResetPeriod(uint256) external; + function setDowntimeGracePeriod(uint256 value) external; // only registered contract function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool); @@ -39,6 +41,7 @@ interface IValidators { bytes calldata, bytes calldata ) external returns (bool); + function mintStableToEpochManager(uint256 amount) external; // only VM function updateValidatorScoreFromSigner(address, uint256) external; @@ -49,6 +52,8 @@ interface IValidators { function halveSlashingMultiplier(address) external; // view functions + function maxGroupSize() external view returns (uint256); + function downtimeGracePeriod() external view returns (uint256); function getCommissionUpdateDelay() external view returns (uint256); function getValidatorScoreParameters() external view returns (uint256, uint256); function getMembershipHistory( @@ -62,6 +67,7 @@ interface IValidators { function getValidator( address account ) external view returns (bytes memory, bytes memory, address, uint256, address); + function getValidatorsGroup(address account) external view returns (address affiliation); function getValidatorGroup( address ) @@ -70,6 +76,7 @@ interface IValidators { returns (address[] memory, uint256, uint256, uint256, uint256[] memory, uint256, uint256); function getGroupNumMembers(address) external view returns (uint256); function getTopGroupValidators(address, uint256) external view returns (address[] memory); + function getTopGroupValidatorsAccounts(address, uint256) external view returns (address[] memory); function getGroupsNumMembers( address[] calldata accounts ) external view returns (uint256[] memory); @@ -85,4 +92,10 @@ interface IValidators { function getValidatorGroupSlashingMultiplier(address) external view returns (uint256); function getMembershipInLastEpoch(address) external view returns (address); function getMembershipInLastEpochFromSigner(address) external view returns (address); + function computeEpochReward( + address account, + uint256 score, + uint256 maxPayment + ) external view returns (uint256); + function getMembershipHistoryLength() external view returns (uint256); } diff --git a/packages/protocol/contracts/governance/interfaces/IValidatorsInitializer.sol b/packages/protocol/contracts/governance/interfaces/IValidatorsInitializer.sol index c8c8964e8ed..21c68bb975b 100644 --- a/packages/protocol/contracts/governance/interfaces/IValidatorsInitializer.sol +++ b/packages/protocol/contracts/governance/interfaces/IValidatorsInitializer.sol @@ -1,4 +1,5 @@ pragma solidity >=0.5.13 <0.9.0; +pragma experimental ABIEncoderV2; interface IValidatorsInitializer { function initialize( @@ -12,7 +13,14 @@ interface IValidatorsInitializer { uint256 _membershipHistoryLength, uint256 _slashingMultiplierResetPeriod, uint256 _maxGroupSize, - uint256 _commissionUpdateDelay, - uint256 _downtimeGracePeriod + InitParamsLib.InitParams calldata initParams ) external; } + +library InitParamsLib { + struct InitParams { + // The number of blocks to delay a ValidatorGroup's commission + uint256 commissionUpdateDelay; + uint256 downtimeGracePeriod; + } +} diff --git a/packages/protocol/contracts/governance/test/MockElection.sol b/packages/protocol/contracts/governance/test/MockElection.sol index 395dec5b252..b3ea2717fe2 100644 --- a/packages/protocol/contracts/governance/test/MockElection.sol +++ b/packages/protocol/contracts/governance/test/MockElection.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.5.13; +pragma solidity >=0.5.13 <0.8.20; import "../../../contracts-0.8/common/IsL2Check.sol"; @@ -9,6 +9,8 @@ contract MockElection is IsL2Check { mapping(address => bool) public isIneligible; mapping(address => bool) public isEligible; mapping(address => bool) public allowedToVoteOverMaxNumberOfGroups; + mapping(address => uint256) public groupRewardsBasedOnScore; + mapping(address => uint256) public distributedEpochRewards; address[] public electedValidators; uint256 active; uint256 total; @@ -17,7 +19,7 @@ contract MockElection is IsL2Check { isIneligible[account] = true; } - function markGroupEligible(address account, address, address) external onlyL1 { + function markGroupEligible(address account, address, address) external { isEligible[account] = true; } @@ -33,23 +35,23 @@ contract MockElection is IsL2Check { electedValidators = _electedValidators; } - function vote(address, uint256, address, address) external onlyL1 returns (bool) { + function vote(address, uint256, address, address) external pure returns (bool) { return true; } - function activate(address) external onlyL1 returns (bool) { + function activate(address) external pure returns (bool) { return true; } - function revokeAllActive(address, address, address, uint256) external returns (bool) { + function revokeAllActive(address, address, address, uint256) external pure returns (bool) { return true; } - function revokeActive(address, uint256, address, address, uint256) external returns (bool) { + function revokeActive(address, uint256, address, address, uint256) external pure returns (bool) { return true; } - function revokePending(address, uint256, address, address, uint256) external returns (bool) { + function revokePending(address, uint256, address, address, uint256) external pure returns (bool) { return true; } @@ -72,15 +74,38 @@ contract MockElection is IsL2Check { return active; } - function getTotalVotesByAccount(address) external view returns (uint256) { + function getTotalVotesByAccount(address) external pure returns (uint256) { return 0; } - function electValidatorSigners() external view returns (address[] memory) { + function electValidatorSigners() external view onlyL1 returns (address[] memory) { + return electedValidators; + } + function electValidators() external view onlyL2 returns (address[] memory) { return electedValidators; } - function setAllowedToVoteOverMaxNumberOfGroups(address account, bool flag) public onlyL1 { + function setAllowedToVoteOverMaxNumberOfGroups(address account, bool flag) public { allowedToVoteOverMaxNumberOfGroups[account] = flag; } + + function getGroupEpochRewardsBasedOnScore( + address group, + uint256, + uint256 + ) external view returns (uint256) { + return groupRewardsBasedOnScore[group]; + } + + function setGroupEpochRewardsBasedOnScore(address group, uint256 groupRewards) external { + groupRewardsBasedOnScore[group] = groupRewards; + } + + function distributeEpochRewards(address group, uint256 value, address, address) external { + distributedEpochRewards[group] = value; + } + + function electValidatorAccounts() external view returns (address[] memory) { + return electedValidators; + } } diff --git a/packages/protocol/contracts/governance/test/MockGovernance.sol b/packages/protocol/contracts/governance/test/MockGovernance.sol index 29888c3adb4..936f1a42175 100644 --- a/packages/protocol/contracts/governance/test/MockGovernance.sol +++ b/packages/protocol/contracts/governance/test/MockGovernance.sol @@ -27,22 +27,16 @@ contract MockGovernance is IGovernance { removeVotesCalledFor[account] = maxAmountAllowed; } - function setConstitution(address destination, bytes4 functionId, uint256 threshold) external { + function setConstitution(address, bytes4, uint256) external { revert("not implemented"); } - function votePartially( - uint256 proposalId, - uint256 index, - uint256 yesVotes, - uint256 noVotes, - uint256 abstainVotes - ) external returns (bool) { + function votePartially(uint256, uint256, uint256, uint256, uint256) external returns (bool) { return true; } function getProposal( - uint256 proposalId + uint256 ) external view returns (address, uint256, uint256, uint256, string memory, uint256, bool) { return (address(0), 0, 0, 0, "", 0, false); } diff --git a/packages/protocol/contracts/governance/test/MockLockedGold.sol b/packages/protocol/contracts/governance/test/MockLockedGold.sol index c0cc60c20e8..6b52c329eeb 100644 --- a/packages/protocol/contracts/governance/test/MockLockedGold.sol +++ b/packages/protocol/contracts/governance/test/MockLockedGold.sol @@ -115,10 +115,7 @@ contract MockLockedGold is ILockedGold { return totalGovernancePower[account]; } - function getPendingWithdrawal( - address account, - uint256 index - ) external view returns (uint256, uint256) { + function getPendingWithdrawal(address, uint256) external view returns (uint256, uint256) { return (0, 0); } @@ -126,7 +123,7 @@ contract MockLockedGold is ILockedGold { return 0; } - function getAccountNonvotingLockedGold(address account) external view returns (uint256) { + function getAccountNonvotingLockedGold(address) external view returns (uint256) { return 0; } diff --git a/packages/protocol/contracts/governance/test/MockValidators.sol b/packages/protocol/contracts/governance/test/MockValidators.sol index 97cf017a9a3..9b7f27d4802 100644 --- a/packages/protocol/contracts/governance/test/MockValidators.sol +++ b/packages/protocol/contracts/governance/test/MockValidators.sol @@ -2,14 +2,21 @@ pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "../interfaces/IValidators.sol"; import "../../../contracts-0.8/common/IsL2Check.sol"; +// Mocks Validators, compatible with 0.5 +// For forge tests, can be avoided with calls to deployCodeTo + /** * @title Holds a list of addresses of validators */ -contract MockValidators is IsL2Check { +contract MockValidators is IValidators, IsL2Check { using SafeMath for uint256; + event HavelSlashingMultiplierHalved(address validator); + event ValidatorDeaffiliatedCalled(address validator); + uint256 private constant FIXED1_UINT = 1000000000000000000000000; mapping(address => bool) public isValidator; @@ -19,10 +26,12 @@ contract MockValidators is IsL2Check { mapping(address => bool) private doesNotMeetAccountLockedGoldRequirements; mapping(address => address[]) private members; mapping(address => address) private affiliations; + mapping(address => uint256) private commissions; uint256 private numRegisteredValidators; + mapping(address => uint256) private epochRewards; + uint256 public mintedStable; function updateEcdsaPublicKey(address, address, bytes calldata) external returns (bool) { - allowOnlyL1(); return true; } @@ -46,7 +55,6 @@ contract MockValidators is IsL2Check { } function affiliate(address group) external returns (bool) { - allowOnlyL1(); affiliations[msg.sender] = group; return true; } @@ -61,30 +69,47 @@ contract MockValidators is IsL2Check { function setMembers(address group, address[] calldata _members) external { members[group] = _members; + for (uint256 i; i < _members.length; i++) { + affiliations[_members[i]] = group; + } + } + + function setCommission(address group, uint256 commission) external { + commissions[group] = commission; } function setAccountLockedGoldRequirement(address account, uint256 value) external { lockedGoldRequirements[account] = value; } - function halveSlashingMultiplier(address) external { - allowOnlyL1(); + function halveSlashingMultiplier(address validator) external { + emit HavelSlashingMultiplierHalved(validator); } function forceDeaffiliateIfValidator(address validator) external { - allowOnlyL1(); + emit ValidatorDeaffiliatedCalled(validator); + } + + function getValidatorsGroup(address validator) external view returns (address) { + return affiliations[validator]; } - function getTopGroupValidators( + function getTopGroupValidatorsAccounts( address group, uint256 n ) external view returns (address[] memory) { - require(n <= members[group].length); - address[] memory validators = new address[](n); - for (uint256 i = 0; i < n; i = i.add(1)) { - validators[i] = members[group][i]; - } - return validators; + return getTopGroupValidators(group, n); + } + + function getValidatorGroup( + address group + ) + external + view + returns (address[] memory, uint256, uint256, uint256, uint256[] memory, uint256, uint256) + { + uint256[] memory sizeHistory; + return (members[group], commissions[group], 0, 0, sizeHistory, 0, 0); } function getValidatorGroupSlashingMultiplier(address) external view returns (uint256) { @@ -117,11 +142,193 @@ contract MockValidators is IsL2Check { } function groupMembershipInEpoch(address addr, uint256, uint256) external view returns (address) { - allowOnlyL1(); return affiliations[addr]; } function getGroupNumMembers(address group) public view returns (uint256) { return members[group].length; } + + function getTopGroupValidators(address group, uint256 n) public view returns (address[] memory) { + require(n <= members[group].length); + address[] memory validators = new address[](n); + for (uint256 i = 0; i < n; i = i.add(1)) { + validators[i] = members[group][i]; + } + return validators; + } + + // Not implemented in mock, added here to support the interface + // without the interface, missing function erros get hard to debug + + function addFirstMember(address, address, address) external returns (bool) { + revert("Method not implemented in mock"); + } + + function registerValidatorGroup(uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function registerValidatorNoBls(bytes calldata) external returns (bool) { + revert("Method not implemented in mock"); + } + function removeMember(address) external returns (bool) { + revert("Method not implemented in mock"); + } + function setGroupLockedGoldRequirements(uint256, uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + function setMembershipHistoryLength(uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + function setNextCommissionUpdate(uint256) external { + revert("Method not implemented in mock"); + } + function setSlashingMultiplierResetPeriod(uint256) external { + revert("Method not implemented in mock"); + } + + function updateCommission() external { + revert("Method not implemented in mock"); + } + + function updateBlsPublicKey(bytes calldata, bytes calldata) external returns (bool) { + revert("Method not implemented in mock"); + } + + function setValidatorScoreParameters(uint256, uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function setValidatorLockedGoldRequirements(uint256, uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function setMaxGroupSize(uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function setDowntimeGracePeriod(uint256) external { + revert("Method not implemented in mock"); + } + + function setCommissionUpdateDelay(uint256) external { + revert("Method not implemented in mock"); + } + + function resetSlashingMultiplier() external { + revert("Method not implemented in mock"); + } + + function reorderMember(address, address, address) external returns (bool) { + revert("Method not implemented in mock"); + } + + function updateValidatorScoreFromSigner(address, uint256) external { + revert("Method not implemented in mock"); + } + + function mintStableToEpochManager(uint256 amount) external { + mintedStable = mintedStable.add(amount); + } + + function maxGroupSize() external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function getValidatorScoreParameters() external view returns (uint256, uint256) { + revert("Method not implemented in mock"); + } + + function getValidatorLockedGoldRequirements() external view returns (uint256, uint256) { + revert("Method not implemented in mock"); + } + + function getValidatorBlsPublicKeyFromSigner(address) external view returns (bytes memory) { + revert("Method not implemented in mock"); + } + + function getRegisteredValidators() external view returns (address[] memory) { + revert("Method not implemented in mock"); + } + + function getRegisteredValidatorGroups() external view returns (address[] memory) { + revert("Method not implemented in mock"); + } + + function getMembershipInLastEpochFromSigner(address) external view returns (address) { + revert("Method not implemented in mock"); + } + + function getMembershipInLastEpoch(address) external view returns (address) { + revert("Method not implemented in mock"); + } + + function getMembershipHistoryLength() external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function addMember(address) external returns (bool) { + revert("Method not implemented in mock"); + } + + function calculateEpochScore(uint256) external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function deaffiliate() external returns (bool) { + revert("Method not implemented in mock"); + } + + function deregisterValidator(uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function deregisterValidatorGroup(uint256) external returns (bool) { + revert("Method not implemented in mock"); + } + + function distributeEpochPaymentsFromSigner(address, uint256) external onlyL1 returns (uint256) { + revert("Method not implemented in mock"); + } + + function downtimeGracePeriod() external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function getCommissionUpdateDelay() external view returns (uint256) { + revert("Method not implemented in mock"); + } + + function getGroupLockedGoldRequirements() external view returns (uint256, uint256) { + revert("Method not implemented in mock"); + } + + function computeEpochReward(address account, uint256, uint256) external view returns (uint256) { + return epochRewards[account]; + } + + function setEpochRewards(address account, uint256 reward) external { + epochRewards[account] = reward; + } + + function registerValidator( + bytes calldata, + bytes calldata, + bytes calldata + ) external returns (bool) { + revert("Method not implemented in mock"); + } + + function getMembershipHistory( + address + ) external view returns (uint256[] memory, address[] memory, uint256, uint256) { + revert("Method not implemented in mock"); + } + + function getValidator( + address + ) external view returns (bytes memory, bytes memory, address, uint256, address) { + revert("Method not implemented in mock"); + } } diff --git a/packages/protocol/contracts/governance/test/ValidatorsMock.sol b/packages/protocol/contracts/governance/test/ValidatorsMock.sol deleted file mode 100644 index ab9557badec..00000000000 --- a/packages/protocol/contracts/governance/test/ValidatorsMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -pragma solidity ^0.5.13; - -import "../Validators.sol"; -import "../../common/FixidityLib.sol"; - -/** - * @title A wrapper around Validators that exposes onlyVm functions for testing. - */ -contract ValidatorsMock is Validators(true) { - function updateValidatorScoreFromSigner(address signer, uint256 uptime) external { - return _updateValidatorScoreFromSigner(signer, uptime); - } - - function distributeEpochPaymentsFromSigner( - address signer, - uint256 maxPayment - ) external returns (uint256) { - return _distributeEpochPaymentsFromSigner(signer, maxPayment); - } -} diff --git a/packages/protocol/contracts/identity/IdentityProxyHub.sol b/packages/protocol/contracts/identity/IdentityProxyHub.sol index 5c8f98be223..608c1ea6281 100644 --- a/packages/protocol/contracts/identity/IdentityProxyHub.sol +++ b/packages/protocol/contracts/identity/IdentityProxyHub.sol @@ -103,10 +103,7 @@ contract IdentityProxyHub is UsingRegistry, ICeloVersionedContract { for (uint256 i = 0; i < addresses.length; i++) { address otherAddr = addresses[i]; if (otherAddr != addr) { - (uint32 otherCompleted, uint32 _requested) = attestations.getAttestationStats( - identifier, - otherAddr - ); + (uint32 otherCompleted, ) = attestations.getAttestationStats(identifier, otherAddr); hasMostCompletions = hasMostCompletions && otherCompleted <= completed; } } diff --git a/packages/protocol/contracts/identity/Random.sol b/packages/protocol/contracts/identity/Random.sol index 81249f17540..41ffdbc1d22 100644 --- a/packages/protocol/contracts/identity/Random.sol +++ b/packages/protocol/contracts/identity/Random.sol @@ -8,7 +8,6 @@ import "../common/CalledByVm.sol"; import "../common/Initializable.sol"; import "../common/UsingPrecompiles.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; -import "../../contracts-0.8/common/IsL2Check.sol"; /** * @title Provides randomness for verifier selection @@ -19,15 +18,14 @@ contract Random is Ownable, Initializable, UsingPrecompiles, - CalledByVm, - IsL2Check + CalledByVm { using SafeMath for uint256; /* Stores most recent commitment per address */ - mapping(address => bytes32) public commitments; + mapping(address => bytes32) private deprecated_commitments; - uint256 public randomnessBlockRetentionWindow; + uint256 private deprecated_randomnessBlockRetentionWindow; mapping(uint256 => bytes32) private history; uint256 private historyFirst; @@ -79,6 +77,20 @@ contract Random is return _getBlockRandomness(block.number, block.number); } + /** + * @notice Returns the most recent commitment by a validator. + * @param addr Address of the validator. + * @return The validator's most recent commitment. + * @dev The Random system will be deprecated once Celo becomes an L2. + */ + function commitments(address addr) external view onlyL1 returns (bytes32) { + return deprecated_commitments[addr]; + } + + function randomnessBlockRetentionWindow() external view onlyL1 returns (uint256) { + return deprecated_randomnessBlockRetentionWindow; + } + /** * @notice Get randomness values of previous blocks. * @param blockNumber The number of block whose randomness value we want to know. @@ -107,7 +119,7 @@ contract Random is */ function setRandomnessBlockRetentionWindow(uint256 value) public onlyL1 onlyOwner { require(value > 0, "randomnessBlockRetetionWindow cannot be zero"); - randomnessBlockRetentionWindow = value; + deprecated_randomnessBlockRetentionWindow = value; emit RandomnessBlockRetentionWindowSet(value); } @@ -135,11 +147,11 @@ contract Random is require(newCommitment != computeCommitment(0), "cannot commit zero randomness"); // ensure revealed randomness matches previous commitment - if (commitments[proposer] != 0) { + if (deprecated_commitments[proposer] != 0) { require(randomness != 0, "randomness cannot be zero if there is a previous commitment"); bytes32 expectedCommitment = computeCommitment(randomness); require( - expectedCommitment == commitments[proposer], + expectedCommitment == deprecated_commitments[proposer], "commitment didn't match the posted randomness" ); } else { @@ -150,7 +162,7 @@ contract Random is uint256 blockNumber = block.number == 0 ? 0 : block.number.sub(1); addRandomness(block.number, keccak256(abi.encodePacked(history[blockNumber], randomness))); - commitments[proposer] = newCommitment; + deprecated_commitments[proposer] = newCommitment; } /** @@ -172,16 +184,16 @@ contract Random is if (historySize == 0) { historyFirst = blockNumber; historySize = 1; - } else if (historySize > randomnessBlockRetentionWindow) { + } else if (historySize > deprecated_randomnessBlockRetentionWindow) { deleteHistoryIfNotLastEpochBlock(historyFirst); deleteHistoryIfNotLastEpochBlock(historyFirst.add(1)); historyFirst = historyFirst.add(2); historySize = historySize.sub(1); - } else if (historySize == randomnessBlockRetentionWindow) { + } else if (historySize == deprecated_randomnessBlockRetentionWindow) { deleteHistoryIfNotLastEpochBlock(historyFirst); historyFirst = historyFirst.add(1); } else { - // historySize < randomnessBlockRetentionWindow + // historySize < deprecated_randomnessBlockRetentionWindow historySize = historySize.add(1); } } @@ -208,8 +220,8 @@ contract Random is require( blockNumber == lastEpochBlock || (blockNumber > cur.sub(historySize) && - (randomnessBlockRetentionWindow >= cur || - blockNumber > cur.sub(randomnessBlockRetentionWindow))), + (deprecated_randomnessBlockRetentionWindow >= cur || + blockNumber > cur.sub(deprecated_randomnessBlockRetentionWindow))), "Cannot query randomness older than the stored history" ); return history[blockNumber]; diff --git a/packages/protocol/contracts/stability/SortedOracles.sol b/packages/protocol/contracts/stability/SortedOracles.sol index 62c648385ef..37bfd51f964 100644 --- a/packages/protocol/contracts/stability/SortedOracles.sol +++ b/packages/protocol/contracts/stability/SortedOracles.sol @@ -5,6 +5,7 @@ import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./interfaces/ISortedOracles.sol"; +import "./interfaces/ISortedOraclesInitializer.sol"; import "../common/interfaces/ICeloVersionedContract.sol"; import "./interfaces/IBreakerBox.sol"; @@ -36,7 +37,14 @@ import "../../contracts-0.8/common/interfaces/IOracle.sol"; * "token" are actually referring to the rateFeedId. * */ -contract SortedOracles is ISortedOracles, IOracle, ICeloVersionedContract, Ownable, Initializable { +contract SortedOracles is + ISortedOracles, + ISortedOraclesInitializer, + IOracle, + ICeloVersionedContract, + Ownable, + Initializable +{ using SafeMath for uint256; using AddressSortedLinkedListWithMedian for SortedLinkedListWithMedian.List; using FixidityLib for FixidityLib.Fraction; diff --git a/packages/protocol/contracts/stability/interfaces/ISortedOracles.sol b/packages/protocol/contracts/stability/interfaces/ISortedOracles.sol index 6c06b2b300e..bed0eca9e5b 100644 --- a/packages/protocol/contracts/stability/interfaces/ISortedOracles.sol +++ b/packages/protocol/contracts/stability/interfaces/ISortedOracles.sol @@ -2,7 +2,6 @@ pragma solidity >=0.5.13 <0.9.0; interface ISortedOracles { - function initialize(uint256) external; // TODO move all the initializers to another interface in all contracts function addOracle(address, address) external; function removeOracle(address, address, uint256) external; function report(address, uint256, address, address) external; diff --git a/packages/protocol/contracts/stability/interfaces/ISortedOraclesInitializer.sol b/packages/protocol/contracts/stability/interfaces/ISortedOraclesInitializer.sol new file mode 100644 index 00000000000..a7d59679d88 --- /dev/null +++ b/packages/protocol/contracts/stability/interfaces/ISortedOraclesInitializer.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; + +interface ISortedOraclesInitializer { + function initialize(uint256) external; +} diff --git a/packages/protocol/contracts/stability/test/MockSortedOracles.sol b/packages/protocol/contracts/stability/test/MockSortedOracles.sol index d19ce120b89..5c212954acd 100644 --- a/packages/protocol/contracts/stability/test/MockSortedOracles.sol +++ b/packages/protocol/contracts/stability/test/MockSortedOracles.sol @@ -1,9 +1,10 @@ pragma solidity >=0.5.13 <0.9.0; +import "../../../contracts-0.8/common/interfaces/IOracle.sol"; /** * @title A mock SortedOracles for testing. */ -contract MockSortedOracles { +contract MockSortedOracles is IOracle { uint256 public constant DENOMINATOR = 1000000000000000000000000; mapping(address => uint256) public numerators; mapping(address => uint256) public medianTimestamp; @@ -32,17 +33,23 @@ contract MockSortedOracles { return _numRates[token]; } - function medianRate(address token) external view returns (uint256, uint256) { - if (numerators[token] > 0) { - return (numerators[token], DENOMINATOR); - } - return (0, 0); + function getExchangeRate( + address token + ) external view returns (uint256 numerator, uint256 denominator) { + (numerator, denominator) = medianRate(token); } function setOldestReportExpired(address token) public { expired[token] = true; } + function medianRate(address token) public view returns (uint256, uint256) { + if (numerators[token] > 0) { + return (numerators[token], DENOMINATOR); + } + return (0, 0); + } + function isOldestReportExpired(address token) public view returns (bool, address) { return (expired[token], token); } diff --git a/packages/protocol/foundry.toml b/packages/protocol/foundry.toml index b6a500daa6e..c934ff7bbbe 100644 --- a/packages/protocol/foundry.toml +++ b/packages/protocol/foundry.toml @@ -1,30 +1,44 @@ [profile.default] -src = 'contracts' +src = 'contracts-0.8' out = 'out' test = 'test-sol' libs = ['lib', 'node_modules'] remappings = [ + '@celo-contracts/=contracts/', + '@celo-contracts-8=contracts-0.8/', + '@mento-core/=lib/mento-core', 'openzeppelin-solidity/=lib/openzeppelin-contracts/', - 'solidity-bytes-utils/=lib/solidity-bytes-utils/', - 'forge-std/=lib/celo-foundry/lib/forge-std/src/', - 'ds-test/=lib/celo-foundry/lib/forge-std/lib/ds-test/src/', + '@openzeppelin/contracts8/=lib/openzeppelin-contracts8/contracts/', 'celo-foundry/=lib/celo-foundry/src/', - '@summa-tx/memview.sol/=lib/memview.sol', 'celo-foundry-8/=lib/celo-foundry-8/src/', + '@test-sol/=test-sol/', + '@migrations-sol/=migrations_sol/', + 'forge-std/=lib/celo-foundry/lib/forge-std/src/', 'forge-std-8/=lib/celo-foundry-8/lib/forge-std/src/', - '@celo-contracts-8=contracts-0.8/', - '@openzeppelin/contracts8/=lib/openzeppelin-contracts8/contracts/', - '@celo-contracts/=contracts/', - '@celo-migrations/=migrations_sol/' + '@summa-tx/memview.sol/=lib/memview.sol', + 'solidity-bytes-utils/=lib/solidity-bytes-utils/', + 'solidity-bytes-utils-8/=lib/solidity-bytes-utils-8/', + 'ds-test/=lib/celo-foundry/lib/forge-std/lib/ds-test/src/', ] no_match_test = "skip" -# `BLS12Passthrough.sol` is excluded, because it's tested in the celo-blockain repo as -# described here: https://github.com/celo-org/celo-monorepo/pull/10240 -# `Random.sol` is excluded, but I'm not sure why. It was already excluded so I'm leaving it here. -no_match_path = "*test/{BLS12Passthrough.sol,RandomTest.sol}" +# 1. `BLS12Passthrough.sol` is excluded, because it's tested in the celo-blockain repo as described here: https://github.com/celo-org/celo-monorepo/pull/10240 +# 2. `Random.sol` is excluded, but I'm not sure why. It was already excluded so I'm leaving it here. +# 3. `test-sol/devchain/` tests are excluded because they require an anvil devchain to be serving at a localhost +# Helper: Test glob patterns here: https://globster.xyz/ +no_match_path = "{**/test/BLS12Passthrough.sol,**/test/RandomTest.sol,**/test-sol/devchain/**}" + +fs_permissions = [ + { access = "read", path = "./out"}, + { access = "read", path = "./migrations_sol/migrationsConfig.json"}, + { access = "read", path = "./governanceConstitution.json"}, + { access = "read", path = "./artifacts/"} + ] -fs_permissions = [{ access = "read", path = "./out"}, { access = "read", path = "./migrations_sol/migrationsConfig.json"}, { access = "read", path = "./governanceConstitution.json"}] +[profile.devchain] # Special profile for the tests that require an anvil devchain +test = 'test-sol/devchain' +match_path = "**/test-sol/devchain/**" +no_match_path = "{**/test/BLS12Passthrough.sol,**/test/RandomTest.sol}" # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/packages/protocol/governanceConstitution.js b/packages/protocol/governanceConstitution.js index 601bda35862..fa0e0ea0be7 100644 --- a/packages/protocol/governanceConstitution.js +++ b/packages/protocol/governanceConstitution.js @@ -135,6 +135,7 @@ const DefaultConstitution = { setValidatorLockedGoldRequirements: 0.8, setSlashingMultiplierResetPeriod: 0.7, setValidatorScoreParameters: 0.7, + __contractPackage: contractPackages.SOLIDITY_08_PACKAGE, }, } diff --git a/packages/protocol/lib/artifactsSingleton.ts b/packages/protocol/lib/artifactsSingleton.ts index 3d475431836..03de766b5ff 100644 --- a/packages/protocol/lib/artifactsSingleton.ts +++ b/packages/protocol/lib/artifactsSingleton.ts @@ -6,33 +6,37 @@ export interface ArtifactSet { getProxy(key: string): any; } -function getProxyName(contractName:string){ +function getProxyName(contractName: string) { return contractName + "Proxy"; } // This class is meant to be used to wrap truffle artifacts // and extend its interface. // ArtifactsSingleton.wrap returns an instance of DefaultArtifact -export class DefaultArtifact implements ArtifactSet{ +export class DefaultArtifact implements ArtifactSet { public artifacts: any - + public constructor(artifacts) { this.artifacts = artifacts } - + public require(key: string) { return this.artifacts.require(key) } - + public getProxy(key: string) { return this.require(getProxyName(key)) } - + + public contains(key: string) { + return this.artifacts.require(key) !== undefined + } + } // This objects replicates a Truffle `artifacts.require` singleton // but constructed manually -export class ArtifactsSingleton implements ArtifactSet{ +export class ArtifactsSingleton implements ArtifactSet { public static setNetwork(network: any) { this.network = network } @@ -56,8 +60,8 @@ export class ArtifactsSingleton implements ArtifactSet{ return ArtifactsSingleton.instances[namespace] } - public static wrap(artifacts:any){ - if (artifacts instanceof ArtifactsSingleton || artifacts instanceof DefaultArtifact){ + public static wrap(artifacts: any) { + if (artifacts instanceof ArtifactsSingleton || artifacts instanceof DefaultArtifact) { return artifacts } @@ -70,22 +74,29 @@ export class ArtifactsSingleton implements ArtifactSet{ public artifacts: { [key: string]: any } = {} - private constructor() {} + private constructor() { } public addArtifact(key: string, value: any) { this.artifacts[key] = value } - public require(key: string) { - return this.artifacts[key] + public require(key: string, defaultArtifacts?: any) { + if (key in this.artifacts) { + return this.artifacts[key] + } + return defaultArtifacts?.require(key) + } + + public contains(key: string) { + return key in this.artifacts } - public getProxy(key: string, defaultArtifacts?:any) { + public getProxy(key: string, defaultArtifacts?: any) { const proxyArtifactName = getProxyName(key) const toReturn = this.require(proxyArtifactName) - if (toReturn === undefined){ + if (toReturn === undefined) { // in case the package of this artifact has proxiesPath set // this needs to be changed to support it, now only "/" path is supported return defaultArtifacts?.require(proxyArtifactName) diff --git a/packages/protocol/lib/compatibility/ast-layout.ts b/packages/protocol/lib/compatibility/ast-layout.ts index c9c79205945..a2897e9ae2d 100644 --- a/packages/protocol/lib/compatibility/ast-layout.ts +++ b/packages/protocol/lib/compatibility/ast-layout.ts @@ -42,7 +42,7 @@ export const getLayout = (artifact: Artifact, artifacts: BuildArtifacts) => { const selectIncompatibleOperations = (diff: Operation[]) => diff.filter(operation => operation.action !== 'append' - && !(operation.action === 'rename' && (`deprecated_${operation.original.label}` === operation.updated.label && operation.original.type === operation.updated.type))) + && !(operation.action === 'rename' && (((`deprecated_${operation.original.label}` === operation.updated.label) || (`ignoreRenaming_` === operation.updated.label.slice(0, 15))) && operation.original.type === operation.updated.type))) export interface ASTStorageCompatibilityReport { contract: string @@ -199,6 +199,14 @@ export const generateCompatibilityReport = (oldArtifact: Artifact, oldArtifacts: const layoutReport = generateLayoutCompatibilityReport(oldLayout, newLayout) const structsReport = generateStructsCompatibilityReport(oldLayout, newLayout) + if (!layoutReport.compatible) { + console.log(newArtifact.contractName, "layoutReport incompatible", JSON.stringify(layoutReport.errors)); + } + + if (!structsReport.compatible) { + console.log(newArtifact.contractName, "structsReport incompatible", JSON.stringify(structsReport.errors)); + } + return { contract: newArtifact.contractName, compatible: layoutReport.compatible && structsReport.compatible, diff --git a/packages/protocol/lib/compatibility/utils.ts b/packages/protocol/lib/compatibility/utils.ts index e051619c25f..fb640c71be2 100644 --- a/packages/protocol/lib/compatibility/utils.ts +++ b/packages/protocol/lib/compatibility/utils.ts @@ -1,11 +1,11 @@ -import { reportASTIncompatibilities } from '@celo/protocol/lib/compatibility/ast-code' -import { reportLayoutIncompatibilities } from '@celo/protocol/lib/compatibility/ast-layout' -import { Categorizer } from '@celo/protocol/lib/compatibility/categorizer' -import { reportLibraryLinkingIncompatibilities } from '@celo/protocol/lib/compatibility/library-linking' -import { ASTDetailedVersionedReport, ASTReports } from '@celo/protocol/lib/compatibility/report' -import { linkedLibraries } from '@celo/protocol/migrationsConfig' -import { BuildArtifacts, Contracts, getBuildArtifacts } from '@openzeppelin/upgrades' -import { readJsonSync } from 'fs-extra' +import { reportASTIncompatibilities } from '@celo/protocol/lib/compatibility/ast-code'; +import { reportLayoutIncompatibilities } from '@celo/protocol/lib/compatibility/ast-layout'; +import { Categorizer } from '@celo/protocol/lib/compatibility/categorizer'; +import { reportLibraryLinkingIncompatibilities } from '@celo/protocol/lib/compatibility/library-linking'; +import { ASTDetailedVersionedReport, ASTReports } from '@celo/protocol/lib/compatibility/report'; +import { linkedLibraries } from '@celo/protocol/migrationsConfig'; +import { BuildArtifacts, Contracts, getBuildArtifacts } from '@openzeppelin/upgrades'; +import { readJsonSync } from 'fs-extra'; @@ -16,7 +16,7 @@ import { readJsonSync } from 'fs-extra' export class ASTBackwardReport { static create = ( - oldArtifactsFolder: string, + oldArtifactsFolders: string[], newArtifactsFolders: string[], oldArtifacts: BuildArtifacts[], newArtifacts: BuildArtifacts[], @@ -44,14 +44,14 @@ export class ASTBackwardReport { logFunction("Done\n") return new ASTBackwardReport( - oldArtifactsFolder, + oldArtifactsFolders, newArtifactsFolders, exclude.toString(), versionedReport) } constructor( - public readonly oldArtifactsFolder: string, + public readonly oldArtifactsFolder: string[], public readonly newArtifactsFolder: string[], public readonly exclude: string, public readonly report: ASTDetailedVersionedReport diff --git a/packages/protocol/lib/compatibility/verify-bytecode.ts b/packages/protocol/lib/compatibility/verify-bytecode.ts index b5dfdb59f26..56d6d9d0ee8 100644 --- a/packages/protocol/lib/compatibility/verify-bytecode.ts +++ b/packages/protocol/lib/compatibility/verify-bytecode.ts @@ -21,7 +21,12 @@ let ignoredContracts = [ // These contracts are not in the Registry (before release 1) 'ReserveSpenderMultiSig', - 'GovernanceApproverMultiSig' + 'GovernanceApproverMultiSig', + + // These contracts live in monorepo but are not part of the core protocol + 'CeloFeeCurrencyAdapterOwnable', + 'FeeCurrencyAdapter', + 'FeeCurrencyAdapterOwnable', ] interface VerificationContext { diff --git a/packages/protocol/lib/proxy-utils.ts b/packages/protocol/lib/proxy-utils.ts index a2bd042271e..a24bd1fe688 100644 --- a/packages/protocol/lib/proxy-utils.ts +++ b/packages/protocol/lib/proxy-utils.ts @@ -42,27 +42,33 @@ export async function setAndInitializeImplementation( }, ...args: any[] ) { - const callData = web3.eth.abi.encodeFunctionCall(initializerAbi, args) - if (txOptions.from != null) { - // The proxied contract needs to be funded prior to initialization - if (txOptions.value != null) { - // Proxy's fallback fn expects the contract's implementation to be set already - // So we set the implementation first, send the funding, and then set and initialize again. - await retryTx(proxy._setImplementation, [implementationAddress, { from: txOptions.from }]) - await retryTx(web3.eth.sendTransaction, [ - { - from: txOptions.from, - to: proxy.address, - value: txOptions.value, - }, + try { + + + const callData = web3.eth.abi.encodeFunctionCall(initializerAbi, args) + if (txOptions.from != null) { + // The proxied contract needs to be funded prior to initialization + if (txOptions.value != null) { + // Proxy's fallback fn expects the contract's implementation to be set already + // So we set the implementation first, send the funding, and then set and initialize again. + await retryTx(proxy._setImplementation, [implementationAddress, { from: txOptions.from }]) + await retryTx(web3.eth.sendTransaction, [ + { + from: txOptions.from, + to: proxy.address, + value: txOptions.value, + }, + ]) + } + return retryTx(proxy._setAndInitializeImplementation, [ + implementationAddress, + callData as any, + { from: txOptions.from }, ]) + } else { + return retryTx(proxy._setAndInitializeImplementation, [implementationAddress, callData as any]) } - return retryTx(proxy._setAndInitializeImplementation, [ - implementationAddress, - callData as any, - { from: txOptions.from }, - ]) - } else { - return retryTx(proxy._setAndInitializeImplementation, [implementationAddress, callData as any]) + } catch (error) { + console.log("errror", error); } } diff --git a/packages/protocol/lib/registry-utils.ts b/packages/protocol/lib/registry-utils.ts index 9b58e47cecd..36777a976ef 100644 --- a/packages/protocol/lib/registry-utils.ts +++ b/packages/protocol/lib/registry-utils.ts @@ -14,10 +14,14 @@ export enum CeloContractName { Accounts = 'Accounts', Attestations = 'Attestations', BlockchainParameters = 'BlockchainParameters', + CeloToken = 'CeloToken', DoubleSigningSlasher = 'DoubleSigningSlasher', DowntimeSlasher = 'DowntimeSlasher', Election = 'Election', EpochRewards = 'EpochRewards', + EpochManagerEnabler = 'EpochManagerEnabler', + EpochManager = 'EpochManager', + ScoreManager = 'ScoreManager', Escrow = 'Escrow', Exchange = 'Exchange', ExchangeEUR = 'ExchangeEUR', @@ -35,7 +39,8 @@ export enum CeloContractName { GovernanceApproverMultiSig = 'GovernanceApproverMultiSig', GrandaMento = 'GrandaMento', LockedGold = 'LockedGold', - MintGoldSchedule = 'MintGoldSchedule', + LockedCelo = 'LockedCelo', + CeloUnreleasedTreasury = 'CeloUnreleasedTreasury', OdisPayments = 'OdisPayments', Random = 'Random', Reserve = 'Reserve', @@ -68,7 +73,7 @@ export const hasEntryInRegistry: ContractPackage[] = [ CeloContractName.FederatedAttestations, CeloContractName.FeeCurrencyWhitelist, CeloContractName.Freezer, - CeloContractName.GoldToken, + CeloContractName.GoldToken, //TODO: Update when contract name is changed. CeloContractName.GovernanceSlasher, CeloContractName.OdisPayments, CeloContractName.Random, diff --git a/packages/protocol/lib/solidity-bytes-utils-8 b/packages/protocol/lib/solidity-bytes-utils-8 new file mode 160000 index 00000000000..df88556cbbc --- /dev/null +++ b/packages/protocol/lib/solidity-bytes-utils-8 @@ -0,0 +1 @@ +Subproject commit df88556cbbc267b33a787a3a6eaa32fd7247b589 diff --git a/packages/protocol/lib/web3-utils.ts b/packages/protocol/lib/web3-utils.ts index 127c09ff6d5..ebaae5d256a 100644 --- a/packages/protocol/lib/web3-utils.ts +++ b/packages/protocol/lib/web3-utils.ts @@ -207,13 +207,17 @@ export async function _setInitialProxyImplementation< return receipt.tx } +export const getProxiedContract = async (contractName: string, contractPackage: ContractPackage) => { + const artifactsObject = ArtifactsSingleton.getInstance(contractPackage, artifacts) + /* eslint-disable-next-line */ + return await getDeployedProxiedContract(contractName, artifactsObject) +} + export async function getDeployedProxiedContract( contractName: string, customArtifacts: any ): Promise { - const Contract: Truffle.Contract = customArtifacts.require(contractName) - let Proxy: ProxyContract // this wrap avoids a lot of rewrite const overloadedArtifact = ArtifactsSingleton.wrap(customArtifacts) @@ -271,11 +275,11 @@ export const makeTruffleContractForMigrationWithoutSingleton = (contractName: st const artifact = require(`${path.join(__dirname, "..")}/build/contracts-${contractPath}/${contractName}.json`) const Contract = truffleContract({ + contractName: artifact.contractName, abi: artifact.abi, unlinked_binary: artifact.bytecode, }) - Contract.setProvider(web3.currentProvider) Contract.setNetwork(network.network_id) @@ -292,9 +296,14 @@ export const makeTruffleContractForMigrationWithoutSingleton = (contractName: st export const makeTruffleContractForMigration = (contractName: string, contractPath: ContractPackage, web3: Web3) => { + const singleton = ArtifactsSingleton.getInstance(contractPath) + if (singleton.contains(contractName)) { + return singleton.require(contractName) + } + const network = ArtifactsSingleton.getNetwork() const Contract = makeTruffleContractForMigrationWithoutSingleton(contractName, network, contractPath.name, web3) - ArtifactsSingleton.getInstance(contractPath).addArtifact(contractName, Contract) + singleton.addArtifact(contractName, Contract) return Contract } @@ -382,7 +391,7 @@ export async function transferOwnershipOfProxy( export async function transferOwnershipOfProxyAndImplementation< ContractInstance extends OwnableInstance >(contractName: string, owner: string, artifacts: any) { - console.info(` Transferring ownership of ${contractName} and its Proxy to ${owner}`) + console.info(`Transferring ownership of ${contractName} and its Proxy to ${owner}`) const contract: ContractInstance = await getDeployedProxiedContract( contractName, artifacts diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index 1522bd1be88..79045a72c5d 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -77,6 +77,9 @@ const DefaultConfig = { carbonOffsettingFraction: 1 / 1000, frozen: false, }, + epochManager: { + newEpochDuration: 100, + }, exchange: { spread: 5 / 1000, reserveFraction: 1 / 100, @@ -165,6 +168,9 @@ const DefaultConfig = { numRequiredConfirmations: 1, numInternalRequiredConfirmations: 1, }, + scoreManager: { + newEpochDuration: 100, + }, stableToken: { decimals: 18, goldPrice: 1, @@ -581,7 +587,7 @@ NetworkConfigs.mainnet = NetworkConfigs.rc1 const linkedLibraries = { Proposals: ['Governance'], - AddressLinkedList: ['Validators', 'ValidatorsMock'], + AddressLinkedList: ['Validators'], AddressSortedLinkedList: ['Election', 'ElectionTest'], IntegerSortedLinkedList: ['Governance', 'IntegerSortedLinkedListMock'], AddressSortedLinkedListWithMedian: ['SortedOracles', 'AddressSortedLinkedListWithMedianMock'], diff --git a/packages/protocol/migrations_sol/CONTRIBUTING.md b/packages/protocol/migrations_sol/CONTRIBUTING.md new file mode 100644 index 00000000000..6fcbdbedfae --- /dev/null +++ b/packages/protocol/migrations_sol/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Anvil migrations + +The "Anvil migrations" are a set of scripts that generate a local anvil-based devchain. +This devchain is useful for testing and development of the Celo protocol. + +## Usage + +### Start L1 devchain + +```sh +$ yarn anvil-devchain:start-L1 +``` + +Starts a new anvil devchain serving at localhost (default port 8546). + +You can now run commands against the local devchain. For example: + +```sh +cast block-number \ +--rpc-url http://127.0.0.1:8546 +266 +``` + +### Start L2 devchain + +```sh +$ yarn anvil-devchain:start-L2 +``` + +Starts a new anvil devchain serving at localhost (default port 8546). + +You can now run commands against the local devchain. For example: + +```sh +# Call `isL2()` on `CeloUnreleasedTreasury.sol` +cast call \ +0xA16cF67AFa80BB9Ce7a325597F80057c6B290fD4 \ +"isL2()(bool)" \ +--rpc-url=http://127.0.0.1:8546 +true +``` + +### Check if devchain is running + +```sh +$ yarn anvil-devchain:status + +# If devchain is running +Devchain is serving at http://localhost:8546 + +# If devchain is not running +Devchain is not running. +``` + +### Stop devchain + +```sh +$ yarn anvil-devchain:stop +``` + +Terminates any anvil nodes serving at localhost. For example: + +```sh +$ yarn anvil-devchain:stop + +Killed Anvil +``` \ No newline at end of file diff --git a/packages/protocol/migrations_sol/HelperInterFaces.sol b/packages/protocol/migrations_sol/HelperInterFaces.sol index ace2cd69a0f..486aa976929 100644 --- a/packages/protocol/migrations_sol/HelperInterFaces.sol +++ b/packages/protocol/migrations_sol/HelperInterFaces.sol @@ -49,3 +49,17 @@ interface IExchangeInitializer { interface IExchange { function activateStable() external; } + +interface IReserveSpenderMultiSig { + /** + @dev Contract constructor sets initial owners and required number of confirmations. + @param _owners List of initial owners. + @param _required Number of required confirmations for external transactions. + @param _internalRequired Number of required confirmations for internal transactions. + */ + function initialize( + address[] calldata _owners, + uint256 _required, + uint256 _internalRequired + ) external; +} diff --git a/packages/protocol/migrations_sol/Migration.s.sol b/packages/protocol/migrations_sol/Migration.s.sol index 8913154eeb1..aaa9303e3fa 100644 --- a/packages/protocol/migrations_sol/Migration.s.sol +++ b/packages/protocol/migrations_sol/Migration.s.sol @@ -4,51 +4,60 @@ pragma solidity >=0.8.7 <0.8.20; import { Script } from "forge-std-8/Script.sol"; +// Foundry imports import "forge-std/console.sol"; import "forge-std/StdJson.sol"; -import "@celo-contracts/common/interfaces/IProxyFactory.sol"; +// Helper contract imports +import "@migrations-sol/HelperInterFaces.sol"; +import { MigrationsConstants } from "@migrations-sol/constants.sol"; +import "@openzeppelin/contracts8/utils/math/Math.sol"; + +// Core contract imports on Solidity 0.5 import "@celo-contracts/common/interfaces/IProxy.sol"; +import "@celo-contracts/common/interfaces/IProxyFactory.sol"; import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/common/interfaces/IRegistryInitializer.sol"; import "@celo-contracts/common/interfaces/IFreezer.sol"; -import "@celo-contracts/common/interfaces/IFeeCurrencyWhitelist.sol"; -import "@celo-contracts-8/common/FeeCurrencyDirectory.sol"; -import "@celo-contracts/common/interfaces/ICeloToken.sol"; // TODO move these to Initializer +import "@celo-contracts/common/interfaces/IFreezerInitializer.sol"; +import "@celo-contracts/common/interfaces/ICeloTokenInitializer.sol"; import "@celo-contracts/common/interfaces/IAccountsInitializer.sol"; +import "@celo-contracts/common/interfaces/IFeeHandlerSellerInitializer.sol"; +import "@celo-contracts/common/interfaces/IFeeHandler.sol"; +import "@celo-contracts/common/interfaces/IFeeHandlerInitializer.sol"; +import "@celo-contracts/common/interfaces/IFeeCurrencyWhitelist.sol"; import "@celo-contracts/common/interfaces/IAccounts.sol"; -import "@celo-contracts/governance/interfaces/LockedGoldfunctionInitializer.sol"; +import "@celo-contracts/common/interfaces/IEpochManagerEnabler.sol"; +import "@celo-contracts/governance/interfaces/ILockedGoldInitializer.sol"; import "@celo-contracts/governance/interfaces/IValidatorsInitializer.sol"; import "@celo-contracts/governance/interfaces/IElectionInitializer.sol"; import "@celo-contracts/governance/interfaces/IEpochRewardsInitializer.sol"; import "@celo-contracts/governance/interfaces/IBlockchainParametersInitializer.sol"; -import "@celo-contracts/governance/interfaces/ILockedGold.sol"; -import "@celo-contracts/common/interfaces/IAccounts.sol"; import "@celo-contracts/governance/interfaces/IGovernanceSlasherInitializer.sol"; import "@celo-contracts/governance/interfaces/IDoubleSigningSlasherInitializer.sol"; import "@celo-contracts/governance/interfaces/IDowntimeSlasherInitializer.sol"; import "@celo-contracts/governance/interfaces/IGovernanceApproverMultiSigInitializer.sol"; import "@celo-contracts/governance/interfaces/IGovernanceInitializer.sol"; - +import "@celo-contracts/governance/interfaces/ILockedGold.sol"; import "@celo-contracts/governance/interfaces/IGovernance.sol"; - -import "@celo-contracts/common/interfaces/IFeeHandlerSellerInitializer.sol"; -import "@celo-contracts/common/interfaces/IFeeHandlerInitializer.sol"; -import "@celo-contracts/common/interfaces/IFeeHandler.sol"; - import "@celo-contracts/identity/interfaces/IRandomInitializer.sol"; import "@celo-contracts/identity/interfaces/IEscrowInitializer.sol"; import "@celo-contracts/identity/interfaces/IOdisPaymentsInitializer.sol"; import "@celo-contracts/identity/interfaces/IFederatedAttestationsInitializer.sol"; +import "@celo-contracts/stability/interfaces/ISortedOraclesInitializer.sol"; import "@celo-contracts/stability/interfaces/ISortedOracles.sol"; -import "@celo-contracts-8/common/interfaces/IGasPriceMinimumInitializer.sol"; -import "@celo-contracts-8/common/interfaces/IMintGoldScheduleInitializer.sol"; - -import "./HelperInterFaces.sol"; -import "@openzeppelin/contracts8/utils/math/Math.sol"; +// Core contract imports on Solidity 0.8 +import "@celo-contracts-8/common/interfaces/IFeeCurrencyDirectoryInitializer.sol"; +import "@celo-contracts-8/common/interfaces/IGasPriceMinimumInitializer.sol"; +import "@celo-contracts-8/common/interfaces/ICeloUnreleasedTreasuryInitializer.sol"; +import "@celo-contracts-8/common/interfaces/IEpochManagerEnablerInitializer.sol"; +import "@celo-contracts-8/common/interfaces/IEpochManagerInitializer.sol"; +import "@celo-contracts-8/common/interfaces/IScoreManagerInitializer.sol"; +import "@celo-contracts-8/common/interfaces/IFeeCurrencyDirectory.sol"; import "@celo-contracts-8/common/UsingRegistry.sol"; -import { Constants } from "@celo-migrations/constants.sol"; +import "@test-sol/utils/SECP256K1.sol"; contract ForceTx { // event to trigger so a tx can be processed @@ -61,18 +70,18 @@ contract ForceTx { } } -contract Migration is Script, UsingRegistry, Constants { +contract Migration is Script, UsingRegistry, MigrationsConstants { using stdJson for string; - /** - * This is Anvil's default account - */ - address constant deployerAccount = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + struct InitParamsTunnel { + // The number of blocks to delay a ValidatorGroup's commission + uint256 commissionUpdateDelay; + uint256 downtimeGracePeriod; + } IProxyFactory proxyFactory; uint256 proxyNonce = 0; - address constant registryAddress = address(0x000000000000000000000000000000000000ce10); event Result(bytes); @@ -113,12 +122,12 @@ contract Migration is Script, UsingRegistry, Constants { } function addToRegistry(string memory contractName, address proxyAddress) public { - IProxy proxy = IProxy(registryAddress); + IProxy proxy = IProxy(REGISTRY_ADDRESS); if (proxy._getImplementation() == address(0)) { console.log("Can't add to registry because implementation not set"); return; } - registry = IRegistry(registryAddress); + registry = IRegistry(REGISTRY_ADDRESS); console.log(" Setting on the registry contract:", contractName); registry.setAddressFor(contractName, proxyAddress); } @@ -188,8 +197,8 @@ contract Migration is Script, UsingRegistry, Constants { * Entry point of the script */ function run() external { - // TODO check that this matches deployerAccount and the pK can be avoided with --unlock - vm.startBroadcast(deployerAccount); + // TODO check that this matches DEPLOYER_ACCOUNT and the pK can be avoided with --unlock + vm.startBroadcast(DEPLOYER_ACCOUNT); string memory json = vm.readFile("./migrations_sol/migrationsConfig.json"); @@ -199,24 +208,20 @@ contract Migration is Script, UsingRegistry, Constants { // Proxy for Registry is already set, just deploy implementation migrateRegistry(); - - // Foloowing lines required by parent UsingRegistry - _transferOwnership(deployerAccount); - setRegistry(registryAddress); - - // End UsingRegistry setup + setupUsingRegistry(); migrateFreezer(); migrateFeeCurrencyWhitelist(); migrateFeeCurrencyDirectory(); - migrateGoldToken(json); + migrateCeloToken(json); migrateSortedOracles(json); migrateGasPriceMinimum(json); + migrateReserveSpenderMultiSig(json); migrateReserve(json); migrateStableToken(json); migrateExchange(json); migrateAccount(); - migrateLockedGold(json); + migrateLockedCelo(json); migrateValidators(json); // this triggers a revert, the deploy after the json reads migrateElection(json); migrateEpochRewards(json); @@ -234,7 +239,10 @@ contract Migration is Script, UsingRegistry, Constants { migrateUniswapFeeHandlerSeller(); migrateFeeHandler(json); migrateOdisPayments(); - migrateMintGoldSchedule(); + migrateCeloUnreleasedTreasury(); + migrateEpochManagerEnabler(); + migrateEpochManager(json); + migrateScoreManager(); migrateGovernance(json); vm.stopBroadcast(); @@ -242,27 +250,42 @@ contract Migration is Script, UsingRegistry, Constants { // Functions with broadcast with different addresses // Validators needs to lock, which can be only used by the msg.sender electValidators(json); + + vm.startBroadcast(DEPLOYER_ACCOUNT); + + captureEpochManagerEnablerValidators(); + + vm.stopBroadcast(); + } + + /** + * The function calls defined here are required by the parent UsingRegistry.sol contract. + */ + function setupUsingRegistry() public { + _transferOwnership(DEPLOYER_ACCOUNT); + setRegistry(REGISTRY_ADDRESS); } function migrateRegistry() public { setImplementationOnProxy( - IProxy(registryAddress), + IProxy(REGISTRY_ADDRESS), "Registry", - abi.encodeWithSelector(IRegistry.initialize.selector) + abi.encodeWithSelector(IRegistryInitializer.initialize.selector) ); // set registry in registry itself - console.log("Owner of the Registry Proxy is", IProxy(registryAddress)._getOwner()); - addToRegistry("Registry", registryAddress); + console.log("Owner of the Registry Proxy is", IProxy(REGISTRY_ADDRESS)._getOwner()); + addToRegistry("Registry", REGISTRY_ADDRESS); console.log("Done migration registry"); } function migrateFreezer() public { - // TODO migrate the initializations interface - deployProxiedContract("Freezer", abi.encodeWithSelector(IFreezer.initialize.selector)); + deployProxiedContract( + "Freezer", + abi.encodeWithSelector(IFreezerInitializer.initialize.selector) + ); } function migrateFeeCurrencyWhitelist() public { - // TODO migrate the initializations interface deployProxiedContract( "FeeCurrencyWhitelist", abi.encodeWithSelector(IFeeCurrencyWhitelist.initialize.selector) @@ -270,23 +293,23 @@ contract Migration is Script, UsingRegistry, Constants { } function migrateFeeCurrencyDirectory() public { - // TODO migrate the initializations interface deployProxiedContract( "FeeCurrencyDirectory", - abi.encodeWithSelector(FeeCurrencyDirectory.initialize.selector) + abi.encodeWithSelector(IFeeCurrencyDirectoryInitializer.initialize.selector) ); } - function migrateGoldToken(string memory json) public { + function migrateCeloToken(string memory json) public { // TODO change pre-funded addresses to make it match circulation supply - address goldProxyAddress = deployProxiedContract( + address celoProxyAddress = deployProxiedContract( "GoldToken", - abi.encodeWithSelector(ICeloToken.initialize.selector, registryAddress) + abi.encodeWithSelector(ICeloTokenInitializer.initialize.selector, REGISTRY_ADDRESS) ); + addToRegistry("CeloToken", celoProxyAddress); bool frozen = abi.decode(json.parseRaw(".goldToken.frozen"), (bool)); if (frozen) { - getFreezer().freeze(goldProxyAddress); + getFreezer().freeze(celoProxyAddress); } } @@ -297,7 +320,7 @@ contract Migration is Script, UsingRegistry, Constants { ); deployProxiedContract( "SortedOracles", - abi.encodeWithSelector(ISortedOracles.initialize.selector, reportExpirySeconds) + abi.encodeWithSelector(ISortedOraclesInitializer.initialize.selector, reportExpirySeconds) ); } @@ -320,7 +343,7 @@ contract Migration is Script, UsingRegistry, Constants { "GasPriceMinimum", abi.encodeWithSelector( IGasPriceMinimumInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, gasPriceMinimumFloor, targetDensity, adjustmentSpeed, @@ -329,9 +352,31 @@ contract Migration is Script, UsingRegistry, Constants { ); } - function migrateReserve(string memory json) public { - // Reserve spend multisig not migrated + function migrateReserveSpenderMultiSig(string memory json) public { + address[] memory owners = new address[](1); + owners[0] = DEPLOYER_ACCOUNT; + + uint256 required = abi.decode(json.parseRaw(".reserveSpenderMultiSig.required"), (uint256)); + uint256 internalRequired = abi.decode( + json.parseRaw(".reserveSpenderMultiSig.internalRequired"), + (uint256) + ); + + // Deploys and adds the ReserveSpenderMultiSig to the Registry for ease of reference. + // The ReserveSpenderMultiSig is not in the Registry on Mainnet, but it's useful to keep a + // reference of the deployed contract, so it's in the Registry on the devchain. + deployProxiedContract( + "ReserveSpenderMultiSig", + abi.encodeWithSelector( + IReserveSpenderMultiSig.initialize.selector, + owners, + required, + internalRequired + ) + ); + } + function migrateReserve(string memory json) public { uint256 tobinTaxStalenessThreshold = abi.decode( json.parseRaw(".reserve.tobinTaxStalenessThreshold"), (uint256) @@ -359,7 +404,7 @@ contract Migration is Script, UsingRegistry, Constants { "Reserve", abi.encodeWithSelector( IReserveInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, tobinTaxStalenessThreshold, spendingRatio, frozenGold, @@ -374,9 +419,14 @@ contract Migration is Script, UsingRegistry, Constants { // TODO this should be a transfer from the deployer rather than a deal vm.deal(reserveProxyAddress, initialBalance); - address reserveSpenderMultiSig = deployerAccount; - IReserve(reserveProxyAddress).addSpender(reserveSpenderMultiSig); - console.log("reserveSpenderMultiSig set to:", reserveSpenderMultiSig); + // Adds ReserveSpenderMultiSig to Reserve + bool useSpender = abi.decode(json.parseRaw(".reserveSpenderMultiSig.required"), (bool)); + address spender = useSpender + ? registry.getAddressForString("ReserveSpenderMultiSig") + : DEPLOYER_ACCOUNT; + + IReserve(reserveProxyAddress).addSpender(spender); + console.log("reserveSpenderMultiSig added as Reserve spender"); } function deployStable( @@ -399,7 +449,7 @@ contract Migration is Script, UsingRegistry, Constants { name, symbol, decimals, - registryAddress, + REGISTRY_ADDRESS, inflationRate, inflationFactorUpdatePeriod, initialBalanceAddresses, @@ -413,7 +463,7 @@ contract Migration is Script, UsingRegistry, Constants { } // TODO add more configurable oracles from the json - getSortedOracles().addOracle(stableTokenProxyAddress, deployerAccount); + getSortedOracles().addOracle(stableTokenProxyAddress, DEPLOYER_ACCOUNT); if (celoPrice != 0) { console.log("before report"); @@ -431,13 +481,13 @@ contract Migration is Script, UsingRegistry, Constants { */ uint256 mockIntrinsicGas = 21000; - FeeCurrencyDirectory(registry.getAddressForStringOrDie("FeeCurrencyDirectory")) + IFeeCurrencyDirectory(registry.getAddressForStringOrDie("FeeCurrencyDirectory")) .setCurrencyConfig(stableTokenProxyAddress, address(getSortedOracles()), mockIntrinsicGas); } function migrateStableToken(string memory json) public { string[] memory names = abi.decode(json.parseRaw(".stableTokens.names"), (string[])); - string[] memory symbols = abi.decode(json.parseRaw(".stableTokens.names"), (string[])); + string[] memory symbols = abi.decode(json.parseRaw(".stableTokens.symbols"), (string[])); string[] memory contractSufixs = abi.decode( json.parseRaw(".stableTokens.contractSufixs"), (string[]) @@ -459,7 +509,7 @@ contract Migration is Script, UsingRegistry, Constants { uint256 celoPrice = abi.decode(json.parseRaw(".stableTokens.celoPrice"), (uint256)); address[] memory initialBalanceAddresses = new address[](1); - initialBalanceAddresses[0] = deployerAccount; + initialBalanceAddresses[0] = DEPLOYER_ACCOUNT; uint256[] memory initialBalanceValues = new uint256[](1); initialBalanceValues[0] = initialBalanceValue; @@ -493,7 +543,7 @@ contract Migration is Script, UsingRegistry, Constants { "Exchange", abi.encodeWithSelector( IExchangeInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, stableTokenIdentifier, spread, reserveFraction, @@ -513,23 +563,25 @@ contract Migration is Script, UsingRegistry, Constants { function migrateAccount() public { address accountsProxyAddress = deployProxiedContract( "Accounts", - abi.encodeWithSelector(IAccountsInitializer.initialize.selector, registryAddress) + abi.encodeWithSelector(IAccountsInitializer.initialize.selector, REGISTRY_ADDRESS) ); IAccounts(accountsProxyAddress).setEip712DomainSeparator(); } - function migrateLockedGold(string memory json) public { + function migrateLockedCelo(string memory json) public { uint256 unlockingPeriod = abi.decode(json.parseRaw(".lockedGold.unlockingPeriod"), (uint256)); - deployProxiedContract( + address LockedCeloProxyAddress = deployProxiedContract( "LockedGold", abi.encodeWithSelector( ILockedGoldInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, unlockingPeriod ) ); + + addToRegistry("LockedCelo", LockedCeloProxyAddress); } function migrateValidators(string memory json) public { @@ -575,11 +627,16 @@ contract Migration is Script, UsingRegistry, Constants { (uint256) ); + InitParamsTunnel memory initParamsTunnel = InitParamsTunnel({ + commissionUpdateDelay: commissionUpdateDelay, + downtimeGracePeriod: downtimeGracePeriod + }); + deployProxiedContract( "Validators", abi.encodeWithSelector( IValidatorsInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, groupRequirementValue, groupRequirementDuration, validatorRequirementValue, @@ -589,8 +646,7 @@ contract Migration is Script, UsingRegistry, Constants { membershipHistoryLength, slashingMultiplierResetPeriod, maxGroupSize, - commissionUpdateDelay, - downtimeGracePeriod + initParamsTunnel ) ); } @@ -617,7 +673,7 @@ contract Migration is Script, UsingRegistry, Constants { "Election", abi.encodeWithSelector( IElectionInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, minElectableValidators, maxElectableValidators, maxNumGroupsVotedFor, @@ -676,7 +732,7 @@ contract Migration is Script, UsingRegistry, Constants { "EpochRewards", abi.encodeWithSelector( IEpochRewardsInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, targetVotingYieldInitial, targetVotingYieldMax, targetVotingYieldAdjustmentFactor, @@ -739,7 +795,7 @@ contract Migration is Script, UsingRegistry, Constants { function migrateGovernanceSlasher() public { deployProxiedContract( "GovernanceSlasher", - abi.encodeWithSelector(IGovernanceSlasherInitializer.initialize.selector, registryAddress) + abi.encodeWithSelector(IGovernanceSlasherInitializer.initialize.selector, REGISTRY_ADDRESS) ); getLockedGold().addSlasher("GovernanceSlasher"); @@ -753,7 +809,7 @@ contract Migration is Script, UsingRegistry, Constants { "DoubleSigningSlasher", abi.encodeWithSelector( IDoubleSigningSlasherInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, penalty, reward ) @@ -774,7 +830,7 @@ contract Migration is Script, UsingRegistry, Constants { "DowntimeSlasher", abi.encodeWithSelector( IDowntimeSlasherInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, penalty, reward, slashableDowntime @@ -786,7 +842,7 @@ contract Migration is Script, UsingRegistry, Constants { function migrateGovernanceApproverMultiSig(string memory json) public { address[] memory owners = new address[](1); - owners[0] = deployerAccount; + owners[0] = DEPLOYER_ACCOUNT; uint256 required = abi.decode(json.parseRaw(".governanceApproverMultiSig.required"), (uint256)); uint256 internalRequired = abi.decode( @@ -822,7 +878,7 @@ contract Migration is Script, UsingRegistry, Constants { "MentoFeeHandlerSeller", abi.encodeWithSelector( IFeeHandlerSellerInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, tokenAddresses, minimumReports ) @@ -837,7 +893,7 @@ contract Migration is Script, UsingRegistry, Constants { "UniswapFeeHandlerSeller", abi.encodeWithSelector( IFeeHandlerSellerInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, tokenAddresses, minimumReports ) @@ -856,7 +912,7 @@ contract Migration is Script, UsingRegistry, Constants { "FeeHandler", abi.encodeWithSelector( IFeeHandlerInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, newFeeBeneficiary, newBurnFraction, tokens, @@ -879,10 +935,43 @@ contract Migration is Script, UsingRegistry, Constants { ); } - function migrateMintGoldSchedule() public { + function migrateCeloUnreleasedTreasury() public { + deployProxiedContract( + "CeloUnreleasedTreasury", + abi.encodeWithSelector( + ICeloUnreleasedTreasuryInitializer.initialize.selector, + REGISTRY_ADDRESS + ) + ); + } + + function migrateEpochManagerEnabler() public { + deployProxiedContract( + "EpochManagerEnabler", + abi.encodeWithSelector(IEpochManagerEnablerInitializer.initialize.selector, REGISTRY_ADDRESS) + ); + } + + function migrateScoreManager() public { + deployProxiedContract( + "ScoreManager", + abi.encodeWithSelector(IScoreManagerInitializer.initialize.selector) + ); + } + + function migrateEpochManager(string memory json) public { + address newEpochDuration = abi.decode( + json.parseRaw(".epochManager.newEpochDuration"), + (address) + ); + deployProxiedContract( - "MintGoldSchedule", - abi.encodeWithSelector(IMintGoldScheduleInitializer.initialize.selector) + "EpochManager", + abi.encodeWithSelector( + IEpochManagerInitializer.initialize.selector, + REGISTRY_ADDRESS, + newEpochDuration + ) ); } @@ -891,7 +980,7 @@ contract Migration is Script, UsingRegistry, Constants { address approver = useApprover ? registry.getAddressForString("GovernanceApproverMultiSig") - : deployerAccount; + : DEPLOYER_ACCOUNT; uint256 concurrentProposals = abi.decode( json.parseRaw(".governance.concurrentProposals"), (uint256) @@ -928,7 +1017,7 @@ contract Migration is Script, UsingRegistry, Constants { "Governance", abi.encodeWithSelector( IGovernanceInitializer.initialize.selector, - registryAddress, + REGISTRY_ADDRESS, approver, concurrentProposals, minDeposit, @@ -944,10 +1033,10 @@ contract Migration is Script, UsingRegistry, Constants { ); _setConstitution(governanceProxyAddress, json); - _transferOwnerShipCoreContact(governanceProxyAddress, json); + _transferOwnerShipCoreContract(governanceProxyAddress, json); } - function _transferOwnerShipCoreContact(address governanceAddress, string memory json) public { + function _transferOwnerShipCoreContract(address governanceAddress, string memory json) public { bool skipTransferOwnership = abi.decode( json.parseRaw(".governance.skipTransferOwnership"), (bool) @@ -980,7 +1069,7 @@ contract Migration is Script, UsingRegistry, Constants { } console.log(string.concat("Setting constitution thresholds for: ", contractName)); - IRegistry registry = IRegistry(registryAddress); + IRegistry registry = IRegistry(REGISTRY_ADDRESS); address contractAddress = registry.getAddressForString(contractName); @@ -1016,14 +1105,12 @@ contract Migration is Script, UsingRegistry, Constants { function registerValidator( uint256 validatorIndex, - bytes memory ecdsaPubKey, uint256 validatorKey, uint256 amountToLock, address groupToAffiliate ) public returns (address) { vm.startBroadcast(validatorKey); lockGold(amountToLock); - bytes memory _ecdsaPubKey = ecdsaPubKey; address accountAddress = (new ForceTx()).identity(); // these blobs are not checked in the contract @@ -1038,29 +1125,23 @@ contract Migration is Script, UsingRegistry, Constants { bytes16(0x05050505050505050505050505050506), bytes16(0x06060606060606060606060606060607) ); + + (bytes memory ecdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner(accountAddress, validatorKey); getValidators().registerValidator(ecdsaPubKey, newBlsPublicKey, newBlsPop); getValidators().affiliate(groupToAffiliate); - console.log("Done registering validatora"); + console.log("Done registering validators"); vm.stopBroadcast(); return accountAddress; } function getValidatorKeyIndex( + uint256 groupCount, uint256 groupIndex, uint256 validatorIndex, uint256 membersInAGroup ) public returns (uint256) { - return groupIndex * membersInAGroup + validatorIndex + 1; - } - - function getValidatorKeyFromGroupGroup( - uint256[] memory keys, - uint256 groupIndex, - uint256 validatorIndex, - uint256 membersInAGroup - ) public returns (uint256) { - return keys[getValidatorKeyIndex(groupIndex, validatorIndex, membersInAGroup)]; + return groupCount + groupIndex * membersInAGroup + validatorIndex; } function registerValidatorGroup( @@ -1079,6 +1160,57 @@ contract Migration is Script, UsingRegistry, Constants { vm.stopBroadcast(); } + function _generateEcdsaPubKeyWithSigner( + address _validator, + uint256 _signerPk + ) internal returns (bytes memory ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) { + (v, r, s) = getParsedSignatureOfAddress(_validator, _signerPk); + + bytes32 addressHash = keccak256(abi.encodePacked(_validator)); + + ecdsaPubKey = addressToPublicKey(addressHash, v, r, s); + } + + function addressToPublicKey( + bytes32 message, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public returns (bytes memory) { + address SECP256K1Address = actor("SECP256K1Address"); + deployCodeTo("SECP256K1.sol:SECP256K1", SECP256K1Address); + ISECP256K1 sECP256K1 = ISECP256K1(SECP256K1Address); + + string memory header = "\x19Ethereum Signed Message:\n32"; + bytes32 _message = keccak256(abi.encodePacked(header, message)); + (uint256 x, uint256 y) = sECP256K1.recover( + uint256(_message), + _v - 27, + uint256(_r), + uint256(_s) + ); + return abi.encodePacked(x, y); + } + + function actor(string memory name) internal returns (address) { + return vm.addr(uint256(keccak256(abi.encodePacked(name)))); + } + + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } + + function getParsedSignatureOfAddress( + address _address, + uint256 privateKey + ) public pure returns (uint8, bytes32, bytes32) { + bytes32 addressHash = keccak256(abi.encodePacked(_address)); + bytes32 prefixedHash = toEthSignedMessageHash(addressHash); + return vm.sign(privateKey, prefixedHash); + } + function electValidators(string memory json) public { console.log("Electing validators: "); @@ -1093,7 +1225,6 @@ contract Migration is Script, UsingRegistry, Constants { json.parseRaw(".validators.validatorLockedGoldRequirements.value"), (uint256) ); - bytes[] memory ecdsaPubKeys = abi.decode(json.parseRaw(".validators.ecdsaPubKeys"), (bytes[])); // attestationKeys not migrated if (valKeys.length == 0) { @@ -1106,52 +1237,76 @@ contract Migration is Script, UsingRegistry, Constants { ); } - uint256 validatorGroup0Key = valKeys[0]; + uint256 groupCount = 3; + console.log("groupCount", groupCount); - address groupAddress = registerValidatorGroup( - validatorGroup0Key, - maxGroupSize * validatorLockedGoldRequirements, - commission, - json - ); + address[] memory groups = new address[](groupCount); + + // register 3 validator groups + for (uint256 groupIndex = 0; groupIndex < groupCount; groupIndex++) { + address groupAddress = registerValidatorGroup( + valKeys[groupIndex], + maxGroupSize * validatorLockedGoldRequirements, + commission, + json + ); + groups[groupIndex] = groupAddress; + console.log("registered group: ", groupAddress); + } - console.log(" * Registering ${group.valKeys.length} validators ..."); + console.log(" * Registering validators ... Count: ", valKeys.length - groupCount); // Split the validator keys into groups that will fit within the max group size. - uint256 amountOfGroups = Math.ceilDiv(valKeys.length, maxGroupSize); // TODO change name of variable amount of groups for amount in group - for (uint256 validatorIndex = 0; validatorIndex < amountOfGroups; validatorIndex++) { - console.log("Validator key index", getValidatorKeyIndex(0, validatorIndex, maxGroupSize)); - console.log("Registering validator #: ", validatorIndex); - bytes memory ecdsaPubKey = ecdsaPubKeys[ - getValidatorKeyIndex(0, validatorIndex, maxGroupSize) - ]; - address validator = registerValidator( - validatorIndex, - ecdsaPubKey, - getValidatorKeyFromGroupGroup(valKeys, 0, validatorIndex, maxGroupSize), - validatorLockedGoldRequirements, - groupAddress - ); - // TODO start broadcast - console.log("Adding to group..."); - - vm.startBroadcast(validatorGroup0Key); - if (validatorIndex == 0) { - getValidators().addFirstMember(validator, address(0), address(0)); - console.log("Making group vote for itself"); - getElection().vote( - groupAddress, - getLockedGold().getAccountNonvotingLockedGold(groupAddress), - address(0), - address(0) + for (uint256 groupIndex = 0; groupIndex < groupCount; groupIndex++) { + address groupAddress = groups[groupIndex]; + console.log("Registering members for group: ", groupAddress); + for (uint256 validatorIndex = 0; validatorIndex < maxGroupSize; validatorIndex++) { + uint256 validatorKeyIndex = getValidatorKeyIndex( + groupCount, + groupIndex, + validatorIndex, + maxGroupSize ); - } else { - // unimplemented - console.log("WARNING: case not implemented"); - } + console.log("Registering validator #: ", validatorIndex); + address validator = registerValidator( + validatorIndex, + valKeys[validatorKeyIndex], + validatorLockedGoldRequirements, + groupAddress + ); + // TODO start broadcast + console.log("Adding to group..."); + + vm.startBroadcast(groups[groupIndex]); + address greater = groupIndex == 0 ? address(0) : groups[groupIndex - 1]; + + if (validatorIndex == 0) { + getValidators().addFirstMember(validator, address(0), greater); + console.log("Making group vote for itself"); + } else { + getValidators().addMember(validator); + } + getElection().vote(groupAddress, validatorLockedGoldRequirements, address(0), greater); - vm.stopBroadcast(); + vm.stopBroadcast(); + } } } + + function captureEpochManagerEnablerValidators() public { + address numberValidatorsInCurrentSetPrecompileAddress = 0x00000000000000000000000000000000000000f9; + numberValidatorsInCurrentSetPrecompileAddress.call( + abi.encodeWithSignature("setNumberOfValidators()") + ); + + address validatorSignerAddressFromCurrentSetPrecompileAddress = 0x00000000000000000000000000000000000000fa; + validatorSignerAddressFromCurrentSetPrecompileAddress.call( + abi.encodeWithSignature("setValidators()") + ); + + address epochManagerEnabler = registry.getAddressForString("EpochManagerEnabler"); + IEpochManagerEnabler epochManagerEnablerContract = IEpochManagerEnabler(epochManagerEnabler); + epochManagerEnablerContract.captureEpochAndValidators(); + } } diff --git a/packages/protocol/migrations_sol/MigrationL2.s.sol b/packages/protocol/migrations_sol/MigrationL2.s.sol new file mode 100644 index 00000000000..72c80815eb1 --- /dev/null +++ b/packages/protocol/migrations_sol/MigrationL2.s.sol @@ -0,0 +1,50 @@ +pragma solidity >=0.8.7 <0.8.20; + +import { Script } from "forge-std-8/Script.sol"; +import { MigrationsConstants } from "@migrations-sol/constants.sol"; + +// Foundry imports +import "forge-std/console.sol"; + +import "@celo-contracts/common/FixidityLib.sol"; +import "@celo-contracts-8/common/UsingRegistry.sol"; +import "@celo-contracts/common/interfaces/IEpochManagerEnabler.sol"; + +contract MigrationL2 is Script, MigrationsConstants, UsingRegistry { + using FixidityLib for FixidityLib.Fraction; + + /** + * Entry point of the script + */ + function run() external { + vm.startBroadcast(DEPLOYER_ACCOUNT); + + setupUsingRegistry(); + dealToCeloUnreleasedTreasury(); + + initializeEpochManagerSystem(); + + vm.stopBroadcast(); + } + + function setupUsingRegistry() public { + _transferOwnership(DEPLOYER_ACCOUNT); + setRegistry(REGISTRY_ADDRESS); + } + + function dealToCeloUnreleasedTreasury() public { + vm.deal(address(getCeloUnreleasedTreasury()), L2_INITIAL_STASH_BALANCE); + } + + function initializeEpochManagerSystem() public { + console.log("Initializing EpochManager system"); + address[] memory firstElected = getValidators().getRegisteredValidators(); + IEpochManager epochManager = getEpochManager(); + address epochManagerEnablerAddress = registry.getAddressForOrDie( + EPOCH_MANAGER_ENABLER_REGISTRY_ID + ); + + IEpochManagerEnabler epochManagerEnabler = IEpochManagerEnabler(epochManagerEnablerAddress); + epochManagerEnabler.initEpochManager(); + } +} diff --git a/packages/protocol/migrations_sol/README.md b/packages/protocol/migrations_sol/README.md new file mode 100644 index 00000000000..2d814c0e79e --- /dev/null +++ b/packages/protocol/migrations_sol/README.md @@ -0,0 +1,109 @@ +# @celo/devchain-anvil + +This package contains anvil state that allows you to start an [anvil](https://book.getfoundry.sh/reference/anvil/) instance in seconds. + +This anvil instance serves at `localhost:8545` on your machine, and comes pre-configured with core contracts like the [`Registry`](https://github.com/celo-org/celo-monorepo/blob/master/packages/protocol/contracts/common/Registry.sol) contract (at `0x000000000000000000000000000000000000ce10`). You can make RPC calls against this anvil instance as if you were interacting with Celo on `Alfajores` or `Mainnet`. + +## Usage + +```bash +npm install --save-dev @celo/devchain-anvil +anvil --state +``` + +Files in this package: + +1. Use `devchain.json` for a Celo L1-like devchain. +1. Use `l2-devchain.json` for a Celo L2-like devchain. + +### Example + +1. Make a test directory + + ```sh + # Create a demo directory + $ mkdir ~/Documents/local-anvil-demo + + # Move into the demo directory + $ cd ~/Documents/local-anvil-demo + ``` + +2. Install the package + + ```sh + $ npm install --save-dev @celo/devchain-anvil + ``` + +3. Start an anvil instance with the state file from the package + + ```sh + $ anvil --state node_modules/@celo/devchain-anvil/devchain.json + + _ _ + (_) | | + __ _ _ __ __ __ _ | | + / _` | | '_ \ \ \ / / | | | | + | (_| | | | | | \ V / | | | | + \__,_| |_| |_| \_/ |_| |_| + + 0.2.0 (f625d0f 2024-04-02T00:16:42.824772000Z) + https://github.com/foundry-rs/foundry + + Available Accounts + ================== + + (0) 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000.000000000000000000 ETH) + + # ... + Listening on 127.0.0.1:8545 + ``` + +4. Make RPC calls against the anvil instance serving at `http://127.0.0.1:8545` + +## Background + +[Foundry](https://book.getfoundry.sh/reference/anvil/) allows you to start anvil instances with state you read from `json` files. +The idea is to save the state of an anvil instance at a certain point in time, and then load it back up later. + +``` +$ anvil --state + This is an alias for both --load-state and --dump-state. + + It initializes the chain with the state and block environment stored at the file, if it + exists, and dumps the chain's state on exit. +``` + +Source: [Anvil docs](https://book.getfoundry.sh/reference/cli/anvil?highlight=--state#anvil) + +We use this feature to pre-configure an anvil instance with core contracts and other state, and then start it up in seconds for testing. +We call this a "devchain". The scripts we use to configure the devchain are here: [`celo-org/celo-monorepo/` > `packages/protocol/migrations_sol`](https://github.com/celo-org/celo-monorepo/tree/master/packages/protocol/migrations_sol) + +## Limitations + +The anvil instance is not a full Celo node. It is a lightweight, in-memory instance that you can use for testing. + +1. It does not sync with the Celo network ❌ +1. It does not persist data between sessions ❌ +1. It does not support all RPC methods ❌ +1. It does not support fee currency transactions (like [CIP64](https://github.com/celo-org/celo-proposals/blob/master/CIPs/cip-0064.md) or [CIP66](https://github.com/celo-org/celo-proposals/blob/master/CIPs/cip-0066.md)) ❌ +1. It does not support all Celo pre-compiles ❌ + +## How we work + +We are a GitHub-first team, which means we have a strong preference for communicating via GitHub. +Please use GitHub to: + +🐞 [File a bug report](https://github.com/celo-org/celo-monorepo/issues/new/choose) + +💬 [Ask a question](https://github.com/celo-org/celo-monorepo/discussions) + +✨ [Suggest a feature](https://github.com/celo-org/celo-monorepo/issues/new/choose) + +🧑‍💻 [Contribute!](https://github.com/celo-org/celo-monorepo/tree/master/packages/protocol/migrations_sol/CONTRIBUTING.md) + +🚔 [Report a security vulnerability](https://github.com/celo-org/celo-monorepo/issues/new/choose) + +> [!TIP] +> +> Please avoid messaging us via Slack, Telegram, or email. We are more likely to respond to you on +> GitHub than if you message us anywhere else. We actively monitor GitHub, and will get back to you shortly 🌟 diff --git a/packages/protocol/migrations_sol/celo-anvil-README.md b/packages/protocol/migrations_sol/celo-anvil-README.md deleted file mode 100644 index 46a9b59be67..00000000000 --- a/packages/protocol/migrations_sol/celo-anvil-README.md +++ /dev/null @@ -1,10 +0,0 @@ -# celo devchain Anvil - -Anvil state with Celo core contracts for local testing and development. - -# Usage - -```bash -npm install --save-dev @celo/devchain-anvil -anvil --state -``` diff --git a/packages/protocol/migrations_sol/constants.sol b/packages/protocol/migrations_sol/constants.sol index 81bee276509..c47449a5e1c 100644 --- a/packages/protocol/migrations_sol/constants.sol +++ b/packages/protocol/migrations_sol/constants.sol @@ -1,21 +1,29 @@ pragma solidity >=0.8.7 <0.8.20; -contract Constants { +import { TestConstants } from "@test-sol/constants.sol"; + +contract MigrationsConstants is TestConstants { + // Addresses + address constant DEPLOYER_ACCOUNT = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + // List of contracts that are expected to be in Registry.sol - string[23] contractsInRegistry = [ + string[27] contractsInRegistry = [ "Accounts", "BlockchainParameters", + "CeloUnreleasedTreasury", + "CeloToken", "DoubleSigningSlasher", "DowntimeSlasher", "Election", "EpochRewards", + "EpochManagerEnabler", + "EpochManager", "Escrow", "FederatedAttestations", "FeeCurrencyWhitelist", "FeeCurrencyDirectory", "Freezer", "FeeHandler", - "GoldToken", "Governance", "GovernanceSlasher", "LockedGold", @@ -25,6 +33,7 @@ contract Constants { "SortedOracles", "UniswapFeeHandlerSeller", "MentoFeeHandlerSeller", - "Validators" + "Validators", + "ScoreManager" ]; } diff --git a/packages/protocol/migrations_sol/create_and_migrate_anvil_devchain.sh b/packages/protocol/migrations_sol/create_and_migrate_anvil_devchain.sh deleted file mode 100755 index 8050b93c302..00000000000 --- a/packages/protocol/migrations_sol/create_and_migrate_anvil_devchain.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Keeping track of start time to measure how long it takes to run the script entirely -START_TIME=$SECONDS - -export ANVIL_PORT=8546 - -echo "Forge version: $(forge --version)" - -# TODO make this configurable -FROM_ACCOUNT_NO_ZERO="f39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # This is Anvil's default account (1) -export FROM_ACCOUNT="0x$FROM_ACCOUNT_NO_ZERO" - -# Create temporary directory -TEMP_FOLDER="$PWD/.tmp" -if [ -d "$TEMP_FOLDER" ]; then - # Remove temporary directory first it if exists - echo "Removing existing temporary folder..." - rm -rf $TEMP_FOLDER -fi -mkdir -p $TEMP_FOLDER - -# Start a local anvil instance -source $PWD/migrations_sol/start_anvil.sh - -# Deploy libraries to the anvil instance -source $PWD/migrations_sol/deploy_libraries.sh -echo "Library flags are: $LIBRARY_FLAGS" - -# Build all contracts with deployed libraries -# Including contracts that depend on libraries. This step replaces the library placeholder -# in the bytecode with the address of the actually deployed library. -echo "Compiling with libraries... " -time forge build $LIBRARY_FLAGS - -# Deploy precompile contracts -source $PWD/migrations_sol/deploy_precompiles.sh - -echo "Setting Registry Proxy" -REGISTRY_ADDRESS="0x000000000000000000000000000000000000ce10" -PROXY_BYTECODE=`cat ./out/Proxy.sol/Proxy.json | jq -r '.deployedBytecode.object'` -cast rpc anvil_setCode --rpc-url http://127.0.0.1:$ANVIL_PORT $REGISTRY_ADDRESS $PROXY_BYTECODE -REGISTRY_OWNER_ADDRESS=$FROM_ACCOUNT_NO_ZERO - -echo "Setting Registry owner" -# Sets the storage of the registry so that it has an owner we control -# position is bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); -cast rpc anvil_setStorageAt --rpc-url http://127.0.0.1:$ANVIL_PORT $REGISTRY_ADDRESS 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 "0x000000000000000000000000$REGISTRY_OWNER_ADDRESS" - -# run migrations -echo "Running migration script... " -# helpers to disable broadcast and simulation -# TODO move to configuration -BROADCAST="--broadcast" -SKIP_SIMULATION="" -# SKIP_SIMULATION="--skip-simulation" -# BROADCAST="" -time forge script migrations_sol/Migration.s.sol --tc Migration --rpc-url http://127.0.0.1:$ANVIL_PORT -vvv $BROADCAST --non-interactive --sender $FROM_ACCOUNT --unlocked $LIBRARY_FLAGS || echo "Migration script failed" - -# Keeping track of the finish time to measure how long it takes to run the script entirely -ELAPSED_TIME=$(($SECONDS - $START_TIME)) -echo "Total elapsed time: $ELAPSED_TIME seconds" -# Rename devchain artifact and remove unused directory -mv $TEMP_FOLDER/devchain/state.json $TEMP_FOLDER/devchain.json -rm -rf $TEMP_FOLDER/devchain diff --git a/packages/protocol/migrations_sol/deploy_precompiles.sh b/packages/protocol/migrations_sol/deploy_precompiles.sh deleted file mode 100755 index eb09bb0e2dc..00000000000 --- a/packages/protocol/migrations_sol/deploy_precompiles.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -echo "Deploying precompiles:" - - -# TODO insteadl of deploying one by one -EpochSizeAddress=0x00000000000000000000000000000000000000f8 -EpochSizeBytecode=`cat ./out/EpochSizePrecompile.sol/EpochSizePrecompile.json | jq -r '.deployedBytecode.object'` -cast rpc anvil_setCode --rpc-url http://127.0.0.1:$ANVIL_PORT $EpochSizeAddress $EpochSizeBytecode - -ProofOfPossesionAddress=0x00000000000000000000000000000000000000fb -ProofOfPossesionBytecode=`cat ./out/ProofOfPossesionPrecompile.sol/ProofOfPossesionPrecompile.json | jq -r '.deployedBytecode.object'` -cast rpc anvil_setCode --rpc-url http://127.0.0.1:$ANVIL_PORT $ProofOfPossesionAddress $ProofOfPossesionBytecode \ No newline at end of file diff --git a/packages/protocol/migrations_sol/migrationsConfig.json b/packages/protocol/migrations_sol/migrationsConfig.json index 94ad15ddc2f..ac1bdafe623 100644 --- a/packages/protocol/migrations_sol/migrationsConfig.json +++ b/packages/protocol/migrations_sol/migrationsConfig.json @@ -1,5 +1,5 @@ { - "goldToken": {"frozen":false}, + "goldToken": { "frozen": false }, "sortedOracles": { "reportExpirySeconds": 300 }, @@ -21,9 +21,23 @@ "0x246f4599eFD3fA67AC44335Ed5e749E518Ffd8bB", "0x298FbD6dad2Fc2cB56d7E37d8aCad8Bf07324f67" ], - "assetAllocationSymbols_": ["cGLD", "BTC", "ETH", "DAI", "They are don't by converting string to hex, and then `cast to-bytes32`"], - "assetAllocationSymbols": ["0x63474c4400000000000000000000000000000000000000000000000000000000", "0x4254430000000000000000000000000000000000000000000000000000000000", "0x4554480000000000000000000000000000000000000000000000000000000000", "0x4441490000000000000000000000000000000000000000000000000000000000"], - "assetAllocationWeights": [500000000000000000000000, 300000000000000000000000, 150000000000000000000000, 50000000000000000000000], + "assetAllocationSymbols_": [ + "cGLD", + "BTC", + "ETH", + "DAI", + "They are don't by converting string to hex, and then `cast to-bytes32`" + ], + "assetAllocationSymbols": [ + "0x63474c4400000000000000000000000000000000000000000000000000000000", + "0x4254430000000000000000000000000000000000000000000000000000000000", + "0x4554480000000000000000000000000000000000000000000000000000000000", + "0x4441490000000000000000000000000000000000000000000000000000000000" + ], + "assetAllocationWeights": [ + 500000000000000000000000, 300000000000000000000000, 150000000000000000000000, + 50000000000000000000000 + ], "initialBalance": 5000000000000000000000000 }, "stableTokens": { @@ -39,7 +53,7 @@ }, "exchange": { "spread": 5000000000000000000000, - "reserveFraction": 10000000000000000000000, + "reserveFraction": 10000000000000000000000, "updateFrequency": 300, "minimumReports": 1, "frozen": false @@ -64,10 +78,10 @@ "adjustmentSpeed": 100000000000000000000000 }, "membershipHistoryLength": 60, - "commissionUpdateDelay": 51840, - "commissionUpdateDelay_help": "(3 * DAY) / 5", + "commissionUpdateDelay": 51840, + "commissionUpdateDelay_help": "(3 * DAY) / 5", - "maxGroupSize": 5, + "maxGroupSize": 2, "slashingMultiplierResetPeriod": 2592000, "slashingPenaltyResetPeriod_help": "30 * DAY", "downtimeGracePeriod": 0, @@ -76,27 +90,34 @@ "groupName": "cLabs", "commission": 100000000000000000000000, "votesRatioOfLastVsFirstGroup": 2000000000000000000000000, - "valKeys": ["0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d","0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"], - "ecdsaPubKeys": [ - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "0x9d9031e97dd78ff8c15aa86939de9b1e791066a0224e331bc962a2099a7b1f0464b8bbafe1535f2301c72c2cb3535b172da30b02686ab0393d348614f157fbdb"] + "valKeys": [ + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", + "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", + "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", + "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba", + "0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e", + "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356", + "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97", + "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" + ] }, "election": { "minElectableValidators": 1, - "maxElectableValidators": 100, + "maxElectableValidators": 110, "maxNumGroupsVotedFor": 10, "electabilityThreshold": 1000000000000000000000 }, "epochRewards": { "targetVotingYieldParameters": { - "initial": 0, - "max": 500000000000000000000, + "initial": 160000000000000000000, + "max": 500000000000000000000, "max_helper": "(x + 1) ^ 365 = 1.20", - "adjustmentFactor": 0, + "adjustmentFactor": 0, "adjustmentFactor_helper": "Change to 1 / 3650 once Mainnet activated." }, "rewardsMultiplierParameters": { - "max": 2, + "max": 2000000000000000000000000, "adjustmentFactors": { "underspend": 500000000000000000000000, "overspend": 5000000000000000000000000 @@ -106,10 +127,13 @@ "maxValidatorEpochPayment": 205479452054794520547, "maxValidatorEpochPayment_helper": "(75,000 / 365) * 10 ^ 18", "communityRewardFraction": 250000000000000000000000, - "carbonOffsettingPartner": "0x0000000000000000000000000000000000000000", + "carbonOffsettingPartner": "0xD533Ca259b330c7A88f74E000a3FaEa2d63B7972", "carbonOffsettingFraction": 1000000000000000000000, "frozen": false }, + "epochManager": { + "newEpochDuration": 86400 + }, "random": { "randomnessBlockRetentionWindow": "720", "randomnessBlockRetentionWindow_helper": "HOUR / 5" @@ -134,6 +158,10 @@ "internalRequired": 1, "governanceApproverMultiSig": true }, + "reserveSpenderMultiSig": { + "required": 1, + "internalRequired": 1 + }, "feeHandler": { "beneficiary": "0x2A486910DBC72cACcbb8d0e1439C96b03B2A4699", "burnFraction": 800000000000000000000000 @@ -143,7 +171,7 @@ "dequeueFrequency": 14400, "queueExpiry": 2419200, "queueExpiry_helper": "4 * WEEK", - "dequeueFrequency_helper":"4 * HOUR", + "dequeueFrequency_helper": "4 * HOUR", "approvalStageDuration": 14400, "approvalStageDuration_helper": "4 * HOUR", "referendumStageDuration": 86400, @@ -158,4 +186,4 @@ "skipSetConstitution": false, "skipTransferOwnership": false } -} \ No newline at end of file +} diff --git a/packages/protocol/migrations_sol/run_integration_tests_in_anvil.sh b/packages/protocol/migrations_sol/run_integration_tests_in_anvil.sh deleted file mode 100755 index af25e4b069a..00000000000 --- a/packages/protocol/migrations_sol/run_integration_tests_in_anvil.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Generate and run devchain -echo "Generating and running devchain before running integration tests..." -source $PWD/migrations_sol/create_and_migrate_anvil_devchain.sh - -# Run integration tests -echo "Running integration tests..." -forge test \ --vvv \ ---match-contract RegistryIntegrationTest \ ---fork-url http://127.0.0.1:$ANVIL_PORT - -# helper kill anvil -# kill $(lsof -i tcp:$ANVIL_PORT | tail -n 1 | awk '{print $2}') - -echo "Killing Anvil" -if [[ -n $ANVIL_PID ]]; then - kill $ANVIL_PID -fi diff --git a/packages/protocol/migrations_sol/start_anvil.sh b/packages/protocol/migrations_sol/start_anvil.sh deleted file mode 100755 index e75a82162b1..00000000000 --- a/packages/protocol/migrations_sol/start_anvil.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -timestamp=`date -Iseconds` -TMP_FOLDER="$PWD/.tmp" -ANVIL_FOLDER="$TMP_FOLDER/devchain" -mkdir -p $ANVIL_FOLDER -echo "Anvil state will be saved to $ANVIL_FOLDER" - -# create package.json -echo "{\"name\": \"@celo/devchain-anvil\",\"version\": \"1.0.0\",\"description\": \"Anvil based devchain that contains core smart contracts of celo\",\"author\":\"Celo\",\"license\": \"LGPL-3.0\"}" > $TMP_FOLDER/package.json - -cp $PWD/migrations_sol/celo-anvil-README.md $TMP_FOLDER/README.md - -if nc -z localhost $ANVIL_PORT; then - echo "Port already used" - kill $(lsof -i tcp:$ANVIL_PORT | tail -n 1 | awk '{print $2}') - echo "Killed previous Anvil" -fi - -anvil --port $ANVIL_PORT --gas-limit 50000000 --steps-tracing --code-size-limit 245760 --balance 60000 --dump-state $ANVIL_FOLDER --state-interval 1 & - -# alternatively: -# ANVIL_PID=`lsof -i tcp:8545 | tail -n 1 | awk '{print $2}'` - -export ANVIL_PID=$! - -echo "Waiting Anvil to launch on $ANVIL_PORT..." - - -while ! nc -z localhost $ANVIL_PORT; do - sleep 0.1 # wait for 1/10 of the second before check again -done - -# enabled logging -cast rpc anvil_setLoggingEnabled true --rpc-url http://127.0.0.1:$ANVIL_PORT - -echo "Anvil launched" -sleep 1 diff --git a/packages/protocol/migrations_ts/01_libraries.ts b/packages/protocol/migrations_ts/01_libraries.ts index 3cb33f15561..6dd5b4047e7 100644 --- a/packages/protocol/migrations_ts/01_libraries.ts +++ b/packages/protocol/migrations_ts/01_libraries.ts @@ -1,10 +1,21 @@ +import { SOLIDITY_08_PACKAGE } from '@celo/protocol/contractPackages' +import { ArtifactsSingleton } from '@celo/protocol/lib/artifactsSingleton' +import { makeTruffleContractForMigration } from '@celo/protocol/lib/web3-utils' import { linkedLibraries } from '@celo/protocol/migrationsConfig' module.exports = (deployer: any) => { Object.keys(linkedLibraries).forEach((lib: string) => { - const Library = artifacts.require(lib) + const artifacts08 = ArtifactsSingleton.getInstance(SOLIDITY_08_PACKAGE, artifacts) + + for (const contractName of SOLIDITY_08_PACKAGE.contracts) { + makeTruffleContractForMigration(contractName, SOLIDITY_08_PACKAGE, web3) + } + + const Library = artifacts08.require(lib, artifacts) deployer.deploy(Library) - const Contracts = linkedLibraries[lib].map((contract: string) => artifacts.require(contract)) + const Contracts = linkedLibraries[lib].map((contract: string) => + artifacts08.require(contract, artifacts) + ) deployer.link(Library, Contracts) }) } diff --git a/packages/protocol/migrations_ts/04_goldtoken.ts b/packages/protocol/migrations_ts/04_goldtoken.ts index 59138ccc537..5b1d9a1e321 100644 --- a/packages/protocol/migrations_ts/04_goldtoken.ts +++ b/packages/protocol/migrations_ts/04_goldtoken.ts @@ -4,7 +4,7 @@ import { getDeployedProxiedContract, } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' -import { FreezerInstance, GoldTokenInstance } from 'types' +import { FreezerInstance, GoldTokenInstance, RegistryInstance } from 'types' const initializeArgs = async () => { return [config.registry.predeployedProxyAddress] @@ -23,5 +23,7 @@ module.exports = deploymentForCoreContract( ) await freezer.freeze(goldToken.address) } + const registry = await getDeployedProxiedContract('Registry', artifacts) + await registry.setAddressFor(CeloContractName.CeloToken, goldToken.address) } ) diff --git a/packages/protocol/migrations_ts/12_lockedgold.ts b/packages/protocol/migrations_ts/12_lockedgold.ts index 5eb603e7fb0..4404ec9fbe9 100644 --- a/packages/protocol/migrations_ts/12_lockedgold.ts +++ b/packages/protocol/migrations_ts/12_lockedgold.ts @@ -1,11 +1,22 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { + deploymentForCoreContract, + getDeployedProxiedContract, +} from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' -import { LockedGoldInstance } from 'types' +import { LockedGoldInstance, RegistryInstance } from 'types' + +const initializeArgs = async (): Promise => { + return [config.registry.predeployedProxyAddress, config.lockedGold.unlockingPeriod] +} module.exports = deploymentForCoreContract( web3, artifacts, CeloContractName.LockedGold, - async () => [config.registry.predeployedProxyAddress, config.lockedGold.unlockingPeriod] + initializeArgs, + async (lockedGold: LockedGoldInstance) => { + const registry = await getDeployedProxiedContract('Registry', artifacts) + await registry.setAddressFor(CeloContractName.LockedCelo, lockedGold.address) + } ) diff --git a/packages/protocol/migrations_ts/13_validators.ts b/packages/protocol/migrations_ts/13_validators.ts index 2cc51d2ae48..fc29dfdd635 100644 --- a/packages/protocol/migrations_ts/13_validators.ts +++ b/packages/protocol/migrations_ts/13_validators.ts @@ -2,7 +2,8 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' import { config } from '@celo/protocol/migrationsConfig' import { toFixed } from '@celo/utils/lib/fixidity' -import { ValidatorsInstance } from 'types' +import { ValidatorsInstance } from 'types/08' +import { SOLIDITY_08_PACKAGE } from '../contractPackages' const initializeArgs = async (): Promise => { return [ @@ -16,8 +17,10 @@ const initializeArgs = async (): Promise => { config.validators.membershipHistoryLength, config.validators.slashingPenaltyResetPeriod, config.validators.maxGroupSize, - config.validators.commissionUpdateDelay, - config.validators.downtimeGracePeriod, + { + commissionUpdateDelay: config.validators.commissionUpdateDelay, + downtimeGracePeriod: config.validators.downtimeGracePeriod, + }, ] } @@ -25,5 +28,7 @@ module.exports = deploymentForCoreContract( web3, artifacts, CeloContractName.Validators, - initializeArgs + initializeArgs, + undefined, + SOLIDITY_08_PACKAGE ) diff --git a/packages/protocol/migrations_ts/26_101_score_manager.ts b/packages/protocol/migrations_ts/26_101_score_manager.ts new file mode 100644 index 00000000000..93a74670975 --- /dev/null +++ b/packages/protocol/migrations_ts/26_101_score_manager.ts @@ -0,0 +1,17 @@ +import { SOLIDITY_08_PACKAGE } from '@celo/protocol/contractPackages' +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { ScoreManagerInstance } from 'types/08' + +const initializeArgs = async (): Promise => { + return [] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.ScoreManager, + initializeArgs, + undefined, + SOLIDITY_08_PACKAGE +) diff --git a/packages/protocol/migrations_ts/26_102_epoch_manager_enabler.ts b/packages/protocol/migrations_ts/26_102_epoch_manager_enabler.ts new file mode 100644 index 00000000000..6a9a3d9220f --- /dev/null +++ b/packages/protocol/migrations_ts/26_102_epoch_manager_enabler.ts @@ -0,0 +1,18 @@ +import { SOLIDITY_08_PACKAGE } from '@celo/protocol/contractPackages' +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' +import { EpochManagerEnablerInstance } from 'types/08' + +const initializeArgs = async (): Promise => { + return [config.registry.predeployedProxyAddress] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.EpochManagerEnabler, + initializeArgs, + undefined, + SOLIDITY_08_PACKAGE +) diff --git a/packages/protocol/migrations_ts/26_103_epoch_manager.ts b/packages/protocol/migrations_ts/26_103_epoch_manager.ts new file mode 100644 index 00000000000..4956ebcc139 --- /dev/null +++ b/packages/protocol/migrations_ts/26_103_epoch_manager.ts @@ -0,0 +1,18 @@ +import { SOLIDITY_08_PACKAGE } from '@celo/protocol/contractPackages' +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' +import { EpochManagerInstance } from 'types/08' + +const initializeArgs = async (): Promise => { + return [config.registry.predeployedProxyAddress, config.epochManager.newEpochDuration] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.EpochManager, + initializeArgs, + undefined, + SOLIDITY_08_PACKAGE +) diff --git a/packages/protocol/migrations_ts/28_celo_unreleased_treasury.ts b/packages/protocol/migrations_ts/28_celo_unreleased_treasury.ts new file mode 100644 index 00000000000..81f0c6d1f7d --- /dev/null +++ b/packages/protocol/migrations_ts/28_celo_unreleased_treasury.ts @@ -0,0 +1,25 @@ +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { + deploymentForCoreContract, + getDeployedProxiedContract, +} from '@celo/protocol/lib/web3-utils' +import { RegistryInstance } from '@celo/protocol/types' +import { CeloUnreleasedTreasuryInstance } from '@celo/protocol/types/08' +import { SOLIDITY_08_PACKAGE } from '../contractPackages' + +const initializeArgs = async (): Promise<[string]> => { + const registry: RegistryInstance = await getDeployedProxiedContract( + 'Registry', + artifacts + ) + return [registry.address] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.CeloUnreleasedTreasury, + initializeArgs, + undefined, + SOLIDITY_08_PACKAGE +) diff --git a/packages/protocol/migrations_ts/28_mintGoldSchedule.ts b/packages/protocol/migrations_ts/28_mintGoldSchedule.ts deleted file mode 100644 index 8dd1f3820dc..00000000000 --- a/packages/protocol/migrations_ts/28_mintGoldSchedule.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' -import { MintGoldScheduleInstance } from 'types/08' -import { SOLIDITY_08_PACKAGE } from '../contractPackages' - -const initializeArgs = async () => { - return [] -} - -module.exports = deploymentForCoreContract( - web3, - artifacts, - CeloContractName.MintGoldSchedule, - initializeArgs, - undefined, - SOLIDITY_08_PACKAGE -) diff --git a/packages/protocol/migrations_ts/29_governance.ts b/packages/protocol/migrations_ts/29_governance.ts index 56845d78a8d..634f711a5ac 100644 --- a/packages/protocol/migrations_ts/29_governance.ts +++ b/packages/protocol/migrations_ts/29_governance.ts @@ -113,7 +113,6 @@ module.exports = deploymentForCoreContract( 'Random', 'Registry', 'SortedOracles', - 'Validators', ], }, { @@ -130,7 +129,13 @@ module.exports = deploymentForCoreContract( __contractPackage: MENTO_PACKAGE, }, { - contracts: ['GasPriceMinimum'], + contracts: [ + 'GasPriceMinimum', + 'Validators', + 'EpochManager', + 'ScoreManager', + 'EpochManagerEnabler', + ], __contractPackage: SOLIDITY_08_PACKAGE, }, ] diff --git a/packages/protocol/migrations_ts/30_elect_validators.ts b/packages/protocol/migrations_ts/30_elect_validators.ts index aaf0fd1424a..17d35a0b6c7 100644 --- a/packages/protocol/migrations_ts/30_elect_validators.ts +++ b/packages/protocol/migrations_ts/30_elect_validators.ts @@ -1,6 +1,8 @@ import { NULL_ADDRESS } from '@celo/base/lib/address' import { CeloTxObject } from '@celo/connect' import { getBlsPoP, getBlsPublicKey } from '@celo/cryptographic-utils/lib/bls' +import { SOLIDITY_08_PACKAGE } from '@celo/protocol/contractPackages' +import { ArtifactsSingleton } from '@celo/protocol/lib/artifactsSingleton' import { getDeployedProxiedContract, sendTransactionWithPrivateKey, @@ -10,7 +12,8 @@ import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/addr import { toFixed } from '@celo/utils/lib/fixidity' import { signMessage } from '@celo/utils/lib/signatureUtils' import { BigNumber } from 'bignumber.js' -import { AccountsInstance, ElectionInstance, LockedGoldInstance, ValidatorsInstance } from 'types' +import { AccountsInstance, ElectionInstance, LockedGoldInstance } from 'types' +import { ValidatorsInstance } from 'types/08' import Web3 from 'web3' const truffle = require('@celo/protocol/truffle-config.js') @@ -221,6 +224,8 @@ async function registerValidator( } module.exports = async (_deployer: any, networkName: string) => { + const artifacts08 = ArtifactsSingleton.getInstance(SOLIDITY_08_PACKAGE, artifacts) + const accounts: AccountsInstance = await getDeployedProxiedContract( 'Accounts', artifacts @@ -228,7 +233,7 @@ module.exports = async (_deployer: any, networkName: string) => { const validators: ValidatorsInstance = await getDeployedProxiedContract( 'Validators', - artifacts + artifacts08 ) const lockedGold: LockedGoldInstance = await getDeployedProxiedContract( diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 330d769084d..e9210652a2c 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -10,7 +10,7 @@ "lint:sol": "solhint --version && solhint './contracts/**/*.sol' && solhint './contracts-0.8/**/*.sol'", "lint": "yarn run lint:ts && yarn run lint:sol", "clean": "rm -rf ./types/typechain && rm -rf build/* && rm -rf .0x-artifacts/* && rm -rf migrations/*.js* && rm -rf migrations_ts/*.js* && rm -rf test/**/*.js* && rm -f lib/*.js* && rm -f lib/**/*.js* && rm -f scripts/*.js* && yarn clean:foundry", - "clean:foundry": "rm -rf cache out", + "clean:foundry": "forge clean && rm -rf cache out", "test": "rm test/**/*.js ; node runTests.js", "test:scripts": "yarn ts-node scripts/run-scripts-tests.ts --testPathPattern=scripts/", "quicktest": "./scripts/bash/quicktest.sh", @@ -27,6 +27,7 @@ "prebuild": "rm -rf ./build", "determine-release-version": "yarn --silent ts-node --preferTsExts ./scripts/determine-release-version.ts", "prepare_contracts_and_abis_publishing": "yarn ts-node --preferTsExts ./scripts/prepare-contracts-and-abis-publishing.ts", + "prepare_devchain_anvil_publishing": "yarn ts-node --preferTsExts ./scripts/change-anvil-devchain-package-version.ts", "validate_abis_exports": "yarn ts-node --preferTsExts ./scripts/validate-abis-package.ts", "sourcify-publish": "yarn ts-node ./scripts/sourcify-publish.ts", "migrate": "./scripts/bash/migrate.sh", @@ -50,6 +51,12 @@ "truffle:migrate": "truffle migrate", "devchain": "yarn ts-node scripts/devchain.ts", "devchain:reset": "yarn devchain generate-tar .tmp/devchain.tar.gz --upto 29", + "anvil-devchain:start-L1": "./scripts/foundry/create_and_migrate_anvil_devchain.sh", + "anvil-devchain:start-L2": "./scripts/foundry/create_and_migrate_anvil_l2_devchain.sh", + "anvil-devchain:status": "if nc -z localhost 8546; then echo 'Devchain is serving at http://localhost:8546'; else echo 'Devchain is not running.'; fi", + "anvil-devchain:stop": "./scripts/foundry/stop_anvil.sh", + "anvil-devchain:e2e-tests": "./scripts/foundry/run_e2e_tests_in_anvil.sh", + "anvil-devchain:integration-tests": "./scripts/foundry/run_integration_tests_in_anvil.sh", "view-tags": "git for-each-ref 'refs/tags/core-contracts.*' --sort=-committerdate --format='%(color:magenta)%(committerdate:short) %(color:blue)%(tree) %(color:green)github.com/celo-org/celo-monorepo/releases/tag/%(color:yellow)%(refname:short)'", "generate-stabletoken-files": "yarn ts-node ./scripts/generate-stabletoken-files.ts", "truffle-verify": "yarn truffle run verify" @@ -102,6 +109,7 @@ "solhint": "^4.5.4", "semver": "^7.5.4", "solidity-bytes-utils": "0.0.7", + "solidity-bytes-utils-8": "npm:solidity-bytes-utils@^0.8.2", "truffle": "5.9.0", "truffle-security": "^1.7.3", "weak-map": "^1.0.5", diff --git a/packages/protocol/releaseData/initializationData/release12.json b/packages/protocol/releaseData/initializationData/release12.json index fada832b995..3f5dad7b26b 100644 --- a/packages/protocol/releaseData/initializationData/release12.json +++ b/packages/protocol/releaseData/initializationData/release12.json @@ -1,4 +1,8 @@ { + "CeloUnreleasedTreasury": ["0x000000000000000000000000000000000000ce10"], + "EpochManager": ["0x000000000000000000000000000000000000ce10", 86400], + "EpochManagerEnabler": ["0x000000000000000000000000000000000000ce10"], + "ScoreManager": [], "FeeCurrencyDirectory": [], - "MintGoldSchedule": [] + "UniswapFeeHandlerSeller": ["0x000000000000000000000000000000000000ce10", [], []] } diff --git a/packages/protocol/remappings.txt b/packages/protocol/remappings.txt index 8722ca0c8b0..58f99cbe97a 100644 --- a/packages/protocol/remappings.txt +++ b/packages/protocol/remappings.txt @@ -1,4 +1,4 @@ @celo-contracts=contracts/ @celo-contracts-8=contracts-0.8/ @test-sol=test-sol -@lib=lib \ No newline at end of file +@lib=lib diff --git a/packages/protocol/scripts/bash/contract-exclusion-regex.sh b/packages/protocol/scripts/bash/contract-exclusion-regex.sh index a92f61da77f..207684a2e79 100644 --- a/packages/protocol/scripts/bash/contract-exclusion-regex.sh +++ b/packages/protocol/scripts/bash/contract-exclusion-regex.sh @@ -3,11 +3,13 @@ set -euo pipefail # Exclude test contracts, mock contracts, contract interfaces, Proxy contracts, inlined libraries, # MultiSig contracts, and the ReleaseGold contract. -CONTRACT_EXCLUSION_REGEX=".*Test|Mock.*|I[A-Z].*|.*Proxy|MultiSig.*|ReleaseGold|SlasherUtil|UsingPrecompiles" +CONTRACT_EXCLUSION_REGEX=".*Test|Mock.*|I[A-Z].*|.*Proxy|MultiSig.*|ReleaseGold|SlasherUtil|UsingPrecompiles|CeloFeeCurrencyAdapterOwnable|FeeCurrencyAdapter|FeeCurrencyAdapterOwnable" # Before CR7, UsingRegistry and UsingRegistryV2 had been deployed, they need to keep getting deployed to keep the release reports without changes. VERSION_NUMBER=$(echo "$BRANCH" | tr -dc '0-9') +echo "VERSION_NUMBER: $VERSION_NUMBER" + if [ $VERSION_NUMBER -gt 6 ] then CONTRACT_EXCLUSION_REGEX="$CONTRACT_EXCLUSION_REGEX|^UsingRegistry" @@ -23,3 +25,15 @@ if [ $VERSION_NUMBER -eq 9 ] then CONTRACT_EXCLUSION_REGEX="$CONTRACT_EXCLUSION_REGEX|SortedOracles" fi + +if [ $VERSION_NUMBER -eq 11 ] + then + # FeeHandlerSeller is not deployed, only its children + CONTRACT_EXCLUSION_REGEX="$CONTRACT_EXCLUSION_REGEX|\\bFeeHandlerSeller\\b" +fi + +if [ $VERSION_NUMBER -eq 12 ] + then + # FeeHandlerSeller is not deployed, only its children + CONTRACT_EXCLUSION_REGEX="$CONTRACT_EXCLUSION_REGEX|\\bFeeHandlerSeller\\b" +fi diff --git a/packages/protocol/scripts/bash/release-on-devchain.sh b/packages/protocol/scripts/bash/release-on-devchain.sh index e1662cbdd8f..56e465b2a03 100755 --- a/packages/protocol/scripts/bash/release-on-devchain.sh +++ b/packages/protocol/scripts/bash/release-on-devchain.sh @@ -53,7 +53,7 @@ echo " - Checkout migrationsConfig.js at $BRANCH" git checkout $BRANCH -- migrationsConfig.js source scripts/bash/contract-exclusion-regex.sh -yarn ts-node scripts/check-backward.ts sem_check --old_contracts $BUILD_DIR/contracts --new_contracts build/contracts --exclude $CONTRACT_EXCLUSION_REGEX --output_file report.json +yarn ts-node scripts/check-backward.ts sem_check --old_contracts $BUILD_DIR/contracts --new_contracts build/contracts --exclude $CONTRACT_EXCLUSION_REGEX --new_branch $BRANCH --output_file report.json echo "- Clean git modified file" git restore migrationsConfig.js diff --git a/packages/protocol/scripts/build.ts b/packages/protocol/scripts/build.ts index fafb06ab9cc..a4455fe650d 100644 --- a/packages/protocol/scripts/build.ts +++ b/packages/protocol/scripts/build.ts @@ -39,7 +39,7 @@ function compile({ coreContractsOnly, solidity: outdir }: BuildTargets) { } exec( - `yarn run truffle compile --silent --contracts_directory=${contractPath} --contracts_build_directory=${outdir}/contracts-${contractPackage.name} --config ${contractPackage.truffleConfig}` // todo change to outdir + `yarn run truffle compile --silent --contracts_directory=${contractPath} --contracts_build_directory=${outdir}/contracts-${contractPackage.name} --config ${contractPackage.truffleConfig} --verbose-rpc` // todo change to outdir ) } diff --git a/packages/protocol/scripts/change-anvil-devchain-package-version.ts b/packages/protocol/scripts/change-anvil-devchain-package-version.ts new file mode 100644 index 00000000000..7937150f7a2 --- /dev/null +++ b/packages/protocol/scripts/change-anvil-devchain-package-version.ts @@ -0,0 +1,35 @@ +import { replacePackageVersionAndMakePublic } from '@celo/protocol/scripts/utils' +import * as child_process from 'child_process' +import * as fs from 'fs' +import * as path from 'path' +import { DEVCHAIN_ANVIL_PACKAGE_SRC_DIR, TSCONFIG_PATH } from './consts' + +function log(...args: any[]) { + // eslint-disable-next-line + console.info('[prepare-devchain-anvil]', ...args) +} + +try { + log('Setting package.json target to ES2020') + const tsconfig = JSON.parse(fs.readFileSync(TSCONFIG_PATH, 'utf8')) + tsconfig.compilerOptions.target = 'ES2020' + fs.writeFileSync(TSCONFIG_PATH, JSON.stringify(tsconfig, null, 4)) + + prepareAnvilDevchainPackage() +} finally { + log('Cleaning up') + child_process.execSync(`git checkout ${TSCONFIG_PATH}`, { stdio: 'inherit' }) +} + +function prepareAnvilDevchainPackage() { + if (process.env.RELEASE_VERSION) { + log('Replacing @celo/devchain-anvil version with RELEASE_VERSION)') + + const packageJsonPath = path.join(DEVCHAIN_ANVIL_PACKAGE_SRC_DIR, 'package.json') + replacePackageVersionAndMakePublic(packageJsonPath) + + return + } + + log('Skipping @celo/devchain-anvil package.json preparation (no RELEASE_VERSION provided)') +} diff --git a/packages/protocol/scripts/check-backward.ts b/packages/protocol/scripts/check-backward.ts index 1c77ca098ed..b671a25492b 100644 --- a/packages/protocol/scripts/check-backward.ts +++ b/packages/protocol/scripts/check-backward.ts @@ -62,6 +62,7 @@ const oldArtifactsFolder08 = path.relative(process.cwd(), argv.old_contracts + ' const newArtifactsFolder = path.relative(process.cwd(), argv.new_contracts) const newArtifactsFolder08 = path.relative(process.cwd(), argv.new_contracts + '-0.8') const newArtifactsFolders = [newArtifactsFolder, newArtifactsFolder08] +const oldArtifactsFolders = [oldArtifactsFolder, oldArtifactsFolder08] const out = (msg: string, force?: boolean): void => { if (force || !argv.quiet) { @@ -79,7 +80,7 @@ const newArtifacts08 = instantiateArtifacts(newArtifactsFolder08) try { const backward = ASTBackwardReport.create( - oldArtifactsFolder, + oldArtifactsFolders, newArtifactsFolders, [oldArtifacts, oldArtifacts08], [newArtifacts, newArtifacts08], diff --git a/packages/protocol/scripts/consts.ts b/packages/protocol/scripts/consts.ts index 2aed53633ad..01ec5386f46 100644 --- a/packages/protocol/scripts/consts.ts +++ b/packages/protocol/scripts/consts.ts @@ -8,6 +8,7 @@ export const CONTRACTS_08_PACKAGE_DESTINATION_DIR = path.join(CONTRACTS_PACKAGE_ export const ABIS_PACKAGE_SRC_DIR = path.join(__dirname, '../abis') export const ABIS_BUILD_DIR = path.join(ABIS_PACKAGE_SRC_DIR, 'src-generated') export const ABIS_DIST_DIR = path.join(ABIS_PACKAGE_SRC_DIR, 'dist') +export const DEVCHAIN_ANVIL_PACKAGE_SRC_DIR = path.join(__dirname, '../.tmp') export const BUILD_EXECUTABLE = path.join(__dirname, 'build.ts') export const TSCONFIG_PATH = path.join(ROOT_DIR, 'tsconfig.json') @@ -30,12 +31,13 @@ export const ProxyContracts = [ 'GoldTokenProxy', 'GovernanceApproverMultiSigProxy', 'GovernanceProxy', + 'GovernanceSlasherProxy', 'LockedGoldProxy', 'OdisPaymentsProxy', 'RegistryProxy', 'SortedOraclesProxy', 'UniswapFeeHandlerSellerProxy', - 'MintGoldScheduleProxy', + 'CeloUnreleasedTreasuryProxy', ] export const CoreContracts = [ @@ -51,12 +53,15 @@ export const CoreContracts = [ 'MultiSig', 'Registry', 'Freezer', - 'MintGoldSchedule', + 'CeloUnreleasedTreasury', // governance 'Election', 'EpochRewards', + 'EpochManager', + 'EpochManagerEnabler', 'Governance', + 'GovernanceSlasher', 'GovernanceApproverMultiSig', 'BlockchainParameters', 'DoubleSigningSlasher', @@ -64,6 +69,7 @@ export const CoreContracts = [ 'LockedGold', 'Validators', 'ReleaseGold', + 'ScoreManager', // identity 'Attestations', diff --git a/packages/protocol/scripts/determine-release-version.ts b/packages/protocol/scripts/determine-release-version.ts index a85f036a38f..08cfa53df44 100644 --- a/packages/protocol/scripts/determine-release-version.ts +++ b/packages/protocol/scripts/determine-release-version.ts @@ -1,6 +1,7 @@ import { execSync } from 'child_process' import { determineNextVersion, getReleaseTypeFromSemVer } from './utils' +const npmPackage = process.env.NPM_PACKAGE?.trim() || '' const npmTag = process.env.NPM_TAG?.trim() || '' const gitTag = process.env.GITHUB_TAG || '' const branchName = execSync('git branch --show-current').toString().trim() @@ -12,7 +13,7 @@ const branchName = execSync('git branch --show-current').toString().trim() // if not on a release branch a dry-run will be done unless an NPM_TAG is provided // in which case we will try to fetch the last published version with that tag and bump or use the canary to get major and start versioning from there the new tag at 0 // (e.g. `@celo/contracts@11.0.0@custom-tag.0`) -const nextVersion = determineNextVersion(gitTag, branchName, npmTag) +const nextVersion = determineNextVersion(gitTag, branchName, npmPackage, npmTag) if (nextVersion === null) { // dry-run will build the package but not publish it diff --git a/packages/protocol/scripts/foundry/constants.sh b/packages/protocol/scripts/foundry/constants.sh new file mode 100755 index 00000000000..d44bdfce4ac --- /dev/null +++ b/packages/protocol/scripts/foundry/constants.sh @@ -0,0 +1,67 @@ +# Anvil accounts +export FROM_ACCOUNT_NO_ZERO="f39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # Anvil default account (1) +export FROM_ACCOUNT="0x$FROM_ACCOUNT_NO_ZERO" # Anvil default account (1) + +# Anvil configurations (Source: https://book.getfoundry.sh/reference/anvil/) +export ANVIL_PORT=8546 +export ANVIL_RPC_URL="http://127.0.0.1:$ANVIL_PORT" +export GAS_LIMIT=50000000 +export CODE_SIZE_LIMIT=245760 # EIP-170: Contract code size limit in bytes. Useful to increase for tests. [default: 0x6000 (~25kb)] +export BALANCE=60000 # Set the balance of the accounts. [default: 10000] +export STATE_INTERVAL=1 # Interval in seconds at which the state and block environment is to be dumped to disk. +export STEPS_TRACING="--steps-tracing" # Steps tracing used for debug calls returning geth-style traces. Enable: "--steps-tracing" / Disable: "" + +# Forge migration script configurations (Source: https://book.getfoundry.sh/reference/forge/forge-script) +export MIGRATION_SCRIPT_PATH="migrations_sol/Migration.s.sol" # Path to migration script +export MIGRATION_L2_SCRIPT_PATH="migrations_sol/MigrationL2.s.sol" # Path to L2 migration script +export MIGRATION_TARGET_CONTRACT="Migration" # The name of the contract you want to run. +export MIGRATION_L2_TARGET_CONTRACT="MigrationL2" # The name of the contract you want to run. +export BROADCAST="--broadcast" # Broadcasts the transactions. Enable: "--broadcast" / Disable: "" +export SKIP_SIMULATION="" # Skips on-chain simulation. Enable: "--skip-simulation" / Disable: "" +export NON_INTERACTIVE="--non-interactive" # Remove interactive prompts which appear if the contract is near the EIP-170 size limit. +export VERBOSITY_LEVEL="-vvv" # Pass multiple times to increase the verbosity (e.g. -v, -vv, -vvv). +export REGISTRY_OWNER_ADDRESS=$FROM_ACCOUNT_NO_ZERO + +# Foundry directories and file names +export L1_DEVCHAIN_FILE_NAME="devchain.json" # Name of the file that will be published to NPM +export L2_DEVCHAIN_FILE_NAME="l2-devchain.json" # Name of the file that will be published to NPM +export TMP_FOLDER="$PWD/.tmp" +export TEMP_DIR="$PWD/.tmp/libraries" +export ANVIL_FOLDER="$TMP_FOLDER/devchain" +export SLEEP_DURATION=20 + +# Contract addresses +export REGISTRY_ADDRESS="0x000000000000000000000000000000000000ce10" +export PROXY_ADMIN_ADDRESS='0x4200000000000000000000000000000000000018' # This address is defined in `IsL2Check.sol` + +# Contract configurations +export COMMUNITY_REWARD_FRACTION="100000000000000000000" # 0.01 in fixidity format +export CARBON_OFFSETTING_PARTNER="0x22579CA45eE22E2E16dDF72D955D6cf4c767B0eF" +export CARBON_OFFSETTING_FRACTION="10000000000000000000" # 0.001 in fixidity format +export REGISTRY_STORAGE_LOCATION="0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103" # Position is bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); +GOLD_TOKEN_CELO_SUPPLY_CAP=1000000000 # `GoldToken.CELO_SUPPLY_CAP()` +GOLD_TOKEN_TOTAL_SUPPLY=700000000 # Arbitrary amount chosen to be approximately equal to `GoldToken.totalSupply()` on the L1 Mainnet (695,313,643 CELO as of this commit). +export CELO_UNRELEASED_TREASURY_INITIAL_BALANCE="$(($GOLD_TOKEN_CELO_SUPPLY_CAP - $GOLD_TOKEN_TOTAL_SUPPLY))" # During the real L2 genesis, the VM will calculate and set an appropriate balance. + +# Contract libraries +export LIBRARIES_PATH=("contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol:AddressSortedLinkedListWithMedian" + "contracts/common/Signatures.sol:Signatures" + "contracts-0.8/common/linkedlists/AddressLinkedList.sol:AddressLinkedList" + "contracts/common/linkedlists/AddressSortedLinkedList.sol:AddressSortedLinkedList" + "contracts/common/linkedlists/IntegerSortedLinkedList.sol:IntegerSortedLinkedList" + "contracts/governance/Proposals.sol:Proposals" +) +export LIBRARY_DEPENDENCIES_PATH=( + "contracts/common/FixidityLib.sol" + "contracts/common/linkedlists/LinkedList.sol" + "contracts-0.8/common/linkedlists/LinkedList.sol" + "contracts/common/linkedlists/SortedLinkedList.sol" + "contracts/common/linkedlists/SortedLinkedListWithMedian.sol" + "lib/openzeppelin-contracts/contracts/math/SafeMath.sol" + "lib/openzeppelin-contracts8/contracts/utils/math/SafeMath.sol" + "lib/openzeppelin-contracts/contracts/math/Math.sol" + "lib/openzeppelin-contracts/contracts/cryptography/ECDSA.sol" + "lib/openzeppelin-contracts/contracts/utils/Address.sol" + "lib/solidity-bytes-utils/contracts/BytesLib.sol" + "lib/celo-foundry/lib/forge-std/src/console.sol" +) diff --git a/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh b/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh new file mode 100755 index 00000000000..1310cc63307 --- /dev/null +++ b/packages/protocol/scripts/foundry/create_and_migrate_anvil_devchain.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -euo pipefail + +### This scripts sets up a local Anvil instance, deploys libraries, precompiles, and runs migrations + +# Read environment variables and constants +source $PWD/scripts/foundry/constants.sh + +# Keeping track of start time to measure how long it takes to run the script entirely +START_TIME=$SECONDS + +echo "Forge version: $(forge --version)" + +# Create temporary directory +if [ -d "$TMP_FOLDER" ]; then + # Remove temporary directory first it if exists + echo "Removing existing temporary folder..." + rm -rf $TMP_FOLDER +fi +mkdir -p $TMP_FOLDER + +# Start a local anvil instance +source $PWD/scripts/foundry/start_anvil.sh + +# Deploy libraries to the anvil instance +source $PWD/scripts/foundry/deploy_libraries.sh +echo "Library flags are: $LIBRARY_FLAGS" + +# Build all contracts with deployed libraries +# Including contracts that depend on libraries. This step replaces the library placeholder +# in the bytecode with the address of the actually deployed library. +echo "Compiling with libraries..." +time FOUNDRY_PROFILE=devchain forge build $LIBRARY_FLAGS + +# Deploy precompile contracts +source $PWD/scripts/foundry/deploy_precompiles.sh + +echo "Setting Registry Proxy" +PROXY_DEPLOYED_BYTECODE=$(jq -r '.deployedBytecode.object' ./out/Proxy.sol/Proxy.json) +cast rpc anvil_setCode $REGISTRY_ADDRESS $PROXY_DEPLOYED_BYTECODE --rpc-url $ANVIL_RPC_URL + +# Sets the storage of the registry so that it has an owner we control +echo "Setting Registry owner" +cast rpc \ +anvil_setStorageAt \ +$REGISTRY_ADDRESS $REGISTRY_STORAGE_LOCATION "0x000000000000000000000000$REGISTRY_OWNER_ADDRESS" \ +--rpc-url $ANVIL_RPC_URL + +# Run migrations +echo "Running migration script... " +forge script \ + $MIGRATION_SCRIPT_PATH \ + --target-contract $MIGRATION_TARGET_CONTRACT \ + --sender $FROM_ACCOUNT \ + --unlocked \ + $VERBOSITY_LEVEL \ + $BROADCAST \ + $SKIP_SIMULATION \ + $NON_INTERACTIVE \ + $LIBRARY_FLAGS \ + --rpc-url $ANVIL_RPC_URL || { echo "Migration script failed"; exit 1; } + +CELO_EPOCH_REWARDS_ADDRESS=$( + cast call \ + $REGISTRY_ADDRESS \ + "getAddressForStringOrDie(string calldata identifier)(address)" \ + "EpochRewards" \ + --rpc-url $ANVIL_RPC_URL +) + +echo "Setting storage of EpochRewards start time to same value as on mainnet" +# Storage slot of start time is 2 and the value is 1587587214 which is identical to mainnet +cast rpc \ +anvil_setStorageAt \ +$CELO_EPOCH_REWARDS_ADDRESS 2 "0x000000000000000000000000000000000000000000000000000000005ea0a88e" \ +--rpc-url $ANVIL_RPC_URL + +# Keeping track of the finish time to measure how long it takes to run the script entirely +ELAPSED_TIME=$(($SECONDS - $START_TIME)) +echo "Migration script total elapsed time: $ELAPSED_TIME seconds" + +# this helps to make sure that devchain state is actually being saved +sleep $SLEEP_DURATION + +if [[ "${KEEP_DEVCHAIN_FOLDER:-}" == "true" ]]; then + cp $ANVIL_FOLDER/state.json $TMP_FOLDER/$L1_DEVCHAIN_FILE_NAME + echo "Keeping devchain folder as per flag." +else + # Rename devchain artifact and remove unused directory + mv $ANVIL_FOLDER/state.json $TMP_FOLDER/$L1_DEVCHAIN_FILE_NAME + rm -rf $ANVIL_FOLDER +fi diff --git a/packages/protocol/scripts/foundry/create_and_migrate_anvil_l2_devchain.sh b/packages/protocol/scripts/foundry/create_and_migrate_anvil_l2_devchain.sh new file mode 100755 index 00000000000..25f2ee31a37 --- /dev/null +++ b/packages/protocol/scripts/foundry/create_and_migrate_anvil_l2_devchain.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Read environment variables and constants +source $PWD/scripts/foundry/constants.sh + +export KEEP_DEVCHAIN_FOLDER=true + +# Generate and run L1 devchain +echo "Generating and running L1 devchain before activating L2..." +source $PWD/scripts/foundry/create_and_migrate_anvil_devchain.sh + +# Activate L2 by deploying arbitrary bytecode to the proxy admin address. +# Note: This can't be done from the migration script +ARBITRARY_BYTECODE=$(cast format-bytes32-string "L2 is activated") +cast rpc anvil_setCode \ + $PROXY_ADMIN_ADDRESS $ARBITRARY_BYTECODE \ + --rpc-url $ANVIL_RPC_URL + +# Fetch address of Celo distribution +CELO_UNRELEASED_TREASURY_ADDRESS=$( + cast call \ + $REGISTRY_ADDRESS \ + "getAddressForStringOrDie(string calldata identifier)(address)" \ + "CeloUnreleasedTreasury" \ + --rpc-url $ANVIL_RPC_URL +) + +# Set the balance of the CeloUnreleasedTreasury (like the Celo client would do during L2 genesis) +# Note: This can't be done from the migration script, because CeloUnreleasedTreasury.sol does not +# implement the receive function nor does it allow ERC20 transfers. This is the only way I +# managed to give the CeloUnreleasedTreasury a balance. +echo "Setting CeloUnreleasedTreasury balance..." +HEX_CELO_UNRELEASED_TREASURY_INITIAL_BALANCE=$(cast to-hex $CELO_UNRELEASED_TREASURY_INITIAL_BALANCE"000000000000000000") +cast rpc \ + anvil_setBalance \ + $CELO_UNRELEASED_TREASURY_ADDRESS $HEX_CELO_UNRELEASED_TREASURY_INITIAL_BALANCE \ + --rpc-url $ANVIL_RPC_URL + +# Run L2 migrations +echo "Running L2 migration script... " +forge script \ + $MIGRATION_L2_SCRIPT_PATH \ + --target-contract $MIGRATION_L2_TARGET_CONTRACT \ + --sender $FROM_ACCOUNT \ + --unlocked \ + $VERBOSITY_LEVEL \ + $BROADCAST \ + $SKIP_SIMULATION \ + $NON_INTERACTIVE \ + --rpc-url $ANVIL_RPC_URL || { echo "Migration script failed"; exit 1; } + +# Give anvil enough time to save the state +sleep $SLEEP_DURATION + +# # Save L2 state so it can published to NPM +mv $ANVIL_FOLDER/state.json $TMP_FOLDER/$L2_DEVCHAIN_FILE_NAME +echo "Saved anvil L2 state to $TMP_FOLDER/$L2_DEVCHAIN_FILE_NAME" + +rm -rf $ANVIL_FOLDER diff --git a/packages/protocol/migrations_sol/deploy_libraries.sh b/packages/protocol/scripts/foundry/deploy_libraries.sh similarity index 61% rename from packages/protocol/migrations_sol/deploy_libraries.sh rename to packages/protocol/scripts/foundry/deploy_libraries.sh index 4a9c5d5ac82..88118567b23 100644 --- a/packages/protocol/migrations_sol/deploy_libraries.sh +++ b/packages/protocol/scripts/foundry/deploy_libraries.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -# Name of temporary directory -TEMP_DIR_NAME=".tmp/libraries" -TEMP_DIR="$PWD/$TEMP_DIR_NAME" +# Read environment variables and constants +source $PWD/scripts/foundry/constants.sh # Create a temporary directory or remove it first it if exists if [ -d "$TEMP_DIR" ]; then @@ -13,14 +12,6 @@ fi mkdir $TEMP_DIR # Copy libraries to the directory -LIBRARIES_PATH=("contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol:AddressSortedLinkedListWithMedian" - "contracts/common/Signatures.sol:Signatures" - "contracts/common/linkedlists/AddressLinkedList.sol:AddressLinkedList" - "contracts/common/linkedlists/AddressSortedLinkedList.sol:AddressSortedLinkedList" - "contracts/common/linkedlists/IntegerSortedLinkedList.sol:IntegerSortedLinkedList" - "contracts/governance/Proposals.sol:Proposals" -) - for LIB_PATH in "${LIBRARIES_PATH[@]}"; do IFS=":" read -r SOURCE DEST <<< "$LIB_PATH" DEST_FILE="$TEMP_DIR/$SOURCE" @@ -30,23 +21,11 @@ for LIB_PATH in "${LIBRARIES_PATH[@]}"; do cp "$SOURCE" "$DEST_FILE" done -# Copy dependencies of the libraries to the directory -LIBRARY_DEPENDENCIES_PATH=( - "contracts/common/FixidityLib.sol" - "contracts/common/linkedlists/LinkedList.sol" - "contracts/common/linkedlists/SortedLinkedList.sol" - "contracts/common/linkedlists/SortedLinkedListWithMedian.sol" - "lib/openzeppelin-contracts/contracts/math/SafeMath.sol" - "lib/openzeppelin-contracts/contracts/math/Math.sol" - "lib/openzeppelin-contracts/contracts/cryptography/ECDSA.sol" - "lib/openzeppelin-contracts/contracts/utils/Address.sol" - "lib/solidity-bytes-utils/contracts/BytesLib.sol" -) - # Creating two variables for better readability SOURCE_DIR=$PWD DEST_DIR=$TEMP_DIR +# Copy dependencies of the libraries to the directory for LIB_PATH in "${LIBRARY_DEPENDENCIES_PATH[@]}"; do # Creates directory for the dependency, including any necessary parent directories mkdir -p $DEST_DIR/$(dirname $LIB_PATH) @@ -73,7 +52,7 @@ for LIB_PATH in "${LIBRARIES_PATH[@]}"; do # LIB_PATH = "contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol:AddressSortedLinkedListWithMedian" # LIB_NAME = AddressSortedLinkedListWithMedian echo "Deploying library: $LIB_NAME" - create_library_out=`forge create $LIB_PATH --from $FROM_ACCOUNT --rpc-url http://127.0.0.1:$ANVIL_PORT --unlocked --json` + create_library_out=`forge create $LIB_PATH --from $FROM_ACCOUNT --rpc-url $ANVIL_RPC_URL --unlocked --json` LIB_ADDRESS=`echo $create_library_out | jq -r '.deployedTo'` # Constructing library flag so the remaining contracts can be built and linkeded to these libraries LIBRARY_FLAGS="$LIBRARY_FLAGS --libraries $LIB_PATH:$LIB_ADDRESS" diff --git a/packages/protocol/scripts/foundry/deploy_precompiles.sh b/packages/protocol/scripts/foundry/deploy_precompiles.sh new file mode 100755 index 00000000000..1ebbae44a1d --- /dev/null +++ b/packages/protocol/scripts/foundry/deploy_precompiles.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Read environment variables and constants +source $PWD/scripts/foundry/constants.sh + +echo "Deploying precompiles:" + +# TODO insteadl of deploying one by one +EpochSizeAddress=0x00000000000000000000000000000000000000f8 +EpochSizeBytecode=`cat ./out/EpochSizePrecompile.sol/EpochSizePrecompile.json | jq -r '.deployedBytecode.object'` +cast rpc anvil_setCode --rpc-url $ANVIL_RPC_URL $EpochSizeAddress $EpochSizeBytecode + +ProofOfPossesionAddress=0x00000000000000000000000000000000000000fb +ProofOfPossesionBytecode=`cat ./out/ProofOfPossesionPrecompile.sol/ProofOfPossesionPrecompile.json | jq -r '.deployedBytecode.object'` +cast rpc anvil_setCode --rpc-url $ANVIL_RPC_URL $ProofOfPossesionAddress $ProofOfPossesionBytecode + +NumberValidatorsInCurrentSetPrecompileAddress=0x00000000000000000000000000000000000000f9 +NumberValidatorsInCurrentSetPrecompileBytecode=`cat ./out/NumberValidatorsInCurrentSetPrecompile.sol/NumberValidatorsInCurrentSetPrecompile.json | jq -r '.deployedBytecode.object'` +cast rpc anvil_setCode --rpc-url $ANVIL_RPC_URL $NumberValidatorsInCurrentSetPrecompileAddress $NumberValidatorsInCurrentSetPrecompileBytecode + +ValidatorSignerAddressFromCurrentSetAddress=0x00000000000000000000000000000000000000fa +ValidatorSignerAddressFromCurrentSetBytecode=`cat ./out/ValidatorSignerAddressFromCurrentSetPrecompile.sol/ValidatorSignerAddressFromCurrentSetPrecompile.json | jq -r '.deployedBytecode.object'` +cast rpc anvil_setCode --rpc-url $ANVIL_RPC_URL $ValidatorSignerAddressFromCurrentSetAddress $ValidatorSignerAddressFromCurrentSetBytecode diff --git a/packages/protocol/scripts/foundry/run_e2e_tests_in_anvil.sh b/packages/protocol/scripts/foundry/run_e2e_tests_in_anvil.sh new file mode 100755 index 00000000000..781543a9b61 --- /dev/null +++ b/packages/protocol/scripts/foundry/run_e2e_tests_in_anvil.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Read environment variables and constants +source $PWD/scripts/foundry/constants.sh + +# Generate and run devchain +echo "Generating and running devchain before running e2e tests..." +source $PWD/scripts/foundry/create_and_migrate_anvil_devchain.sh + + +# Run e2e tests +echo "Running e2e tests..." +time FOUNDRY_PROFILE=devchain forge test \ +-vvv \ +--match-path "*test-sol/devchain/e2e/*" \ +--isolate \ +--fork-url $ANVIL_RPC_URL + +# Stop devchain +echo "Stopping devchain..." +source $PWD/scripts/foundry/stop_anvil.sh diff --git a/packages/protocol/scripts/foundry/run_integration_tests_in_anvil.sh b/packages/protocol/scripts/foundry/run_integration_tests_in_anvil.sh new file mode 100755 index 00000000000..bdcd06f4f09 --- /dev/null +++ b/packages/protocol/scripts/foundry/run_integration_tests_in_anvil.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Read environment variables and constants +source $PWD/scripts/foundry/constants.sh + +# Generate and run devchain +echo "Generating and running devchain before running integration tests..." +source $PWD/scripts/foundry/create_and_migrate_anvil_devchain.sh + +# Run integration tests +echo "Running integration tests..." +time FOUNDRY_PROFILE=devchain forge test \ +-vvv \ +--match-path "test-sol/devchain/migration/*" \ +--fork-url $ANVIL_RPC_URL + +# Stop devchain +echo "Stopping devchain..." +source $PWD/scripts/foundry/stop_anvil.sh \ No newline at end of file diff --git a/packages/protocol/scripts/foundry/start_anvil.sh b/packages/protocol/scripts/foundry/start_anvil.sh new file mode 100755 index 00000000000..ec802d5d980 --- /dev/null +++ b/packages/protocol/scripts/foundry/start_anvil.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Read environment variables and constants +source $PWD/scripts/foundry/constants.sh + +timestamp=`date -Iseconds` + +mkdir -p $ANVIL_FOLDER +echo "Anvil state will be saved to $ANVIL_FOLDER" + +# create package.json +echo "{\"name\": \"@celo/devchain-anvil\",\"version\": \"1.0.0\",\"repository\": { \"url\": \"https://github.com/celo-org/celo-monorepo\", \"directory\": \"packages/protocol/migrations_sol\" },\"homepage\": \"https://github.com/celo-org/celo-monorepo/blob/master/packages/protocol/migrations_sol/README.md\",\"description\": \"Anvil based devchain that contains core smart contracts of celo\",\"author\":\"Celo\",\"license\": \"LGPL-3.0\"}" > $TMP_FOLDER/package.json + +cp $PWD/migrations_sol/README.md $TMP_FOLDER/README.md + +if nc -z localhost $ANVIL_PORT; then + echo "Port already used" + kill $(lsof -t -i:$ANVIL_PORT) + sleep 5 + echo "Killed previous Anvil" +fi + +# Start anvil +anvil \ +--port $ANVIL_PORT \ +--dump-state $ANVIL_FOLDER \ +--state-interval $STATE_INTERVAL \ +--gas-limit $GAS_LIMIT \ +--code-size-limit $CODE_SIZE_LIMIT \ +--balance $BALANCE \ +--steps-tracing & +# For context "&" tells the shell to start a command as a background process. +# This allows you to continue executing other commands without waiting for the background command to finish. + +# alternatively: +# ANVIL_PID=`lsof -i tcp:8545 | tail -n 1 | awk '{print $2}'` + +export ANVIL_PID=$! + +echo "Waiting Anvil to launch on $ANVIL_PORT..." + +while ! nc -z localhost $ANVIL_PORT; do + sleep 0.1 # wait for 1/10 of the second before check again +done + +# enabled logging +cast rpc anvil_setLoggingEnabled true --rpc-url $ANVIL_RPC_URL + +echo "Anvil launched" +sleep 1 diff --git a/packages/protocol/scripts/foundry/stop_anvil.sh b/packages/protocol/scripts/foundry/stop_anvil.sh new file mode 100755 index 00000000000..f492cf26ee0 --- /dev/null +++ b/packages/protocol/scripts/foundry/stop_anvil.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +# A small script to terminate any instance of anvil currently serving at localhost. + +# Read environment variables and constants +source $PWD/scripts/foundry/constants.sh + +if nc -z localhost $ANVIL_PORT; then + kill $(lsof -t -i:$ANVIL_PORT) + echo "Killed Anvil" +fi \ No newline at end of file diff --git a/packages/protocol/scripts/prepare-contracts-and-abis-publishing.ts b/packages/protocol/scripts/prepare-contracts-and-abis-publishing.ts index 7a58e596dc3..25ba22335eb 100644 --- a/packages/protocol/scripts/prepare-contracts-and-abis-publishing.ts +++ b/packages/protocol/scripts/prepare-contracts-and-abis-publishing.ts @@ -1,3 +1,4 @@ +import { Exports, replacePackageVersionAndMakePublic } from '@celo/protocol/scripts/utils' import * as child_process from 'child_process' import * as fs from 'fs' import * as path from 'path' @@ -116,11 +117,6 @@ function createIndex() { fs.writeFileSync(path.join(ABIS_BUILD_DIR, 'index.ts'), reExports.join('\n')) } -type Exports = Record< - string, - { import?: string; require?: string; types?: string; default?: string } -> - // Helper functions function prepareTargetTypesExports() { const exports: Exports = {} @@ -238,38 +234,22 @@ function processRawJsonsAndPrepareExports() { return exports } -type JSON = Record - -function replacePackageVersionAndMakePublic( - packageJsonPath: string, - onDone?: (json: JSON) => void -) { - const json: JSON = JSON.parse(fs.readFileSync(packageJsonPath).toString()) +function prepareAbisPackageJson(exports: Exports) { + log('Preparing @celo/abis package.json') + const packageJsonPath = path.join(ABIS_PACKAGE_SRC_DIR, 'package.json') if (process.env.RELEASE_VERSION) { - log(`Replacing ${json.name as string} version with provided RELEASE_VERSION`) + log('Replacing @celo/abis version with RELEASE_VERSION)') - json.version = process.env.RELEASE_VERSION - json.private = false - } else { - log('No RELEASE_VERSION provided') - } + replacePackageVersionAndMakePublic(packageJsonPath, (json) => { + log('Setting @celo/abis exports') + json.exports = exports + }) - if (onDone !== undefined) { - onDone(json) + return } - fs.writeFileSync(packageJsonPath, JSON.stringify(json, null, 2)) -} - -function prepareAbisPackageJson(exports: Exports) { - log('Preparing @celo/abis package.json') - const packageJsonPath = path.join(ABIS_PACKAGE_SRC_DIR, 'package.json') - - replacePackageVersionAndMakePublic(packageJsonPath, (json) => { - log('Setting @celo/abis exports') - json.exports = exports - }) + log('Skipping @celo/abis package.json preparation (no RELEASE_VERSION provided)') } function prepareContractsPackage() { diff --git a/packages/protocol/scripts/truffle/make-release.ts b/packages/protocol/scripts/truffle/make-release.ts index af7911955b2..440fd8a4566 100644 --- a/packages/protocol/scripts/truffle/make-release.ts +++ b/packages/protocol/scripts/truffle/make-release.ts @@ -118,9 +118,6 @@ const deployImplementation = async ( // without this delay it sometimes fails with ProviderError await delay(getRandomNumber(1, 1000)) - console.log('gas update in2') - console.log('dryRun', dryRun) - const bytecodeSize = (Contract.bytecode.length - 2) / 2 console.log('Bytecode size in bytes:', bytecodeSize) diff --git a/packages/protocol/scripts/truffle/verify-bytecode.ts b/packages/protocol/scripts/truffle/verify-bytecode.ts index 9ca918b3029..b161d4ee6a9 100644 --- a/packages/protocol/scripts/truffle/verify-bytecode.ts +++ b/packages/protocol/scripts/truffle/verify-bytecode.ts @@ -36,8 +36,8 @@ const argv = require('minimist')(process.argv.slice(2), { }) const artifactsDirectory = argv.build_artifacts ? argv.build_artifacts : './build/contracts' -const artifacts08Directory = argv.build_artifacts08 - ? argv.build_artifacts08 +const artifacts08Directory = argv.build_artifacts + ? `${argv.build_artifacts}-0.8` : './build/contracts-0.8' const branch = (argv.branch ? argv.branch : '') as string const network = argv.network ?? 'development' diff --git a/packages/protocol/scripts/utils.test.ts b/packages/protocol/scripts/utils.test.ts index d360cb5d172..f438cc927f7 100644 --- a/packages/protocol/scripts/utils.test.ts +++ b/packages/protocol/scripts/utils.test.ts @@ -23,6 +23,7 @@ describe('utils', () => { const nextVersion = determineNextVersion( 'core-contracts.v11.2.3.post-audit', 'release/core-contracts/11.0.0', + '@celo/contracts', 'alpha' ) @@ -33,6 +34,7 @@ describe('utils', () => { const nextVersion = determineNextVersion( 'core-contracts.v11.2.3.pre-audit', 'release/core-contracts/11.0.0', + '@celo/contracts', 'alpha' ) @@ -42,7 +44,12 @@ describe('utils', () => { it('determines for release git branch when major version matches', () => { execSyncMock.mockReturnValue('11.2.3') - const nextVersion = determineNextVersion('', 'release/core-contracts/11.2.3', 'alpha') + const nextVersion = determineNextVersion( + '', + 'release/core-contracts/11.2.3', + '@celo/contracts', + 'alpha' + ) expect(execSyncMock).toHaveBeenCalledTimes(1) expect(execSyncMock).toHaveBeenNthCalledWith( @@ -56,7 +63,12 @@ describe('utils', () => { it("determines for release git branch when major version doesn't match", () => { execSyncMock.mockReturnValue('11.2.3') - const nextVersion = determineNextVersion('', 'release/core-contracts/12.0.0', 'alpha') + const nextVersion = determineNextVersion( + '', + 'release/core-contracts/12.0.0', + '@celo/contracts', + 'alpha' + ) expect(execSyncMock).toHaveBeenCalledTimes(1) expect(execSyncMock).toHaveBeenNthCalledWith( @@ -72,7 +84,12 @@ describe('utils', () => { return '10.2.4-alpha.0' }) - const nextVersion = determineNextVersion('', 'dev/some-branch-name', 'alpha') + const nextVersion = determineNextVersion( + '', + 'dev/some-branch-name', + '@celo/contracts', + 'alpha' + ) expect(execSyncMock).toHaveBeenCalledTimes(1) expect(execSyncMock).toHaveBeenNthCalledWith( @@ -92,7 +109,12 @@ describe('utils', () => { return '10.1.2-canary.2' }) - const nextVersion = determineNextVersion('', 'dev/some-branch-name', 'alpha') + const nextVersion = determineNextVersion( + '', + 'dev/some-branch-name', + '@celo/contracts', + 'alpha' + ) expect(execSyncMock).toHaveBeenCalledTimes(2) expect(execSyncMock).toHaveBeenNthCalledWith( @@ -110,13 +132,18 @@ describe('utils', () => { }) it("doesn't determine anything when wrong tag is provided", () => { - const nextVersion = determineNextVersion('', '', 'tag-with-dashes-at-the-end-') + const nextVersion = determineNextVersion( + '', + '', + '@celo/contracts', + 'tag-with-dashes-at-the-end-' + ) expect(nextVersion).toBeNull() }) it("doesn't determine anything when nothing is provided", () => { - const nextVersion = determineNextVersion('', '', '') + const nextVersion = determineNextVersion('', '', '', '') expect(nextVersion).toBeNull() }) diff --git a/packages/protocol/scripts/utils.ts b/packages/protocol/scripts/utils.ts index 5ddcce2e53b..30f44698deb 100644 --- a/packages/protocol/scripts/utils.ts +++ b/packages/protocol/scripts/utils.ts @@ -1,12 +1,21 @@ import { execSync } from 'child_process' +import * as fs from 'fs' import { SemVer } from 'semver' const DAILY_RELEASE_TAG = 'canary' const WORKING_RELEASE_BRANCH_PREFIX = 'release/core-contracts/' +export type Exports = Record< + string, + { import?: string; require?: string; types?: string; default?: string } +> + +export type JSON = Record + export const determineNextVersion = ( gitTag: string, gitBranch: string, + npmPackage: string, npmTag: string ): SemVer | null => { let nextVersion: SemVer | null = null @@ -21,7 +30,7 @@ export const determineNextVersion = ( `${tempVersion.major}.${tempVersion.minor}.${tempVersion.patch}-pre-audit.0` ) } else if (gitBranch.startsWith(WORKING_RELEASE_BRANCH_PREFIX)) { - const lastVersion = getPreviousVersion(DAILY_RELEASE_TAG, 'latest') + const lastVersion = getPreviousVersion(npmPackage, DAILY_RELEASE_TAG, 'latest') const lastVersionSemVer = new SemVer(lastVersion) // since branch names are of the form release/core-contracts.XX we can check the major from the branch name @@ -34,7 +43,7 @@ export const determineNextVersion = ( ) nextVersion.major = parseInt(major, 10) } else if (isValidNpmTag(npmTag)) { - const lastVersion = getPreviousVersion(npmTag, DAILY_RELEASE_TAG) + const lastVersion = getPreviousVersion(npmPackage, npmTag, DAILY_RELEASE_TAG) nextVersion = new SemVer(lastVersion).inc('prerelease', npmTag) } @@ -46,16 +55,20 @@ export function isValidNpmTag(tag?: string) { } // get the previous version for this tag or if not exists find the previous for the fallback -export function getPreviousVersion(tag = DAILY_RELEASE_TAG, fallbackTag = 'latest') { +export function getPreviousVersion( + npmPackage: string, + tag = DAILY_RELEASE_TAG, + fallbackTag = 'latest' +) { try { - return fetchVersionFromNpm(tag) + return fetchVersionFromNpm(npmPackage, tag) } catch (e) { - return fetchVersionFromNpm(fallbackTag) + return fetchVersionFromNpm(npmPackage, fallbackTag) } } -export function fetchVersionFromNpm(tag: string) { - return execSync(`npm view @celo/contracts@${tag} version`, { +export function fetchVersionFromNpm(npmPackage: string, tag: string) { + return execSync(`npm view ${npmPackage}@${tag} version`, { stdio: ['ignore', 'pipe', 'ignore'], }) .toString() @@ -73,3 +86,25 @@ export function getVersionFromGitTag(matchedTag: RegExpMatchArray) { export function getReleaseTypeFromSemVer(version: SemVer): string | number { return version.prerelease.length ? version.prerelease[0] : 'latest' } + +export function replacePackageVersionAndMakePublic( + packageJsonPath: string, + onDone?: (json: JSON) => void +) { + const json: JSON = JSON.parse(fs.readFileSync(packageJsonPath).toString()) + + if (process.env.RELEASE_VERSION) { + console.info(`Replacing ${json.name as string} version with provided RELEASE_VERSION`) + + json.version = process.env.RELEASE_VERSION + json.private = false + } else { + console.info('No RELEASE_VERSION provided') + } + + if (onDone !== undefined) { + onDone(json) + } + + fs.writeFileSync(packageJsonPath, JSON.stringify(json, null, 2)) +} diff --git a/packages/protocol/test-sol/README.md b/packages/protocol/test-sol/README.md index 772c23d40f8..e180efd1a64 100644 --- a/packages/protocol/test-sol/README.md +++ b/packages/protocol/test-sol/README.md @@ -6,6 +6,14 @@ You can build this project by simply running forge build ``` + +**Note**: Due to a regression in Foundry, you might need to use an older +version. You can install the most recent version verified to work by running + +```bash +foundryup --version nightly-f625d0fa7c51e65b4bf1e8f7931cd1c6e2e285e9 +``` + ### Testing We are in the process of migrating our tests to use [Foundry](https://book.getfoundry.sh/). The tests in this folder have already been migrated from [Truffle](../test). diff --git a/packages/protocol/test-sol/Readme.md b/packages/protocol/test-sol/Readme.md deleted file mode 100644 index 772c23d40f8..00000000000 --- a/packages/protocol/test-sol/Readme.md +++ /dev/null @@ -1,56 +0,0 @@ -### Building - -You can build this project by simply running - -```bash -forge build -``` - -### Testing - -We are in the process of migrating our tests to use [Foundry](https://book.getfoundry.sh/). The tests in this folder have already been migrated from [Truffle](../test). - -To run tests with Foundry there's no need to `yarn` or manage any Javascript dependencies. Instead, run - -```bash -forge test -``` - -This will run all tests in this folder. To run only a specific file you can use - -```bash -forge test --match-path ./path/to/file.t.sol -``` - -To run only a specific contract in a test file, you can use - -```bash -forge test --match-contract CONTRACT_NAME -``` - -To run only a specific test, you can use - -```bash -forge test --match-test test_ToMatch -``` - -You can specify a verbosity level with the `-v`, `-vv`, `-vvv`, and `-vvvv` flags. The more `v`s you put the more verbose the output will be. - -Putting it all together, you might run something like - -```bash -forge test --match-path ./path/to/file.t.sol --match-test test_ToMatch -vvv -``` - -You can read more about the `forge test` command [here](https://book.getfoundry.sh/reference/forge/forge-test). - -To skip a specific test, you can add `vm.skip(true);` as the first line of the test. - -If a test name begins with `testFail` rather than `test`, foundry will expect the test to fail / revert. - -Please follow the naming convention `test_NameOfTest` / `testFail_NameOfTest`. - -If you're new to Forge / Foundry, we recommend looking through the [Cheatcode Reference](https://book.getfoundry.sh/cheatcodes/) for a list of useful commands that make writing tests easier. - - - diff --git a/packages/protocol/test-sol/TestWithUtils.sol b/packages/protocol/test-sol/TestWithUtils.sol new file mode 100644 index 00000000000..6fc2f8e24d3 --- /dev/null +++ b/packages/protocol/test-sol/TestWithUtils.sol @@ -0,0 +1,173 @@ +pragma solidity ^0.5.13; + +import "celo-foundry/Test.sol"; +import "openzeppelin-solidity/contracts/utils/EnumerableSet.sol"; +import { TestConstants } from "@test-sol/constants.sol"; +import "@test-sol/unit/common/mocks/MockEpochManager.sol"; +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts-8/common/interfaces/IPrecompiles.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; +import "@celo-contracts-8/common/IsL2Check.sol"; +import "@celo-contracts/common/PrecompilesOverrideV2.sol"; + +contract TestWithUtils is Test, TestConstants, IsL2Check, PrecompilesOverrideV2 { + using EnumerableSet for EnumerableSet.AddressSet; + + EnumerableSet.AddressSet addressSet; + IRegistry registry; + MockEpochManager public epochManager; + + function setUp() public { + setupRegistry(); + setupEpochManager(); + } + + function setupRegistry() public { + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); + registry = IRegistry(REGISTRY_ADDRESS); + } + + function setupEpochManager() public { + epochManager = new MockEpochManager(); + + registry.setAddressFor(EpochManagerContract, address(epochManager)); + } + + function timeTravel(uint256 timeDelta) public { + vm.warp(block.timestamp + timeDelta); + } + + function blockTravel(uint256 blockDelta) public { + vm.roll(block.number + blockDelta); + } + + // XXX: this function only increases the block number and timestamp, but does not actually change epoch. + // XXX: you must start and finish epoch processing to change epochs. + function travelNL2Epoch(uint256 n) public { + uint256 blocksInEpoch = L2_BLOCK_IN_EPOCH; + blockTravel(n * blocksInEpoch); + timeTravel(n * DAY); + epochManager.setCurrentEpochNumber(epochManager.getCurrentEpochNumber() + n); + } + + function travelNEpoch(uint256 n) public { + if (isL2()) { + travelNL2Epoch(n); + } else { + blockTravel((n * ph.epochSize()) + 1); + } + } + + function _whenL2() public { + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + } + + function whenL2WithEpochManagerInitialization() internal { + uint256 l1EpochNumber = getEpochNumber(); + + address[] memory _elected = new address[](2); + _elected[0] = actor("validator"); + _elected[1] = actor("otherValidator"); + + _whenL2(); + + epochManager.initializeSystem(l1EpochNumber, block.number, _elected); + } + + function assertAlmostEqual(uint256 actual, uint256 expected, uint256 margin) public { + uint256 diff = actual > expected ? actual - expected : expected - actual; + assertTrue(diff <= margin, string(abi.encodePacked("Difference is ", uintToStr(diff)))); + } + + function uintToStr(uint256 _i) internal pure returns (string memory _uintAsString) { + uint256 number = _i; + if (number == 0) { + return "0"; + } + uint256 j = number; + uint256 len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint256 k = len - 1; + while (number != 0) { + bstr[k--] = bytes1(uint8(48 + (number % 10))); + number /= 10; + } + return string(bstr); + } + + function arraysEqual(address[] memory arr1, address[] memory arr2) public returns (bool) { + if (arr1.length != arr2.length) { + return false; // Arrays of different lengths cannot be equal + } + + // Add addresses from arr1 to the set + for (uint256 i = 0; i < arr1.length; i++) { + addressSet.add(arr1[i]); + } + + // Check if each address in arr2 is in the set + for (uint256 i = 0; i < arr2.length; i++) { + if (!addressSet.contains(arr2[i])) { + clearSet(arr1); + return false; + } + } + + clearSet(arr1); + return true; + } + + function clearSet(address[] memory arr1) private { + for (uint256 i = 0; i < arr1.length; i++) { + addressSet.remove(arr1[i]); + } + } + + // Generates pseudo random number in the range [min, max] using block attributes + function generatePRN(uint256 min, uint256 max, uint256 salt) public view returns (uint256) { + return + (uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty, msg.sender, salt))) % + (max - min + 1)) + min; + } + + // This function can be also found in OpenZeppelin's library, but in a newer version than the one + function compareStrings(string memory a, string memory b) public pure returns (bool) { + return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b)))); + } + + function containsLog( + Vm.Log[] memory logs, + string memory signatureString + ) private view returns (bool) { + bytes32 signature = keccak256(abi.encodePacked(signatureString)); + for (uint256 i = 0; i < logs.length; i++) { + bytes32 logSignature = logs[i].topics[0]; + if (logSignature == signature) { + return true; + } + } + + return false; + } + + function emitsLog(function() action, string memory signatureString) private returns (bool) { + vm.recordLogs(); + action(); + Vm.Log[] memory entries = vm.getRecordedLogs(); + return containsLog(entries, signatureString); + } + + /** + * @notice Counterpart to the `expectEmit` cheatcode: asserts that a given log was *not* emitted. + * @param action Function that, when called, performs the action to be tested. + * @param signatureString The string representation of the event signature + * that should not be emitted (e.g. "Transfer(address,address,uint256)"). + */ + function assertDoesNotEmit(function() action, string memory signatureString) internal { + assertFalse(emitsLog(action, signatureString)); + } +} diff --git a/packages/protocol/test-sol/common/FeeHandler.t.sol b/packages/protocol/test-sol/common/FeeHandler.t.sol deleted file mode 100644 index fc65e2df59c..00000000000 --- a/packages/protocol/test-sol/common/FeeHandler.t.sol +++ /dev/null @@ -1,951 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; - -import "celo-foundry/Test.sol"; -import "@celo-contracts/common/FeeHandler.sol"; -import "../constants.sol"; - -import { Exchange } from "../../lib/mento-core/contracts/Exchange.sol"; -import { StableToken } from "../../lib/mento-core/contracts/StableToken.sol"; -import "@celo-contracts/common/FixidityLib.sol"; -import "@celo-contracts/common/Freezer.sol"; -import "@celo-contracts/common/GoldToken.sol"; -import "@celo-contracts/common/FeeCurrencyWhitelist.sol"; -import "@celo-contracts/common/MentoFeeHandlerSeller.sol"; -import "@celo-contracts/common/UniswapFeeHandlerSeller.sol"; -import "@celo-contracts/uniswap/test/MockUniswapV2Router02.sol"; -import "@celo-contracts/uniswap/test/MockUniswapV2Factory.sol"; -import "@celo-contracts/uniswap/test/MockERC20.sol"; -import "../../lib/mento-core/test/mocks/MockSortedOracles.sol"; -import "../../lib/mento-core/test/mocks/MockReserve.sol"; - -contract FeeHandlerTest is Test, Constants { - using FixidityLib for FixidityLib.Fraction; - - FeeHandler feeHandler; - IRegistry registry; - GoldToken celoToken; - MockSortedOracles mockSortedOracles; - MockReserve mockReserve; - - Freezer freezer; - MockERC20 tokenA; - - MockUniswapV2Router02 uniswapRouter; - MockUniswapV2Router02 uniswapRouter2; - MockUniswapV2Factory uniswapFactory; - MockUniswapV2Factory uniswapFactory2; - - FeeCurrencyWhitelist feeCurrencyWhitelist; - - MentoFeeHandlerSeller mentoSeller; - UniswapFeeHandlerSeller uniswapFeeHandlerSeller; - - Exchange exchangeUSD; - Exchange exchangeEUR; - StableToken stableToken; - StableToken stableTokenEUR; - - address EXAMPLE_BENEFICIARY_ADDRESS = 0x2A486910DBC72cACcbb8d0e1439C96b03B2A4699; - - address registryAddress = 0x000000000000000000000000000000000000ce10; - address owner = address(this); - address user = actor("user"); - - uint256 celoAmountForRate = 1e24; - uint256 stableAmountForRate = 2 * celoAmountForRate; - uint256 spread; - uint256 reserveFraction; - uint256 maxSlippage; - uint256 initialReserveBalance = 1e22; - - uint8 decimals = 18; - uint256 updateFrequency = 60 * 60; - uint256 minimumReports = 2; - - event SoldAndBurnedToken(address token, uint256 value); - event DailyLimitSet(address tokenAddress, uint256 newLimit); - event DailyLimitHit(address token, uint256 burning); - event MaxSlippageSet(address token, uint256 maxSlippage); - event DailySellLimitUpdated(uint256 amount); - event FeeBeneficiarySet(address newBeneficiary); - event BurnFractionSet(uint256 fraction); - event TokenAdded(address tokenAddress, address handlerAddress); - event TokenRemoved(address tokenAddress); - - function setUp() public { - vm.warp(YEAR); // foundry starts block.timestamp at 0, which leads to underflow errors in Uniswap contracts - - spread = FixidityLib.newFixedFraction(3, 1000).unwrap(); - reserveFraction = FixidityLib.newFixedFraction(5, 100).unwrap(); - maxSlippage = FixidityLib.newFixedFraction(1, 100).unwrap(); - - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); - - celoToken = new GoldToken(true); - mockReserve = new MockReserve(); - stableToken = new StableToken(true); - stableTokenEUR = new StableToken(true); - registry = IRegistry(registryAddress); - feeHandler = new FeeHandler(true); - freezer = new Freezer(true); - feeCurrencyWhitelist = new FeeCurrencyWhitelist(true); - mentoSeller = new MentoFeeHandlerSeller(true); - uniswapFeeHandlerSeller = new UniswapFeeHandlerSeller(true); - - tokenA = new MockERC20(); - - feeCurrencyWhitelist.initialize(); - registry.setAddressFor("FeeCurrencyWhitelist", address(feeCurrencyWhitelist)); - registry.setAddressFor("Freezer", address(freezer)); - registry.setAddressFor("GoldToken", address(celoToken)); - registry.setAddressFor("Reserve", address(mockReserve)); - - mockReserve.setGoldToken(address(celoToken)); - mockReserve.addToken(address(stableToken)); - mockReserve.addToken(address(stableTokenEUR)); - - address[] memory tokenAddresses; - uint256[] memory newMininumReports; - - mentoSeller.initialize(address(registry), tokenAddresses, newMininumReports); - celoToken.initialize(address(registry)); - stableToken.initialize( - "Celo Dollar", - "cUSD", - decimals, - address(registry), - FIXED1, - WEEK, - new address[](0), - new uint256[](0), - "Exchange" - ); - - stableTokenEUR.initialize( - "Celo Euro", - "cEUR", - decimals, - address(registry), - FIXED1, - WEEK, - new address[](0), - new uint256[](0), - "ExchangeEUR" - ); - - mockSortedOracles = new MockSortedOracles(); - registry.setAddressFor("SortedOracles", address(mockSortedOracles)); - - mockSortedOracles.setMedianRate(address(stableToken), stableAmountForRate); - mockSortedOracles.setMedianTimestampToNow(address(stableToken)); - mockSortedOracles.setNumRates(address(stableToken), 2); - - mockSortedOracles.setMedianRate(address(stableTokenEUR), stableAmountForRate); - mockSortedOracles.setMedianTimestampToNow(address(stableTokenEUR)); - mockSortedOracles.setNumRates(address(stableTokenEUR), 2); - - fundReserve(); - - exchangeUSD = new Exchange(true); - exchangeUSD.initialize( - address(registry), - "StableToken", - spread, - reserveFraction, - updateFrequency, - minimumReports - ); - - exchangeEUR = new Exchange(true); - exchangeEUR.initialize( - address(registry), - "StableTokenEUR", - spread, - reserveFraction, - updateFrequency, - minimumReports - ); - - registry.setAddressFor("StableToken", address(stableToken)); - registry.setAddressFor("Exchange", address(exchangeUSD)); - registry.setAddressFor("StableTokenEUR", address(stableTokenEUR)); - registry.setAddressFor("ExchangeEUR", address(exchangeEUR)); - - exchangeUSD.activateStable(); - exchangeEUR.activateStable(); - - feeHandler.initialize( - registryAddress, - EXAMPLE_BENEFICIARY_ADDRESS, - 0, - new address[](0), - new address[](0), - new uint256[](0), - new uint256[](0) - ); - } - - function fundReserve() public { - celoToken.transfer(address(mockReserve), initialReserveBalance); - } -} - -contract FeeHandlerTest_SetBurnFraction is FeeHandlerTest { - function test_Reverts_WhenCallerNotOwner() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - feeHandler.setBurnFraction(100); - } - - function test_Reverts_WhenFractionsGreaterThanOne() public { - vm.expectRevert("Burn fraction must be less than or equal to 1"); - feeHandler.setBurnFraction(FixidityLib.newFixedFraction(3, 2).unwrap()); - } - - function test_SetBurnFraction() public { - feeHandler.setBurnFraction(FixidityLib.newFixedFraction(80, 100).unwrap()); - assertEq( - feeHandler.burnFraction(), - FixidityLib.newFixedFraction(80, 100).unwrap(), - "Burn fraction should be set" - ); - } - - function test_ShouldEmitFeeBeneficiarySet() public { - vm.expectEmit(true, true, true, true); - emit BurnFractionSet(FixidityLib.newFixedFraction(80, 100).unwrap()); - feeHandler.setBurnFraction(FixidityLib.newFixedFraction(80, 100).unwrap()); - } -} - -contract FeeHandlerTest_SetHandler is FeeHandlerTest { - function test_Reverts_WhenCallerNotOwner() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - feeHandler.setHandler(address(stableToken), address(mentoSeller)); - } - - function test_SetsHandler() public { - feeHandler.setHandler(address(stableToken), address(mentoSeller)); - assertEq( - feeHandler.getTokenHandler(address(stableToken)), - address(mentoSeller), - "Handler should be set" - ); - } -} - -contract FeeHandlerTest_AddToken is FeeHandlerTest { - function test_Reverts_WhenCallerNotOwner() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - feeHandler.addToken(address(stableToken), address(mentoSeller)); - } - - function test_AddsToken() public { - feeHandler.addToken(address(stableToken), address(mentoSeller)); - address[] memory expectedActiveTokens = new address[](1); - expectedActiveTokens[0] = address(stableToken); - assertEq(feeHandler.getActiveTokens(), expectedActiveTokens); - assertTrue(feeHandler.getTokenActive(address(stableToken))); - assertEq(feeHandler.getTokenHandler(address(stableToken)), address(mentoSeller)); - } - - function test_ShouldEmitTokenAdded() public { - vm.expectEmit(true, true, true, true); - emit TokenAdded(address(stableToken), address(mentoSeller)); - feeHandler.addToken(address(stableToken), address(mentoSeller)); - } -} - -contract FeeHandlerTest_RemoveToken is FeeHandlerTest { - function test_Reverts_WhenCallerNotOwner() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - feeHandler.removeToken(address(stableToken)); - } - - function test_RemovesToken() public { - feeHandler.addToken(address(stableToken), address(mentoSeller)); - feeHandler.removeToken(address(stableToken)); - assertFalse(feeHandler.getTokenActive(address(stableToken))); - assertEq(feeHandler.getActiveTokens().length, 0); - assertEq(feeHandler.getTokenHandler(address(stableToken)), address(0)); - } - - function test_Emits_TokenRemoved() public { - feeHandler.addToken(address(stableToken), address(mentoSeller)); - vm.expectEmit(true, true, true, true); - emit TokenRemoved(address(stableToken)); - feeHandler.removeToken(address(stableToken)); - } -} - -contract FeeHandlerTest_DeactivateAndActivateToken is FeeHandlerTest { - function test_Reverts_WhenActivateCallerNotOwner() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - feeHandler.deactivateToken(address(stableToken)); - } - - function test_Reverts_WhenDeactivateCallerNotOwner() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - feeHandler.activateToken(address(stableToken)); - } - - function test_DeactivateAndActivateToken() public { - feeHandler.addToken(address(stableToken), address(mentoSeller)); - feeHandler.deactivateToken(address(stableToken)); - assertFalse(feeHandler.getTokenActive(address(stableToken))); - assertEq(feeHandler.getActiveTokens().length, 0); - - feeHandler.activateToken(address(stableToken)); - assertTrue(feeHandler.getTokenActive(address(stableToken))); - address[] memory expectedActiveTokens = new address[](1); - expectedActiveTokens[0] = address(stableToken); - assertEq(feeHandler.getActiveTokens(), expectedActiveTokens); - } -} - -contract FeeHandlerTest_SetFeeBeneficiary is FeeHandlerTest { - function test_Reverts_WhenCallerNotOwner() public { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - feeHandler.setFeeBeneficiary(EXAMPLE_BENEFICIARY_ADDRESS); - } - - function test_ShouldEmitFeeBeneficiarySet() public { - vm.expectEmit(true, true, true, true); - emit FeeBeneficiarySet(EXAMPLE_BENEFICIARY_ADDRESS); - feeHandler.setFeeBeneficiary(EXAMPLE_BENEFICIARY_ADDRESS); - } - - function test_SetsAddressCorrectly() public { - feeHandler.setFeeBeneficiary(EXAMPLE_BENEFICIARY_ADDRESS); - assertEq(feeHandler.feeBeneficiary(), EXAMPLE_BENEFICIARY_ADDRESS); - } -} - -contract FeeHandlerTest_Distribute is FeeHandlerTest { - modifier setUpBeneficiary() { - feeHandler.setFeeBeneficiary(EXAMPLE_BENEFICIARY_ADDRESS); - _; - } - - modifier activateToken() { - feeHandler.addToken(address(stableToken), address(mentoSeller)); - feeHandler.activateToken(address(stableToken)); - _; - } - - modifier fundFeeHandlerStable(uint256 stableAmount) { - uint256 celoAmount = 1e18; - celoToken.approve(address(exchangeUSD), celoAmount); - exchangeUSD.sell(celoAmount, 0, true); - stableToken.transfer(address(feeHandler), stableAmount); - _; - } - - modifier setBurnFraction() { - feeHandler.setBurnFraction(FixidityLib.newFixedFraction(80, 100).unwrap()); - _; - } - - modifier setMaxSlippage() { - feeHandler.setMaxSplippage(address(stableToken), FIXED1); - _; - } - - function test_Reverts_WhenNotActive() public setUpBeneficiary { - vm.expectRevert("Handler has to be set to sell token"); - feeHandler.distribute(address(stableToken)); - } - - function test_Reverts_WhenFrozen() public setUpBeneficiary activateToken { - freezer.freeze(address(feeHandler)); - vm.expectRevert("can't call when contract is frozen"); - feeHandler.distribute(address(stableToken)); - } - - function test_DoesntDistributeWhenToDistributeIsZero() - public - setBurnFraction - setMaxSlippage - setUpBeneficiary - activateToken - fundFeeHandlerStable(1e18) - { - // If we uncomment this the test should fail - // feeHandler.sell(address(stableToken)); - vm.recordLogs(); - feeHandler.distribute(address(stableToken)); - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 0); - } - - function test_DoesntDistributeWhenBalanceIsZero() - public - setBurnFraction - setMaxSlippage - setUpBeneficiary - activateToken - { - vm.recordLogs(); - feeHandler.distribute(address(stableToken)); - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 0); - } - - function test_Distribute() - public - setBurnFraction - setMaxSlippage - setUpBeneficiary - activateToken - fundFeeHandlerStable(1e18) - { - feeHandler.sell(address(stableToken)); - - feeHandler.distribute(address(stableToken)); - - assertEq(stableToken.balanceOf(address(feeHandler)), 0); - assertEq(stableToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); - } -} - -contract FeeHandlerTest_BurnCelo is FeeHandlerTest { - modifier activateToken() { - feeHandler.activateToken(address(celoToken)); // celoToken doesn't need to be added before activating - _; - } - - modifier setBurnFraction() { - feeHandler.setBurnFraction(FixidityLib.newFixedFraction(80, 100).unwrap()); - _; - } - - modifier fundFeeHandler() { - uint256 celoAmount = 1e18; - celoToken.transfer(address(feeHandler), celoAmount); - _; - } - - function test_BurnsCorrectly() public activateToken setBurnFraction fundFeeHandler { - feeHandler.burnCelo(); - assertEq(celoToken.balanceOf(address(feeHandler)), 2e17); - assertEq(celoToken.getBurnedAmount(), 8e17); - } - - function test_DoesntBurnPendingDistribution() - public - activateToken - setBurnFraction - fundFeeHandler - { - feeHandler.burnCelo(); - assertEq(celoToken.getBurnedAmount(), 8e17); - // this is the amount pending distribution - assertEq(celoToken.balanceOf(address(feeHandler)), 2e17); - - feeHandler.burnCelo(); - assertEq(celoToken.getBurnedAmount(), 8e17); - // amount pending distribution should not be changed by second burn - assertEq(celoToken.balanceOf(address(feeHandler)), 2e17); - } - - function test_DistributesCorrectlyAfterBurn() - public - activateToken - setBurnFraction - fundFeeHandler - { - feeHandler.burnCelo(); - assertEq(celoToken.balanceOf(address(feeHandler)), 2e17); - - feeHandler.setFeeBeneficiary(EXAMPLE_BENEFICIARY_ADDRESS); - feeHandler.distribute(address(celoToken)); - assertEq(celoToken.balanceOf(address(feeHandler)), 0); - assertEq(celoToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); - } -} - -contract FeeHandlerTest_SellMentoTokens is FeeHandlerTest { - modifier addStableToken() { - feeHandler.addToken(address(stableToken), address(mentoSeller)); - _; - } - - modifier fundFeeHandlerStable(uint256 stableAmount) { - uint256 celoAmount = 1e18; - celoToken.approve(address(exchangeUSD), celoAmount); - exchangeUSD.sell(celoAmount, 0, true); - stableToken.transfer(address(feeHandler), stableAmount); - _; - } - - modifier setBurnFraction() { - feeHandler.setBurnFraction(FixidityLib.newFixedFraction(80, 100).unwrap()); - _; - } - - modifier setUpBeneficiary() { - feeHandler.setFeeBeneficiary(EXAMPLE_BENEFICIARY_ADDRESS); - _; - } - - modifier setMaxSlippage() { - feeHandler.setMaxSplippage(address(stableToken), FIXED1); - _; - } - - function test_Reverts_WhenFrozen() public { - freezer.freeze(address(feeHandler)); - vm.expectRevert("can't call when contract is frozen"); - feeHandler.sell(address(stableToken)); - } - - function test_WontSellWhenBalanceLow() - public - setBurnFraction - setMaxSlippage - addStableToken - fundFeeHandlerStable(feeHandler.MIN_BURN()) - { - uint256 balanceBefore = stableToken.balanceOf(address(feeHandler)); - feeHandler.sell(address(stableToken)); - assertEq(stableToken.balanceOf(address(feeHandler)), balanceBefore); - } - - function test_ResetSellLimitDaily() - public - setBurnFraction - setMaxSlippage - addStableToken - fundFeeHandlerStable(3000) - { - feeHandler.setDailySellLimit(address(stableToken), 1000); - feeHandler.sell(address(stableToken)); - assertEq(stableToken.balanceOf(address(feeHandler)), 2000); - skip(DAY); - feeHandler.sell(address(stableToken)); - assertEq(stableToken.balanceOf(address(feeHandler)), 1000); - } - - function test_DoesntSellWhenBiggerThanLimit() - public - setBurnFraction - setMaxSlippage - addStableToken - fundFeeHandlerStable(3000) - { - feeHandler.setDailySellLimit(address(stableToken), 1000); - feeHandler.sell(address(stableToken)); - assertEq(stableToken.balanceOf(address(feeHandler)), 2000); - // selling again shouldn't do anything - feeHandler.sell(address(stableToken)); - assertEq(stableToken.balanceOf(address(feeHandler)), 2000); - } - - function test_Reverts_WhenHandlerNotSet() - public - setBurnFraction - setMaxSlippage - fundFeeHandlerStable(3000) - { - vm.expectRevert("Handler has to be set to sell token"); - feeHandler.sell(address(stableToken)); - } - - function test_SellsWithMento() - public - setBurnFraction - setMaxSlippage - addStableToken - fundFeeHandlerStable(1e18) - { - assertEq(feeHandler.getPastBurnForToken(address(stableToken)), 0); - uint256 expectedCeloAmount = exchangeUSD.getBuyTokenAmount(8e17, false); - feeHandler.sell(address(stableToken)); - assertEq(feeHandler.getPastBurnForToken(address(stableToken)), 8e17); - assertEq(stableToken.balanceOf(address(feeHandler)), 2e17); - assertEq(feeHandler.getTokenToDistribute(address(stableToken)), 2e17); - assertEq(feeHandler.celoToBeBurned(), expectedCeloAmount); - } - - function test_Reverts_WhenNotEnoughReports() - public - setBurnFraction - setMaxSlippage - addStableToken - fundFeeHandlerStable(1e18) - { - mentoSeller.setMinimumReports(address(stableToken), 3); - vm.expectRevert("Number of reports for token not enough"); - feeHandler.sell(address(stableToken)); - } - - function test_DoesntSellBalanceToDistribute() - public - setBurnFraction - setMaxSlippage - addStableToken - fundFeeHandlerStable(1e18) - { - feeHandler.sell(address(stableToken)); - uint256 balanceBefore = stableToken.balanceOf(address(feeHandler)); - feeHandler.sell(address(stableToken)); - assertEq(stableToken.balanceOf(address(feeHandler)), balanceBefore); - } -} - -contract FeeHandlerTest_SellNonMentoTokens is FeeHandlerTest { - uint256 deadline; - - modifier setMaxSlippage() { - feeHandler.setMaxSplippage(address(stableToken), FIXED1); - feeHandler.setMaxSplippage(address(tokenA), FixidityLib.newFixedFraction(99, 100).unwrap()); - _; - } - - modifier setUpUniswap() { - uniswapFactory = new MockUniswapV2Factory(address(0)); - bytes32 initCodePairHash = uniswapFactory.INIT_CODE_PAIR_HASH(); - uniswapRouter = new MockUniswapV2Router02( - address(uniswapFactory), - address(0), - initCodePairHash - ); - - uniswapFactory2 = new MockUniswapV2Factory(address(0)); - uniswapRouter2 = new MockUniswapV2Router02( - address(uniswapFactory2), - address(0), - initCodePairHash - ); - uniswapFeeHandlerSeller.initialize(address(registry), new address[](0), new uint256[](0)); - uniswapFeeHandlerSeller.setRouter(address(tokenA), address(uniswapRouter)); - _; - } - - modifier setUpLiquidity(uint256 toMint, uint256 toTransfer) { - deadline = block.timestamp + 100; - tokenA.mint(address(feeHandler), toMint); - tokenA.mint(user, toMint); - celoToken.transfer(user, toMint); - - vm.startPrank(user); - tokenA.approve(address(uniswapRouter), toTransfer); - celoToken.approve(address(uniswapRouter), toTransfer); - uniswapRouter.addLiquidity( - address(tokenA), - address(celoToken), - toTransfer, - toTransfer, - toTransfer, - toTransfer, - user, - deadline - ); - vm.stopPrank(); - _; - } - - modifier setUpOracles() { - uniswapFeeHandlerSeller.setMinimumReports(address(tokenA), 1); - mockSortedOracles.setMedianRate(address(tokenA), celoAmountForRate); - mockSortedOracles.setNumRates(address(tokenA), 2); - _; - } - - modifier addToken() { - feeHandler.addToken(address(tokenA), address(uniswapFeeHandlerSeller)); - _; - } - - modifier setBurnFraction() { - feeHandler.setBurnFraction(FixidityLib.newFixedFraction(80, 100).unwrap()); - _; - } - - function test_Reverts_WhenNotEnoughReports() - public - setMaxSlippage - setUpUniswap - setUpLiquidity(1e19, 5e18) - setUpOracles - addToken - setBurnFraction - { - mockSortedOracles.setNumRates(address(tokenA), 0); - vm.expectRevert("Number of reports for token not enough"); - feeHandler.sell(address(tokenA)); - assertEq(tokenA.balanceOf(address(feeHandler)), 1e19); - } - - function test_SellWorksWithReports() - public - setMaxSlippage - setUpUniswap - setUpLiquidity(1e19, 5e18) - setUpOracles - addToken - setBurnFraction - { - feeHandler.sell(address(tokenA)); - assertEq(tokenA.balanceOf(address(feeHandler)), 2e18); - } - - function test_Reverts_WhenOracleSlippageIsHigh() - public - setUpUniswap - setUpLiquidity(1e19, 5e18) - setUpOracles - addToken - setBurnFraction - { - feeHandler.setMaxSplippage(address(tokenA), FixidityLib.newFixedFraction(80, 100).unwrap()); - mockSortedOracles.setMedianRate(address(tokenA), 300 * celoAmountForRate); - - vm.expectRevert("UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"); - feeHandler.sell(address(tokenA)); - } - - function test_UniswapTrade() - public - setMaxSlippage - setUpUniswap - setUpLiquidity(1e19, 5e18) - setUpOracles - addToken - setBurnFraction - { - // Make sure our uniswap mock works - uint256 balanceBeforeA = tokenA.balanceOf(user); - uint256 balanceBeforeCelo = celoToken.balanceOf(user); - - vm.startPrank(user); - tokenA.approve(address(uniswapRouter), 1e18); - address[] memory tokenAddresses = new address[](2); - tokenAddresses[0] = address(tokenA); - tokenAddresses[1] = address(celoToken); - uniswapRouter.swapExactTokensForTokens(1e18, 0, tokenAddresses, user, deadline); - vm.stopPrank(); - - // simple directional check - assertGt(balanceBeforeA, tokenA.balanceOf(user)); - assertGt(celoToken.balanceOf(user), balanceBeforeCelo); - } - - function test_SellsNonMentoTokens() - public - setMaxSlippage - setUpUniswap - setUpLiquidity(1e19, 5e18) - setUpOracles - addToken - setBurnFraction - { - assertEq(tokenA.balanceOf(address(feeHandler)), 1e19); - feeHandler.sell(address(tokenA)); - assertEq(tokenA.balanceOf(address(feeHandler)), 2e18); - } - - function test_Reverts_WhenSlippageIsTooHigh() - public - setUpUniswap - setUpLiquidity(1e19, 5e18) - setUpOracles - addToken - setBurnFraction - { - feeHandler.setMaxSplippage(address(tokenA), maxSlippage); - vm.expectRevert("UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"); - feeHandler.sell(address(tokenA)); - assertEq(tokenA.balanceOf(address(feeHandler)), 1e19); - } - - function test_TriesToGetBestRateWithManyExchanges() - public - setMaxSlippage - setUpUniswap - setUpLiquidity(2e19, 5e18) - setUpOracles - addToken - setBurnFraction - { - // Setup second uniswap exchange - uniswapFeeHandlerSeller.setRouter(address(tokenA), address(uniswapRouter2)); - uint256 toTransfer2 = 1e19; // this is higher than toTransfer1 (5e18) set in modifier - vm.startPrank(user); - tokenA.approve(address(uniswapRouter2), toTransfer2); - celoToken.approve(address(uniswapRouter2), toTransfer2); - uniswapRouter2.addLiquidity( - address(tokenA), - address(celoToken), - toTransfer2, - toTransfer2, - toTransfer2, - toTransfer2, - user, - deadline - ); - vm.stopPrank(); - - address[] memory tokenAddresses = new address[](2); - tokenAddresses[0] = address(tokenA); - tokenAddresses[1] = address(celoToken); - - uint256 quote1before = uniswapRouter.getAmountsOut(1e18, tokenAddresses)[1]; - uint256 quote2before = uniswapRouter2.getAmountsOut(1e18, tokenAddresses)[1]; - - // safety check - assertEq(tokenA.balanceOf(address(feeHandler)), 2e19); - - feeHandler.sell(address(tokenA)); - - // Exchange should have occurred on uniswap2 because it has more liquidity. - // After the exchange, uniswap2 has less Celo liquidity than it did before, - // so the quote for tokenA (denominated in Celo) is lower. - uint256 quote1after = uniswapRouter.getAmountsOut(1e18, tokenAddresses)[1]; - uint256 quote2after = uniswapRouter.getAmountsOut(1e18, tokenAddresses)[1]; - assertEq(quote1before, quote1after); // uniswap1 quote should be untouched, since liquidity hasn't changed - assertGt(quote2before, quote2after); // uniswap2 quoute should be lower, since it now has more tokenA per Celo - assertEq(tokenA.balanceOf(address(feeHandler)), 4e18); // check that it burned - } -} - -contract FeeHandlerTest_HandleMentoTokens is FeeHandlerTest { - modifier setBurnFraction() { - feeHandler.setBurnFraction(FixidityLib.newFixedFraction(80, 100).unwrap()); - _; - } - - modifier setUpBeneficiary() { - feeHandler.setFeeBeneficiary(EXAMPLE_BENEFICIARY_ADDRESS); - _; - } - - modifier fundFeeHandler(uint256 amount) { - celoToken.transfer(address(feeHandler), amount); - _; - } - - modifier activateToken() { - feeHandler.activateToken(address(celoToken)); - _; - } - - function test_Reverts_WhenTokenNotAdded() - public - setBurnFraction - setUpBeneficiary - fundFeeHandler(1e18) - { - vm.expectRevert("Handler has to be set to sell token"); - feeHandler.handle(address(stableToken)); - } - - function test_HandleCelo() public setBurnFraction setUpBeneficiary fundFeeHandler(1e18) { - feeHandler.handle(address(celoToken)); - assertEq(celoToken.getBurnedAmount(), 8e17); - assertEq(celoToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); - } -} - -contract FeeHandlerTest_HandleAll is FeeHandlerTest { - modifier setBurnFraction() { - feeHandler.setBurnFraction(FixidityLib.newFixedFraction(80, 100).unwrap()); - _; - } - - modifier setUpBeneficiary() { - feeHandler.setFeeBeneficiary(EXAMPLE_BENEFICIARY_ADDRESS); - _; - } - - modifier addTokens() { - feeHandler.addToken(address(stableToken), address(mentoSeller)); - feeHandler.addToken(address(stableTokenEUR), address(mentoSeller)); - _; - } - - modifier setMaxSlippage() { - feeHandler.setMaxSplippage(address(stableToken), FIXED1); - feeHandler.setMaxSplippage(address(stableTokenEUR), FIXED1); - _; - } - - modifier fundFeeHandlerStable(uint256 celoAmount, uint256 stableAmount) { - celoToken.approve(address(exchangeUSD), celoAmount); - celoToken.approve(address(exchangeEUR), celoAmount); - exchangeUSD.sell(celoAmount, 0, true); - exchangeEUR.sell(celoAmount, 0, true); - stableToken.transfer(address(feeHandler), stableAmount); - stableTokenEUR.transfer(address(feeHandler), stableAmount); - _; - } - - function test_BurnsWithMento() - public - setUpBeneficiary - setBurnFraction - setMaxSlippage - fundFeeHandlerStable(1e18, 1e18) - addTokens - { - uint256 previousCeloBurn = celoToken.getBurnedAmount(); - assertEq(feeHandler.getPastBurnForToken(address(stableToken)), 0); - assertEq(feeHandler.getPastBurnForToken(address(stableTokenEUR)), 0); - - feeHandler.handleAll(); - - assertEq(feeHandler.getPastBurnForToken(address(stableToken)), 8e17); - assertEq(feeHandler.getPastBurnForToken(address(stableTokenEUR)), 8e17); - assertEq(stableToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); - assertEq(stableTokenEUR.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); - - // everything should have been burned or distributed - assertEq(feeHandler.getTokenToDistribute(address(stableToken)), 0); - assertEq(feeHandler.getTokenToDistribute(address(stableTokenEUR)), 0); - - // celo burn is non zero - assertTrue(celoToken.getBurnedAmount() > previousCeloBurn); - } -} - -contract FeeHandlerTest_Transfer is FeeHandlerTest { - modifier mintToken(uint256 amount) { - tokenA.mint(address(feeHandler), amount); - _; - } - - function test_Reverts_WhenCallerNotOwner() public mintToken(1e18) { - vm.prank(user); - vm.expectRevert("Ownable: caller is not the owner"); - feeHandler.transfer(address(tokenA), user, 1e18); - } - - function test_CanTakeFundsOut() public mintToken(1e18) { - feeHandler.transfer(address(tokenA), user, 1e18); - assertEq(tokenA.balanceOf(user), 1e18); - } -} - -contract FeeHandlerTest_SetDailySellLimit is FeeHandlerTest { - function test_Reverts_WhenCallerNotOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(user); - feeHandler.setDailySellLimit(address(stableToken), celoAmountForRate); - } -} - -contract FeeHandlerTest_SetMaxSlippage is FeeHandlerTest { - function test_Reverts_WhenCallerNotOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(user); - feeHandler.setMaxSplippage(address(stableToken), maxSlippage); - } -} diff --git a/packages/protocol/test-sol/common/GoldToken.t.sol b/packages/protocol/test-sol/common/GoldToken.t.sol deleted file mode 100644 index f5aac2c5f58..00000000000 --- a/packages/protocol/test-sol/common/GoldToken.t.sol +++ /dev/null @@ -1,358 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.5.13; - -import "celo-foundry/Test.sol"; -import "@celo-contracts/common/GoldToken.sol"; -import "./GoldTokenMock.sol"; - -contract GoldTokenTest is Test, IsL2Check { - GoldToken goldToken; - - uint256 constant ONE_GOLDTOKEN = 1000000000000000000; - address receiver; - address sender; - address goldTokenOwner; - address goldTokenMintingSchedule; - - event Transfer(address indexed from, address indexed to, uint256 value); - event TransferComment(string comment); - event SetGoldTokenMintingScheduleAddress(address indexed newScheduleAddress); - - modifier _whenL2() { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); - _; - } - - function setUp() public { - goldTokenOwner = actor("goldTokenOwner"); - goldTokenMintingSchedule = actor("goldTokenMintingSchedule"); - vm.prank(goldTokenOwner); - goldToken = new GoldToken(true); - deployCodeTo("MintGoldSchedule.sol", abi.encode(false), goldTokenMintingSchedule); - receiver = actor("receiver"); - sender = actor("sender"); - vm.deal(receiver, ONE_GOLDTOKEN); - vm.deal(sender, ONE_GOLDTOKEN); - } -} - -contract GoldTokenTest_general is GoldTokenTest { - function setUp() public { - super.setUp(); - } - - function test_name() public { - assertEq(goldToken.name(), "Celo native asset"); - } - - function test_symbol() public { - assertEq(goldToken.symbol(), "CELO"); - } - - function test_decimals() public { - assertEq(uint256(goldToken.decimals()), 18); - } - - function test_balanceOf() public { - assertEq(goldToken.balanceOf(receiver), receiver.balance); - } - - function test_approve() public { - vm.prank(sender); - goldToken.approve(receiver, ONE_GOLDTOKEN); - assertEq(goldToken.allowance(sender, receiver), ONE_GOLDTOKEN); - } - - function test_increaseAllowance() public { - vm.prank(sender); - goldToken.increaseAllowance(receiver, ONE_GOLDTOKEN); - vm.prank(sender); - goldToken.increaseAllowance(receiver, ONE_GOLDTOKEN); - assertEq(goldToken.allowance(sender, receiver), ONE_GOLDTOKEN * 2); - } - - function test_decreaseAllowance() public { - vm.prank(sender); - goldToken.approve(receiver, ONE_GOLDTOKEN * 2); - vm.prank(sender); - goldToken.decreaseAllowance(receiver, ONE_GOLDTOKEN); - assertEq(goldToken.allowance(sender, receiver), ONE_GOLDTOKEN); - } - - function test_allowance() public { - vm.prank(sender); - goldToken.approve(receiver, ONE_GOLDTOKEN); - assertEq(goldToken.allowance(sender, receiver), ONE_GOLDTOKEN); - } -} - -contract GoldTokenTest_transfer is GoldTokenTest { - function setUp() public { - super.setUp(); - } - - function test_ShouldTransferBalanceFromOneUserToAnother() public { - uint256 startBalanceFrom = goldToken.balanceOf(sender); - uint256 startBalanceTo = goldToken.balanceOf(receiver); - vm.prank(sender); - goldToken.transfer(receiver, ONE_GOLDTOKEN); - assertEq(sender.balance, startBalanceFrom - ONE_GOLDTOKEN); - assertEq(receiver.balance, startBalanceTo + ONE_GOLDTOKEN); - } - - function test_ShouldTransferBalanceWithAComment() public { - string memory comment = "tacos at lunch"; - uint256 startBalanceFrom = goldToken.balanceOf(sender); - uint256 startBalanceTo = goldToken.balanceOf(receiver); - vm.prank(sender); - vm.expectEmit(true, true, true, true); - emit Transfer(sender, receiver, ONE_GOLDTOKEN); - vm.expectEmit(true, true, true, true); - emit TransferComment(comment); - goldToken.transferWithComment(receiver, ONE_GOLDTOKEN, comment); - assertEq(sender.balance, startBalanceFrom - ONE_GOLDTOKEN); - assertEq(receiver.balance, startBalanceTo + ONE_GOLDTOKEN); - } - - function test_ShouldNotAllowToTransferToNullAddress() public { - vm.prank(sender); - vm.expectRevert(); - goldToken.transfer(address(0), ONE_GOLDTOKEN); - } -} - -contract GoldTokenTest_transferFrom is GoldTokenTest { - function setUp() public { - super.setUp(); - vm.prank(sender); - goldToken.approve(receiver, ONE_GOLDTOKEN); - } - - function test_ShouldTransferBalanceFromOneUserToAnother() public { - uint256 startBalanceFrom = goldToken.balanceOf(sender); - uint256 startBalanceTo = goldToken.balanceOf(receiver); - vm.prank(receiver); - goldToken.transferFrom(sender, receiver, ONE_GOLDTOKEN); - assertEq(sender.balance, startBalanceFrom - ONE_GOLDTOKEN); - assertEq(receiver.balance, startBalanceTo + ONE_GOLDTOKEN); - } - - function test_ShouldNotAllowToTransferToNullAddress() public { - vm.prank(receiver); - vm.expectRevert(); - goldToken.transferFrom(sender, address(0), ONE_GOLDTOKEN); - } - - function test_ShouldNotAllowTransferMoreThanSenderHas() public { - uint256 value = sender.balance + ONE_GOLDTOKEN * 4; - - vm.prank(receiver); - vm.expectRevert(); - goldToken.transferFrom(sender, receiver, value); - } - - function test_ShouldNotAllowTransferringMoreThanTheSpenderIsAllowed() public { - vm.prank(receiver); - vm.expectRevert(); - goldToken.transferFrom(sender, receiver, ONE_GOLDTOKEN + 1); - } -} - -contract GoldTokenTest_burn is GoldTokenTest { - uint256 startBurn; - address burnAddress = address(0x000000000000000000000000000000000000dEaD); - - function setUp() public { - super.setUp(); - startBurn = goldToken.getBurnedAmount(); - } - - function test_burn_address_starts_with_zero_balance() public { - assertEq(goldToken.balanceOf(burnAddress), 0); - } - - function test_burn_starts_as_start_burn_amount() public { - assertEq(goldToken.getBurnedAmount(), startBurn); - } - - function test_burn_amount_eq_the_balance_of_the_burn_address() public { - assertEq(goldToken.getBurnedAmount(), goldToken.balanceOf(burnAddress)); - } - - function test_returns_right_burn_amount() public { - goldToken.burn(ONE_GOLDTOKEN); - assertEq(goldToken.getBurnedAmount(), ONE_GOLDTOKEN + startBurn); - } -} - -contract GoldTokenTest_mint is GoldTokenTest { - function test_Reverts_whenCalledByOtherThanVm() public { - vm.prank(goldTokenOwner); - vm.expectRevert("Only VM can call."); - goldToken.mint(receiver, ONE_GOLDTOKEN); - - vm.prank(goldTokenMintingSchedule); - vm.expectRevert("Only VM can call."); - goldToken.mint(receiver, ONE_GOLDTOKEN); - } - - function test_Should_increaseGoldTokenTotalSupplyWhencalledByVm() public { - uint256 goldTokenSupplyBefore = goldToken.totalSupply(); - vm.prank(address(0)); - goldToken.mint(receiver, ONE_GOLDTOKEN); - uint256 goldTokenSupplyAfter = goldToken.totalSupply(); - assertGt(goldTokenSupplyAfter, goldTokenSupplyBefore); - } - - function test_Emits_TransferEvent() public { - vm.prank(address(0)); - vm.expectEmit(true, true, true, true); - emit Transfer(address(0), receiver, ONE_GOLDTOKEN); - goldToken.mint(receiver, ONE_GOLDTOKEN); - } -} - -contract GoldTokenTest_mint_l2 is GoldTokenTest { - function setUp() public _whenL2 { - super.setUp(); - vm.prank(goldTokenOwner); - goldToken.setGoldTokenMintingScheduleAddress(goldTokenMintingSchedule); - } - - function test_Reverts_whenCalledByOtherThanMintingSchedule() public { - vm.prank(address(0)); - vm.expectRevert("Only MintGoldSchedule can call."); - goldToken.mint(receiver, ONE_GOLDTOKEN); - - vm.prank(address(9)); - vm.expectRevert("Only MintGoldSchedule can call."); - goldToken.mint(receiver, ONE_GOLDTOKEN); - - vm.prank(goldTokenOwner); - vm.expectRevert("Only MintGoldSchedule can call."); - goldToken.mint(receiver, ONE_GOLDTOKEN); - } - - function test_Should_increaseGoldTokenTotalSupply() public { - uint256 goldTokenSupplyBefore = goldToken.totalSupply(); - vm.prank(goldTokenMintingSchedule); - goldToken.mint(receiver, ONE_GOLDTOKEN); - uint256 goldTokenSupplyAfter = goldToken.totalSupply(); - assertGt(goldTokenSupplyAfter, goldTokenSupplyBefore); - } - - function test_Should_increaseGoldTokenBalanceWhenMintedByGoldTokenMintingSchedule() public { - uint256 originalBalance = goldToken.balanceOf(receiver); - vm.prank(goldTokenMintingSchedule); - goldToken.mint(receiver, ONE_GOLDTOKEN); - uint256 balanceAfterMint = goldToken.balanceOf(receiver); - assertGt(balanceAfterMint, originalBalance); - } - - function test_Emits_TransferEvent() public { - vm.prank(goldTokenMintingSchedule); - vm.expectEmit(true, true, true, true); - emit Transfer(address(0), receiver, ONE_GOLDTOKEN); - goldToken.mint(receiver, ONE_GOLDTOKEN); - } -} - -contract GoldTokenTest_setGoldTokenMintingScheduleAddress is GoldTokenTest { - function test_Reverts_whenCalledByOtherThanL2Governance() public _whenL2 { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(address(0)); - goldToken.setGoldTokenMintingScheduleAddress(goldTokenMintingSchedule); - } - - function test_ShouldSucceedWhenCalledByOwner() public { - vm.prank(goldTokenOwner); - goldToken.setGoldTokenMintingScheduleAddress(goldTokenMintingSchedule); - - assertEq(address(goldToken.goldTokenMintingSchedule()), goldTokenMintingSchedule); - } - function test_ShouldSucceedWhenCalledByL2Governance() public _whenL2 { - vm.prank(goldTokenOwner); - goldToken.setGoldTokenMintingScheduleAddress(goldTokenMintingSchedule); - - assertEq(address(goldToken.goldTokenMintingSchedule()), goldTokenMintingSchedule); - } - function test_Emits_SetGoldTokenMintingScheduleAddressEvent() public { - vm.expectEmit(true, true, true, true); - emit SetGoldTokenMintingScheduleAddress(goldTokenMintingSchedule); - vm.prank(goldTokenOwner); - goldToken.setGoldTokenMintingScheduleAddress(goldTokenMintingSchedule); - } -} - -contract GoldTokenTest_increaseSupply is GoldTokenTest { - function test_ShouldIncreaseTotalSupply() public { - uint256 goldTokenSupplyBefore = goldToken.totalSupply(); - vm.prank(address(0)); - goldToken.increaseSupply(ONE_GOLDTOKEN); - uint256 goldTokenSupplyAfter = goldToken.totalSupply(); - assertGt(goldTokenSupplyAfter, goldTokenSupplyBefore); - } - - function test_Reverts_WhenCalledByOtherThanVm() public { - vm.prank(goldTokenOwner); - vm.expectRevert("Only VM can call"); - goldToken.increaseSupply(ONE_GOLDTOKEN); - vm.prank(goldTokenMintingSchedule); - vm.expectRevert("Only VM can call"); - goldToken.increaseSupply(ONE_GOLDTOKEN); - } -} - -contract GoldTokenTest_increaseSupply_l2 is GoldTokenTest { - function setUp() public _whenL2 { - super.setUp(); - vm.prank(goldTokenOwner); - goldToken.setGoldTokenMintingScheduleAddress(goldTokenMintingSchedule); - } - - function test_Reverts_WhenCalledByAnyone() public { - vm.prank(address(0)); - vm.expectRevert("This method is no longer supported in L2."); - goldToken.increaseSupply(ONE_GOLDTOKEN); - vm.prank(goldTokenMintingSchedule); - vm.expectRevert("This method is no longer supported in L2."); - goldToken.increaseSupply(ONE_GOLDTOKEN); - } - - function test_ShouldNotIncreaseTotalSupply() public { - uint256 goldTokenSupplyBefore = goldToken.totalSupply(); - vm.expectRevert("This method is no longer supported in L2."); - vm.prank(goldTokenOwner); - goldToken.increaseSupply(ONE_GOLDTOKEN); - - uint256 goldTokenSupplyAfter = goldToken.totalSupply(); - assertEq(goldTokenSupplyAfter, goldTokenSupplyBefore); - } -} - -contract GoldTokenMockTest is Test { - GoldTokenMock mockGoldToken; - uint256 ONE_GOLDTOKEN = 1000000000000000000; - address burnAddress = address(0x000000000000000000000000000000000000dEaD); - - function setUp() public { - mockGoldToken = new GoldTokenMock(); - mockGoldToken.setTotalSupply(ONE_GOLDTOKEN * 1000); - } -} - -contract GoldTokenMock_circulatingSupply is GoldTokenMockTest { - function setUp() public { - super.setUp(); - } - - function test_ShouldMatchCirculationSupply_WhenNoBurn() public { - assertEq(mockGoldToken.circulatingSupply(), mockGoldToken.totalSupply()); - } - - function test_ShouldDecreaseCirculatingSupply_WhenThereWasBurn() public { - mockGoldToken.setBalanceOf(burnAddress, ONE_GOLDTOKEN); - assertEq(mockGoldToken.circulatingSupply(), ONE_GOLDTOKEN * 999); - assertEq(mockGoldToken.circulatingSupply(), mockGoldToken.totalSupply() - ONE_GOLDTOKEN); - } -} diff --git a/packages/protocol/test-sol/common/Import05.t.sol b/packages/protocol/test-sol/common/Import05.t.sol deleted file mode 100644 index 30d1f1760cc..00000000000 --- a/packages/protocol/test-sol/common/Import05.t.sol +++ /dev/null @@ -1,8 +0,0 @@ -pragma solidity ^0.5.13; - -// this file only exists so that foundry compiles this contracts -import "../../lib/mento-core/contracts/StableToken.sol"; -import "../../lib/mento-core/contracts/StableTokenBRL.sol"; -import "../../lib/mento-core/contracts/StableTokenEUR.sol"; - -contract Import05 {} diff --git a/packages/protocol/test-sol/common/MintGoldSchedule.t.sol b/packages/protocol/test-sol/common/MintGoldSchedule.t.sol deleted file mode 100644 index 40e3f984578..00000000000 --- a/packages/protocol/test-sol/common/MintGoldSchedule.t.sol +++ /dev/null @@ -1,613 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.7 <0.8.20; -pragma experimental ABIEncoderV2; - -import "celo-foundry-8/Test.sol"; -import "@celo-contracts/common/FixidityLib.sol"; -import "@celo-contracts/common/interfaces/IRegistry.sol"; -import "@celo-contracts-8/common/interfaces/IGoldToken.sol"; -import "@celo-contracts/governance/interfaces/IGovernance.sol"; -import "@celo-contracts-8/common/MintGoldSchedule.sol"; -import "@celo-contracts-8/common/IsL2Check.sol"; -import { Constants } from "@test-sol/constants.sol"; - -import "../governance/mock/MockGovernance.sol"; - -contract MintGoldScheduleTest is Test, Constants, IsL2Check { - using FixidityLib for FixidityLib.Fraction; - - IRegistry registry; - IGoldToken goldToken; - MockGovernance governance; - - MintGoldSchedule mintGoldSchedule; - - address owner = address(this); - - address registryAddress; - address goldTokenAddress = actor("goldTokenAddress"); - - address mintGoldOwner = actor("mintGoldOwner"); - address communityRewardFund = actor("communityRewardFund"); - address carbonOffsettingPartner = actor("carbonOffsettingPartner"); - - address newPartner = actor("newPartner"); - address randomAddress = actor("randomAddress"); - - address constant l1RegistryAddress = 0x000000000000000000000000000000000000ce10; - - // uint256 constant DAILY_MINT_AMOUNT_UPPER = 6749 ether; // 6,749 Gold - uint256 constant DAILY_MINT_AMOUNT_LOWER = 6748256563599655349558; // 6,748 Gold - uint256 constant L1_MINTED_GOLD_SUPPLY = 692702432463315819704447326; // as of May 15 2024 - - uint256 constant GOLD_SUPPLY_CAP = 1000000000 ether; // 1 billion Gold - uint256 constant GENESIS_GOLD_SUPPLY = 600000000 ether; // 600 million Gold - - uint256 constant FIFTEEN_YEAR_LINEAR_REWARD = (GOLD_SUPPLY_CAP - GENESIS_GOLD_SUPPLY) / 2; // 200 million Gold - uint256 constant FIFTEEN_YEAR_GOLD_SUPPLY = GENESIS_GOLD_SUPPLY + FIFTEEN_YEAR_LINEAR_REWARD; // 800 million Gold (includes GENESIS_GOLD_SUPPLY) - - uint256 constant MAX_L2_DISTRIBUTION = FIFTEEN_YEAR_GOLD_SUPPLY - L1_MINTED_GOLD_SUPPLY; // 107.2 million Gold - uint256 constant MAX_L2_COMMUNITY_DISTRIBUTION = MAX_L2_DISTRIBUTION / 4; // 26.8 million Gold - uint256 constant MAX_L2_CARBON_FUND_DISTRIBUTION = MAX_L2_DISTRIBUTION / 1000; // 107,297 Gold - - uint256 constant L2_FIFTEEN_YEAR_GOLD_SUPPLY = - L1_MINTED_GOLD_SUPPLY + MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION; - - uint256 constant l2StartTime = 1715808537; // Arbitary later date (May 15 2024) - uint256 constant communityRewardFraction = FIXED1 / 4; // 25% - uint256 constant carbonOffsettingFraction = FIXED1 / 1000; // 0.1% - uint256 constant newCommunityRewardFraction = FIXED1 / 2; // 50% - uint256 constant newCarbonOffsettingFraction = FIXED1 / 500; // 0.2% - - event CommunityRewardFractionSet(uint256 fraction); - event CarbonOffsettingFundSet(address indexed partner, uint256 fraction); - - function setUp() public virtual { - setUpL1(); - - // Setup L2 after minting L1 supply. - registryAddress = proxyAdminAddress; - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); - registry = IRegistry(registryAddress); - - registry.setAddressFor("GoldToken", address(goldToken)); - registry.setAddressFor("Governance", address(governance)); - - goldToken.setRegistry(registryAddress); - } - - function setUpL1() public { - registryAddress = l1RegistryAddress; - - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); - registry = IRegistry(registryAddress); - - deployCodeTo("GoldToken.sol", abi.encode(false), goldTokenAddress); - goldToken = IGoldToken(goldTokenAddress); - - // Using a mock contract, as foundry does not allow for library linking when using deployCodeTo - governance = new MockGovernance(); - - registry.setAddressFor("GoldToken", address(goldToken)); - - registry.setAddressFor("Governance", address(governance)); - - vm.deal(address(0), GOLD_SUPPLY_CAP); - assertEq(goldToken.totalSupply(), 0, "starting total supply not zero."); - // Mint L1 supply - vm.prank(address(0)); - goldToken.mint(randomAddress, L1_MINTED_GOLD_SUPPLY); - assertEq(goldToken.totalSupply(), L1_MINTED_GOLD_SUPPLY, "total supply incorrect."); - } - - function newMintGold() internal returns (MintGoldSchedule) { - vm.warp(block.timestamp + l2StartTime); - vm.prank(mintGoldOwner); - mintGoldSchedule = new MintGoldSchedule(true); - - goldToken.setGoldTokenMintingScheduleAddress(address(mintGoldSchedule)); - - vm.prank(mintGoldOwner); - mintGoldSchedule.initialize(); - - vm.prank(mintGoldOwner); - - mintGoldSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction, - registryAddress - ); - } -} - -contract MintGoldScheduleTest_initialize is MintGoldScheduleTest { - function setUp() public override { - super.setUp(); - vm.warp(block.timestamp + l2StartTime); - - vm.prank(mintGoldOwner); - mintGoldSchedule = new MintGoldSchedule(true); - goldToken.setGoldTokenMintingScheduleAddress(address(mintGoldSchedule)); - - vm.prank(mintGoldOwner); - mintGoldSchedule.initialize(); - } - - function test_ShouldSetAOwnerToMintGoldScheduleInstance() public { - assertEq(mintGoldSchedule.owner(), mintGoldOwner); - } - - function test_ShouldNotSetBeneficiariesToMintGoldScheduleInstance() public { - assertEq(mintGoldSchedule.communityRewardFund(), address(0)); - assertEq(mintGoldSchedule.carbonOffsettingPartner(), address(0)); - } - - function test_ShouldHaveZeroTotalMintedByScheduleOnInit() public { - assertEq(mintGoldSchedule.totalMintedBySchedule(), 0); - } - - function test_ShouldNotSetTheL2StartTime() public { - assertEq(mintGoldSchedule.l2StartTime(), 0); - } -} - -contract MintGoldScheduleTest_setDependencies_L1 is MintGoldScheduleTest { - function setUp() public override { - super.setUpL1(); - - mintGoldSchedule = new MintGoldSchedule(true); - mintGoldSchedule.initialize(); - } - - function test_Reverts_WhenCalledOnL1() public { - vm.warp(block.timestamp + l2StartTime); - vm.expectRevert("This method is not supported in L1."); - mintGoldSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction, - registryAddress - ); - } -} -contract MintGoldScheduleTest_setDependencies is MintGoldScheduleTest { - function test_ShouldHaveZeroTotalMintedByScheduleOnInit() public { - newMintGold(); - assertEq(mintGoldSchedule.totalMintedBySchedule(), 0); - } - function test_ShouldUpdateDependencies() public { - newMintGold(); - assertEq(mintGoldSchedule.l2StartTime(), l2StartTime); - assertEq(mintGoldSchedule.totalSupplyAtL2Start(), L1_MINTED_GOLD_SUPPLY); - assertEq(mintGoldSchedule.communityRewardFund(), address(governance)); - assertEq(mintGoldSchedule.carbonOffsettingPartner(), carbonOffsettingPartner); - assertEq(mintGoldSchedule.getCarbonOffsettingFraction(), carbonOffsettingFraction); - assertEq(mintGoldSchedule.getCommunityRewardFraction(), communityRewardFraction); - } - - function test_Reverts_WhenRegistryIsTheNullAddress() public { - vm.warp(block.timestamp + l2StartTime); - mintGoldSchedule = new MintGoldSchedule(true); - mintGoldSchedule.initialize(); - - vm.expectRevert("The registry address cannot be the zero address"); - mintGoldSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction, - address(0) - ); - } - - function test_Reverts_WhenCommunityFractionIsZero() public { - vm.warp(block.timestamp + l2StartTime); - mintGoldSchedule = new MintGoldSchedule(true); - mintGoldSchedule.initialize(); - - vm.expectRevert( - "Value must be different from existing community reward fraction and less than 1." - ); - mintGoldSchedule.activate( - l2StartTime, - 0, - carbonOffsettingPartner, - carbonOffsettingFraction, - registryAddress - ); - } - - function test_Reverts_WhenCarbonOffsettingPartnerIsNullAddress() public { - vm.warp(block.timestamp + l2StartTime); - mintGoldSchedule = new MintGoldSchedule(true); - mintGoldSchedule.initialize(); - - vm.expectRevert("Partner cannot be the zero address."); - mintGoldSchedule.activate( - l2StartTime, - communityRewardFraction, - address(0), - carbonOffsettingFraction, - registryAddress - ); - } - - function test_Reverts_WhenRegistryNotUpdated() public { - vm.warp(block.timestamp + l2StartTime); - registry.setAddressFor("Governance", address(0)); - mintGoldSchedule = new MintGoldSchedule(true); - - mintGoldSchedule.initialize(); - - vm.expectRevert("identifier has no registry entry"); - mintGoldSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction, - registryAddress - ); - } - - function test_Reverts_WhenCalledTwice() public { - newMintGold(); - vm.expectRevert("Contract has already been activated."); - - vm.prank(mintGoldOwner); - - mintGoldSchedule.activate( - l2StartTime, - communityRewardFraction, - carbonOffsettingPartner, - carbonOffsettingFraction, - registryAddress - ); - } -} - -contract MintGoldScheduleTest_setCommunityRewardFraction is MintGoldScheduleTest { - function setUp() public override { - super.setUp(); - newMintGold(); - } - function test_ShouldSetNewFraction() public { - vm.prank(mintGoldOwner); - mintGoldSchedule.setCommunityRewardFraction(newCommunityRewardFraction); - assertEq(mintGoldSchedule.getCommunityRewardFraction(), newCommunityRewardFraction); - } - function test_Emits_CommunityRewardFractionSetEvent() public { - vm.expectEmit(true, true, true, true); - emit CommunityRewardFractionSet(newCommunityRewardFraction); - vm.prank(mintGoldOwner); - mintGoldSchedule.setCommunityRewardFraction(newCommunityRewardFraction); - } - function test_Reverts_WhenCalledByOtherThanOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(randomAddress); - mintGoldSchedule.setCommunityRewardFraction(newCommunityRewardFraction); - } - function test_Reverts_WhenFractionIsTheSame() public { - vm.expectRevert( - "Value must be different from existing community reward fraction and less than 1." - ); - vm.prank(mintGoldOwner); - mintGoldSchedule.setCommunityRewardFraction(communityRewardFraction); - } - function test_Reverts_WhenSumOfFractionsGtOne() public { - vm.expectRevert("Sum of partner fractions must be less than or equal to 1."); - vm.prank(mintGoldOwner); - mintGoldSchedule.setCommunityRewardFraction((FIXED1 - 1)); - } - function test_Reverts_WhenDependenciesNotSet() public { - mintGoldSchedule = new MintGoldSchedule(true); - - goldToken.setGoldTokenMintingScheduleAddress(address(mintGoldSchedule)); - - vm.prank(mintGoldOwner); - mintGoldSchedule.initialize(); - - vm.expectRevert("Minting schedule has not been activated."); - vm.prank(mintGoldOwner); - mintGoldSchedule.setCommunityRewardFraction(communityRewardFraction); - } - function test_Reverts_WhenFractionChangesAfter15Years() public { - vm.warp(block.timestamp + (15 * YEAR + 4 * DAY)); - - assertEq(mintGoldSchedule.totalMintedBySchedule(), 0, "Incorrect mintableAmount"); - - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - - vm.warp(block.timestamp + (15 * YEAR) + (4 * DAY)); - - vm.expectRevert( - "Can only update fraction once block reward calculation for years 15-30 has been implemented." - ); - - vm.prank(mintGoldOwner); - mintGoldSchedule.setCommunityRewardFraction(((FIXED1 / 4) * 3)); - } -} - -contract MintGoldScheduleTest_setCarbonOffsettingFund is MintGoldScheduleTest { - function setUp() public override { - super.setUp(); - newMintGold(); - } - - function test_ShouldSetNewPartner() public { - vm.prank(mintGoldOwner); - mintGoldSchedule.setCarbonOffsettingFund(newPartner, carbonOffsettingFraction); - assertEq(mintGoldSchedule.carbonOffsettingPartner(), newPartner); - } - function test_ShouldSetNewFraction() public { - vm.prank(mintGoldOwner); - mintGoldSchedule.setCarbonOffsettingFund(carbonOffsettingPartner, newCarbonOffsettingFraction); - assertEq(mintGoldSchedule.getCarbonOffsettingFraction(), newCarbonOffsettingFraction); - } - - function test_Emits_CarbonOffsettingFundSetEvent() public { - vm.expectEmit(true, true, true, true); - emit CarbonOffsettingFundSet(newPartner, carbonOffsettingFraction); - vm.prank(mintGoldOwner); - mintGoldSchedule.setCarbonOffsettingFund(newPartner, carbonOffsettingFraction); - } - - function test_Reverts_WhenCalledByOtherThanOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(randomAddress); - mintGoldSchedule.setCarbonOffsettingFund(newPartner, carbonOffsettingFraction); - } - - function test_Reverts_WhenPartnerAndFractionAreTheSame() public { - vm.expectRevert("Partner and value must be different from existing carbon offsetting fund."); - vm.prank(mintGoldOwner); - mintGoldSchedule.setCarbonOffsettingFund(carbonOffsettingPartner, carbonOffsettingFraction); - } - - function test_Reverts_WhenSumOfFractionsGtOne() public { - vm.expectRevert("Sum of partner fractions must be less than or equal to 1."); - vm.prank(mintGoldOwner); - mintGoldSchedule.setCarbonOffsettingFund(carbonOffsettingPartner, (FIXED1 - 1)); - } - - function test_Reverts_WhenDependenciesNotSet() public { - mintGoldSchedule = new MintGoldSchedule(true); - - goldToken.setGoldTokenMintingScheduleAddress(address(mintGoldSchedule)); - - vm.prank(mintGoldOwner); - mintGoldSchedule.initialize(); - - vm.expectRevert("Minting schedule has not been activated."); - vm.prank(mintGoldOwner); - mintGoldSchedule.setCarbonOffsettingFund(carbonOffsettingPartner, carbonOffsettingFraction); - } - - function test_Reverts_WhenFractionChangesAfter15Years() public { - vm.warp(block.timestamp + (15 * YEAR + 4 * DAY)); - - assertEq(mintGoldSchedule.totalMintedBySchedule(), 0, "Incorrect mintableAmount"); - - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - - vm.warp(block.timestamp + (15 * YEAR) + (4 * DAY)); - - vm.expectRevert( - "Can only update fraction once block reward calculation for years 15-30 has been implemented." - ); - - vm.prank(mintGoldOwner); - mintGoldSchedule.setCarbonOffsettingFund(carbonOffsettingPartner, ((FIXED1 / 4) * 3)); - } -} - -contract MintGoldScheduleTest_mintAccordingToSchedule_L1 is MintGoldScheduleTest { - uint256 initialMintGoldAmount; - - function setUp() public override { - super.setUpL1(); - - mintGoldSchedule = new MintGoldSchedule(true); - mintGoldSchedule.initialize(); - } - - function test_Reverts_WhenMintingOnL1() public { - vm.warp(block.timestamp + 3 * MONTH + 1 * DAY); - - vm.expectRevert("This method is not supported in L1."); - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - } -} - -contract MintGoldScheduleTest_mintAccordingToSchedule is MintGoldScheduleTest { - uint256 initialMintGoldAmount; - uint256 mintPerPeriod; - - function setUp() public override { - super.setUp(); - - newMintGold(); - } - - function test_Reverts_WhenDependenciesAreNotSet() public { - mintGoldSchedule = new MintGoldSchedule(true); - - vm.prank(mintGoldOwner); - mintGoldSchedule.initialize(); - - vm.expectRevert("Minting schedule has not been activated."); - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - } - - function test_ShouldAllowMintingAsSoon1SecondAfterSettingDependencies() public { - uint256 communityFundBalanceBefore = goldToken.balanceOf(address(governance)); - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - uint256 communityFundBalanceAfter = goldToken.balanceOf(address(governance)); - assertGt(communityFundBalanceAfter, communityFundBalanceBefore); - } - - function test_Reverts_WhenMintableAmountIsZero() public { - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - - vm.expectRevert("Mintable amount must be greater than zero"); - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - } - - function test_ShouldAllowToMint25Percent2years9MonthsPostL2Launch() public { - vm.warp(block.timestamp + 2 * YEAR + 267 * DAY + 63868); // 25% time since L2 - - uint256 expectedMintedAmount = (L2_FIFTEEN_YEAR_GOLD_SUPPLY - L1_MINTED_GOLD_SUPPLY) / 4; - - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - - assertApproxEqRel(mintGoldSchedule.totalMintedBySchedule(), expectedMintedAmount, 1e10); - } - - function test_ShouldAllowToMint50Percent5AndHalfYearsPostL2Launch() public { - vm.warp(block.timestamp + (5 * YEAR) + (170 * DAY) + 41338); - - uint256 expectedMintedAmount = (L2_FIFTEEN_YEAR_GOLD_SUPPLY - L1_MINTED_GOLD_SUPPLY) / 2; - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - - assertApproxEqRel(mintGoldSchedule.totalMintedBySchedule(), expectedMintedAmount, 1e10); - } - - function test_ShouldAllowToMint75Percent11YearsAnd3MonthsPostL2Launch() public { - vm.warp(block.timestamp + 8 * YEAR + 73 * DAY + 18807); - - uint256 expectedMintedAmount = ((L2_FIFTEEN_YEAR_GOLD_SUPPLY - L1_MINTED_GOLD_SUPPLY) / 4) * 3; - - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - - assertApproxEqRel(mintGoldSchedule.totalMintedBySchedule(), expectedMintedAmount, 1e10); - } - - function test_ShouldAllowToMint100Percent11YearsPostL2Launch() public { - uint256 communityFundBalanceBefore = goldToken.balanceOf(address(governance)); - uint256 carbonOffsettingPartnerBalanceBefore = goldToken.balanceOf(carbonOffsettingPartner); - vm.warp(block.timestamp + (11 * YEAR)); - - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - - assertApproxEqRel( - mintGoldSchedule.totalMintedBySchedule(), - MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION, - 1e10 - ); - - uint256 communityFundBalanceAfter = goldToken.balanceOf(address(governance)); - uint256 carbonOffsettingPartnerBalanceAfter = goldToken.balanceOf(carbonOffsettingPartner); - - assertApproxEqRel( - communityFundBalanceAfter - communityFundBalanceBefore, - MAX_L2_COMMUNITY_DISTRIBUTION, - 1e10 - ); - - assertApproxEqRel( - carbonOffsettingPartnerBalanceAfter - carbonOffsettingPartnerBalanceBefore, - MAX_L2_CARBON_FUND_DISTRIBUTION, - 1e10 - ); - } - - function test_ShouldMintUpToLinearSuppplyAfter15Years() public { - vm.warp(block.timestamp + (15 * YEAR) + (4 * DAY)); - - assertEq(mintGoldSchedule.totalMintedBySchedule(), 0, "Incorrect mintableAmount"); - - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - - assertApproxEqRel( - mintGoldSchedule.totalMintedBySchedule(), - MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION, - 1e10 - ); - } - - function test_Reverts_WhenMintingSecondTimeAfter15Years() public { - vm.warp(block.timestamp + (15 * YEAR) + (1 * DAY)); - - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - - assertApproxEqRel( - mintGoldSchedule.totalMintedBySchedule(), - MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION, - 1e10 - ); - - vm.expectRevert("Block reward calculation for years 15-30 unimplemented"); - - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - } -} - -contract MintGoldScheduleTest_getMintableAmount is MintGoldScheduleTest { - uint256 initialMintGoldAmount; - - function setUp() public override { - super.setUp(); - - newMintGold(); - } - - function test_ShouldReturnFullAmountAvailableForThisReleasePeriod() public { - vm.warp(block.timestamp + 1 * DAY); - assertApproxEqRel(mintGoldSchedule.getMintableAmount(), DAILY_MINT_AMOUNT_LOWER, 1e10); - } - - function test_ShouldReturnOnlyAmountNotYetMinted() public { - vm.warp(block.timestamp + 1 * DAY); - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - - vm.warp(block.timestamp + 1 * DAY + 1); - assertApproxEqRel(mintGoldSchedule.getMintableAmount(), DAILY_MINT_AMOUNT_LOWER, 1e10); - } - - function test_ShouldReturnOnlyUpToMaxL2DistributionBeforeItIsMinted() public { - vm.warp(block.timestamp + 16 * YEAR); - assertApproxEqRel( - mintGoldSchedule.getMintableAmount(), - MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION, - 1e10 - ); - } - - function test_Reverts_When15YearsHavePassedAndAllLinearScheduleHaseBeenReleased() public { - vm.warp(block.timestamp + 15 * YEAR); - - vm.prank(randomAddress); - mintGoldSchedule.mintAccordingToSchedule(); - vm.expectRevert("Block reward calculation for years 15-30 unimplemented"); - mintGoldSchedule.getMintableAmount(); - } - - function test_Reverts_WhenDependenciesNotSet() public { - mintGoldSchedule = new MintGoldSchedule(true); - - vm.prank(mintGoldOwner); - mintGoldSchedule.initialize(); - - vm.expectRevert("Minting schedule has not been activated."); - - mintGoldSchedule.getMintableAmount(); - } -} diff --git a/packages/protocol/test-sol/constants.sol b/packages/protocol/test-sol/constants.sol index c677ec8ce77..e6f6a0e15d3 100644 --- a/packages/protocol/test-sol/constants.sol +++ b/packages/protocol/test-sol/constants.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.5.13 <0.8.20; -contract Constants { +contract TestConstants { + // Units uint256 public constant FIXED1 = 1e24; uint256 public constant MINUTE = 60; uint256 public constant HOUR = 60 * MINUTE; @@ -9,15 +10,41 @@ contract Constants { uint256 public constant MONTH = 30 * DAY; uint256 constant WEEK = 7 * DAY; uint256 public constant YEAR = 365 * DAY; + uint256 public constant L2_BLOCK_IN_EPOCH = 43200; - // contract names + // Contract names string constant ElectionContract = "Election"; string constant SortedOraclesContract = "SortedOracles"; string constant StableTokenContract = "StableToken"; string constant GoldTokenContract = "GoldToken"; + string constant CeloTokenContract = "CeloToken"; string constant FreezerContract = "Freezer"; string constant AccountsContract = "Accounts"; string constant LockedGoldContract = "LockedGold"; + string constant LockedCeloContract = "LockedCelo"; string constant ValidatorsContract = "Validators"; string constant GovernanceContract = "Governance"; + string constant EpochRewardsContract = "EpochRewards"; + string constant EpochManagerContract = "EpochManager"; + string constant EpochManagerEnablerContract = "EpochManagerEnabler"; + string constant ScoreManagerContract = "ScoreManager"; + string constant ReserveContract = "Reserve"; + string constant CeloUnreleasedTreasuryContract = "CeloUnreleasedTreasury"; + + // Constant addresses + address constant REGISTRY_ADDRESS = 0x000000000000000000000000000000000000ce10; + address constant PROXY_ADMIN_ADDRESS = 0x4200000000000000000000000000000000000018; + + uint256 constant L1_MINTED_CELO_SUPPLY = 692702432463315819704447326; // as of May 15 2024 + + uint256 constant CELO_SUPPLY_CAP = 1000000000 ether; // 1 billion Celo + uint256 constant GENESIS_CELO_SUPPLY = 600000000 ether; // 600 million Celo + + uint256 constant FIFTEEN_YEAR_LINEAR_REWARD = (CELO_SUPPLY_CAP - GENESIS_CELO_SUPPLY) / 2; // 200 million Celo + + uint256 constant FIFTEEN_YEAR_CELO_SUPPLY = GENESIS_CELO_SUPPLY + FIFTEEN_YEAR_LINEAR_REWARD; // 800 million Celo (includes GENESIS_CELO_SUPPLY) + + uint256 constant MAX_L2_DISTRIBUTION = FIFTEEN_YEAR_CELO_SUPPLY - L1_MINTED_CELO_SUPPLY; // 107.2 million Celo + + uint256 constant L2_INITIAL_STASH_BALANCE = FIFTEEN_YEAR_LINEAR_REWARD + MAX_L2_DISTRIBUTION; // leftover from L1 target supply plus the 2nd 15 year term. } diff --git a/packages/protocol/test-sol/devchain/Import05Dependencies.sol b/packages/protocol/test-sol/devchain/Import05Dependencies.sol new file mode 100644 index 00000000000..30c1a4fa5f8 --- /dev/null +++ b/packages/protocol/test-sol/devchain/Import05Dependencies.sol @@ -0,0 +1,26 @@ +pragma solidity ^0.5.13; + +// this file only exists so that foundry compiles this contracts +import { Proxy } from "@celo-contracts/common/Proxy.sol"; +import { ProxyFactory } from "@celo-contracts/common/ProxyFactory.sol"; +import { GoldToken } from "@celo-contracts/common/GoldToken.sol"; +import { Accounts } from "@celo-contracts/common/Accounts.sol"; +import { Election } from "@celo-contracts/governance/Election.sol"; +import { Governance } from "@celo-contracts/governance/Governance.sol"; +import { LockedGold } from "@celo-contracts/governance/LockedGold.sol"; +import { GovernanceApproverMultiSig } from "@celo-contracts/governance/GovernanceApproverMultiSig.sol"; +import { Escrow } from "@celo-contracts/identity/Escrow.sol"; +import { FederatedAttestations } from "@celo-contracts/identity/FederatedAttestations.sol"; +import { SortedOracles } from "@celo-contracts/stability/SortedOracles.sol"; +import { ReserveSpenderMultiSig } from "@mento-core/contracts/ReserveSpenderMultiSig.sol"; +import { Reserve } from "@mento-core/contracts/Reserve.sol"; +import { StableToken } from "@mento-core/contracts/StableToken.sol"; +import { StableTokenEUR } from "@mento-core/contracts/StableTokenEUR.sol"; +import { StableTokenBRL } from "@mento-core/contracts/StableTokenBRL.sol"; +import { Exchange } from "@mento-core/contracts/Exchange.sol"; + +import { IEpochManager } from "@celo-contracts/common/interfaces/IEpochManager.sol"; +import { IValidators } from "@celo-contracts/governance/interfaces/IValidators.sol"; +import "@celo-contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; + +contract Import05 {} diff --git a/packages/protocol/test-sol/common/ImportPrecompiles.t.sol b/packages/protocol/test-sol/devchain/ImportPrecompiles.t.sol similarity index 61% rename from packages/protocol/test-sol/common/ImportPrecompiles.t.sol rename to packages/protocol/test-sol/devchain/ImportPrecompiles.t.sol index 3c0040b0ab2..a976098c20a 100644 --- a/packages/protocol/test-sol/common/ImportPrecompiles.t.sol +++ b/packages/protocol/test-sol/devchain/ImportPrecompiles.t.sol @@ -3,5 +3,7 @@ pragma solidity >=0.8.7 <0.8.20; // this file only exists so that foundry compiles this contracts import "@test-sol/precompiles/ProofOfPossesionPrecompile.sol"; import "@test-sol/precompiles/EpochSizePrecompile.sol"; +import "@test-sol/precompiles/NumberValidatorsInCurrentSetPrecompile.sol"; +import "@test-sol/precompiles/ValidatorSignerAddressFromCurrentSetPrecompile.sol"; contract ImportPrecompiles {} diff --git a/packages/protocol/test-sol/devchain/README.md b/packages/protocol/test-sol/devchain/README.md new file mode 100644 index 00000000000..2a5099ca230 --- /dev/null +++ b/packages/protocol/test-sol/devchain/README.md @@ -0,0 +1,42 @@ +# Foundry tests that require an anvil devchain + +All tests in this directory are intended to be run against an anvil devchain. To use `forge` +commands in this directory, consume the `[profile.devchain]` defined in +[`foundry.toml`](../../foundry.toml) by +[passing the profile as an environment variable](https://book.getfoundry.sh/config/#configuring-with-foundrytoml), +and use the `--fork-url` flag to point to the anvil devchain. + +These tests are supposed to test the integrity of a chain after it was migrated. + +For example: + +1. Start a local devchain (check latest scripts in [package.json](../../package.json) in + case this command is out-of-date): + + ```sh + $ yarn anvil-devchain:start-L1 + + Waiting Anvil to launch on 8546... + + + _ _ + (_) | | + __ _ _ __ __ __ _ | | + / _` | | '_ \ \ \ / / | | | | + | (_| | | | | | \ V / | | | | + \__,_| |_| |_| \_/ |_| |_| + + 0.2.0 (f625d0f 2024-04-02T00:16:42.824772000Z) + https://github.com/foundry-rs/foundry + + # ... + Listening on 127.0.0.1:8546 + ``` + +1. Run all tests in this directory against the anvil devchain serving at `$ANVIL_RPC_URL`: + + ```sh + $ FOUNDRY_PROFILE=devchain forge test -vvv \ + --match-path "test-sol/devchain/e2e/*" \ + --fork-url $ANVIL_RPC_URL + ``` diff --git a/packages/protocol/test-sol/devchain/e2e/common/EpochManager.t.sol b/packages/protocol/test-sol/devchain/e2e/common/EpochManager.t.sol new file mode 100644 index 00000000000..6283865af02 --- /dev/null +++ b/packages/protocol/test-sol/devchain/e2e/common/EpochManager.t.sol @@ -0,0 +1,862 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import { Devchain } from "@test-sol/devchain/e2e/utils.sol"; +import { Utils08 } from "@test-sol/utils08.sol"; + +import { IEpochManager } from "@celo-contracts/common/interfaces/IEpochManager.sol"; + +import "@celo-contracts-8/common/FeeCurrencyDirectory.sol"; +import "@test-sol/utils/ECDSAHelper08.sol"; +import "@openzeppelin/contracts8/utils/structs/EnumerableSet.sol"; +import { console } from "forge-std/console.sol"; + +contract E2E_EpochManager is Test, Devchain, Utils08, ECDSAHelper08 { + using EnumerableSet for EnumerableSet.AddressSet; + + struct VoterWithPK { + address voter; + uint256 privateKey; + } + + struct GroupWithVotes { + address group; + uint256 votes; + } + + address epochManagerOwner; + address epochManagerEnabler; + address[] firstElected; + + uint256 epochDuration; + + address[] groups; + address[] validatorsArray; + + uint256[] groupScore = [5e23, 7e23, 1e24, 4e23]; + uint256[] validatorScore = [1e23, 1e23, 1e23, 1e23, 1e23, 1e23, 1e23]; + + mapping(address => uint256) addressToPrivateKeys; + mapping(address => VoterWithPK) validatorToVoter; + + EnumerableSet.AddressSet internal electedGroupsHelper; + + function setUp() public virtual { + epochManagerOwner = Ownable(address(epochManager)).owner(); + epochManagerEnabler = registry.getAddressForOrDie(EPOCH_MANAGER_ENABLER_REGISTRY_ID); + firstElected = getValidators().getRegisteredValidators(); + + epochDuration = epochManager.epochDuration(); + + vm.deal(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); // 80% of the total supply to the treasury - whis will be yet distributed + } + + function activateValidators() public { + uint256[] memory valKeys = new uint256[](9); + valKeys[0] = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; + valKeys[1] = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a; + valKeys[2] = 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6; + valKeys[3] = 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a; + valKeys[4] = 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba; + valKeys[5] = 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e; + valKeys[6] = 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356; + valKeys[7] = 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97; + valKeys[8] = 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6; + + for (uint256 i = 0; i < valKeys.length; i++) { + address account = vm.addr(valKeys[i]); + addressToPrivateKeys[account] = valKeys[i]; + } + + address[] memory registeredValidators = getValidators().getRegisteredValidators(); + travelEpochL1(vm); + travelEpochL1(vm); + travelEpochL1(vm); + travelEpochL1(vm); + for (uint256 i = 0; i < registeredValidators.length; i++) { + (, , address validatorGroup, , ) = getValidators().getValidator(registeredValidators[i]); + if (getElection().getPendingVotesForGroup(validatorGroup) == 0) { + continue; + } + vm.startPrank(validatorGroup); + election.activate(validatorGroup); + vm.stopPrank(); + } + } + + function authorizeVoteSigner(uint256 signerPk, address account) internal { + bytes32 messageHash = keccak256(abi.encodePacked(account)); + bytes32 prefixedHash = ECDSAHelper08.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, prefixedHash); + vm.prank(account); + accounts.authorizeVoteSigner(vm.addr(signerPk), v, r, s); + } + + function getLessersAndGreaters( + address[] memory _groups + ) + internal + view + returns ( + address[] memory lessers, + address[] memory greaters, + GroupWithVotes[] memory groupWithVotes + ) + { + (, , uint256 maxTotalRewards, , ) = epochManager.getEpochProcessingState(); + (, groupWithVotes) = getGroupsWithVotes(); + + lessers = new address[](_groups.length); + greaters = new address[](_groups.length); + + uint256[] memory rewards = new uint256[](_groups.length); + + for (uint256 i = 0; i < _groups.length; i++) { + uint256 _groupScore = scoreManager.getGroupScore(_groups[i]); + rewards[i] = election.getGroupEpochRewardsBasedOnScore( + _groups[i], + maxTotalRewards, + _groupScore + ); + } + for (uint256 i = 0; i < _groups.length; i++) { + for (uint256 j = 0; j < groupWithVotes.length; j++) { + if (groupWithVotes[j].group == _groups[i]) { + groupWithVotes[j].votes += rewards[i]; + break; + } + } + sort(groupWithVotes); + + address lesser = address(0); + address greater = address(0); + + for (uint256 j = 0; j < groupWithVotes.length; j++) { + if (groupWithVotes[j].group == _groups[i]) { + greater = j == 0 ? address(0) : groupWithVotes[j - 1].group; + lesser = j == groupWithVotes.length - 1 ? address(0) : groupWithVotes[j + 1].group; + break; + } + } + + lessers[i] = lesser; + greaters[i] = greater; + } + } + + function getGroupsWithVotes() + internal + view + returns (address[] memory groupsInOrder, GroupWithVotes[] memory groupWithVotes) + { + uint256[] memory votesTotal; + (groupsInOrder, votesTotal) = election.getTotalVotesForEligibleValidatorGroups(); + + groupWithVotes = new GroupWithVotes[](groupsInOrder.length); + for (uint256 i = 0; i < groupsInOrder.length; i++) { + groupWithVotes[i] = GroupWithVotes(groupsInOrder[i], votesTotal[i]); + } + } + + // Bubble sort algorithm since it is a small array + function sort(GroupWithVotes[] memory items) internal pure { + uint length = items.length; + for (uint i = 0; i < length; i++) { + for (uint j = 0; j < length - 1; j++) { + if (items[j].votes < items[j + 1].votes) { + // Swap + GroupWithVotes memory temp = items[j]; + items[j] = items[j + 1]; + items[j + 1] = temp; + } + } + } + } + + function assertGroupWithVotes(GroupWithVotes[] memory groupWithVotes) internal { + for (uint256 i = 0; i < groupWithVotes.length; i++) { + assertEq(election.getTotalVotesForGroup(groupWithVotes[i].group), groupWithVotes[i].votes); + } + } + + function registerNewValidatorGroupWithValidator( + uint256 index, + uint256 validatorCount + ) internal returns (address newValidatorGroup, address newValidator) { + require(validatorCount > 0, "validatorCount must be at least 1"); + (, GroupWithVotes[] memory groupWithVotes) = getGroupsWithVotes(); + uint256 newGroupPK = uint256(keccak256(abi.encodePacked("newGroup", index + 1))); + + address[] memory validatorAddresses = new address[](validatorCount); + + (uint256 validatorLockedGoldRequirement, ) = validators.getValidatorLockedGoldRequirements(); + (uint256 groupLockedGoldRequirement, ) = validators.getGroupLockedGoldRequirements(); + + vm.deal(vm.addr(newGroupPK), 100_000_000 ether); + + newValidatorGroup = registerValidatorGroup( + "newGroup", + newGroupPK, + groupLockedGoldRequirement + validatorCount * validatorLockedGoldRequirement, + 100000000000000000000000 + ); + + for (uint256 i = 0; i < validatorCount; i++) { + uint256 newValidatorPK = uint256(keccak256(abi.encodePacked("newValidator", index, i + 1))); + validatorAddresses[i] = vm.addr(newValidatorPK); + vm.deal(vm.addr(newValidatorPK), 100_000_000 ether); + + newValidator = registerValidator( + newValidatorPK, + validatorLockedGoldRequirement, + newValidatorGroup + ); + + vm.prank(scoreManager.owner()); + scoreManager.setValidatorScore(validatorAddresses[i], validatorScore[6]); + } + + vm.prank(newValidatorGroup); + validators.addFirstMember(validatorAddresses[0], address(0), groupWithVotes[0].group); + uint256 nonVotingLockedGold = lockedCelo.getAccountNonvotingLockedGold(validatorAddresses[0]); + vm.prank(newValidatorGroup); + election.vote(newValidatorGroup, nonVotingLockedGold, address(0), groupWithVotes[0].group); + + for (uint256 i = 1; i < validatorAddresses.length; i++) { + vm.prank(validatorAddresses[i]); + validators.affiliate(newValidatorGroup); + vm.prank(newValidatorGroup); + validators.addMember(validatorAddresses[i]); + } + + vm.prank(scoreManager.owner()); + scoreManager.setGroupScore(newValidatorGroup, groupScore[3]); + } + + function getValidatorGroupsFromElected() internal view returns (address[] memory) { + address[] memory elected = epochManager.getElectedAccounts(); + address[] memory validatorGroups = new address[](elected.length); + for (uint256 i = 0; i < elected.length; i++) { + (, , address group, , ) = validators.getValidator(elected[i]); + validatorGroups[i] = group; + } + return validatorGroups; + } + + function registerValidatorGroup( + string memory groupName, + uint256 privateKey, + uint256 amountToLock, + uint256 commission + ) public returns (address accountAddress) { + accountAddress = vm.addr(privateKey); + vm.startPrank(accountAddress); + lockGold(amountToLock); + getAccounts().setName(groupName); + getValidators().registerValidatorGroup(commission); + vm.stopPrank(); + } + + function registerValidator( + uint256 privateKey, + uint256 amountToLock, + address groupToAffiliate + ) public returns (address) { + address accountAddress = vm.addr(privateKey); + vm.startPrank(accountAddress); + lockGold(amountToLock); + + (bytes memory ecdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner(accountAddress, privateKey); + getValidators().registerValidatorNoBls(ecdsaPubKey); + getValidators().affiliate(groupToAffiliate); + + vm.stopPrank(); + return accountAddress; + } + + function _generateEcdsaPubKeyWithSigner( + address _validator, + uint256 _signerPk + ) internal returns (bytes memory ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) { + (v, r, s) = getParsedSignatureOfAddress(_validator, _signerPk); + + bytes32 addressHash = keccak256(abi.encodePacked(_validator)); + ecdsaPubKey = addressToPublicKey(addressHash, v, r, s); + } + + function getParsedSignatureOfAddress( + address _address, + uint256 privateKey + ) public pure returns (uint8, bytes32, bytes32) { + bytes32 addressHash = keccak256(abi.encodePacked(_address)); + bytes32 prefixedHash = toEthSignedMessageHash(addressHash); + return vm.sign(privateKey, prefixedHash); + } + + function lockGold(uint256 value) public { + getAccounts().createAccount(); + getLockedGold().lock{ value: value }(); + } + + function getCurrentlyElectedGroups() internal returns (address[] memory) { + address[] memory currentlyElected = epochManager.getElectedAccounts(); + + // clearElectedGroupsHelper(); + for (uint256 i = 0; i < currentlyElected.length; i++) { + (, , address group, , ) = validators.getValidator(currentlyElected[i]); + electedGroupsHelper.add(group); + } + return electedGroupsHelper.values(); + } +} + +contract E2E_EpochManager_InitializeSystem is E2E_EpochManager { + function setUp() public override { + super.setUp(); + whenL2(vm); + } + + function test_shouldRevert_WhenCalledByNonEnabler() public { + vm.expectRevert("msg.sender is not Enabler"); + epochManager.initializeSystem(1, 1, firstElected); + } + + function test_ShouldInitializeSystem() public { + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(42, 43, firstElected); + + assertEq(epochManager.firstKnownEpoch(), 42); + assertEq(epochManager.getCurrentEpochNumber(), 42); + + assertTrue(epochManager.systemAlreadyInitialized()); + } +} +contract E2E_EpochManager_GetCurrentEpoch is E2E_EpochManager { + function setUp() public override { + super.setUp(); + whenL2(vm); + } + + function test_Revert_WhenSystemNotInitialized() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.getCurrentEpoch(); + } + + function test_ReturnExpectedValues() public { + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(42, 43, firstElected); + + assertEq(epochManager.firstKnownEpoch(), 42); + assertEq(epochManager.getCurrentEpochNumber(), 42); + + ( + uint256 firstBlock, + uint256 lastBlock, + uint256 startTimestamp, + uint256 rewardsBlock + ) = epochManager.getCurrentEpoch(); + assertEq(firstBlock, 43); + assertEq(lastBlock, 0); + assertEq(startTimestamp, block.timestamp); + assertEq(rewardsBlock, 0); + } +} + +contract E2E_EpochManager_StartNextEpochProcess is E2E_EpochManager { + function setUp() public override { + super.setUp(); + activateValidators(); + whenL2(vm); + + validatorsArray = getValidators().getRegisteredValidators(); + groups = getValidators().getRegisteredValidatorGroups(); + + address scoreManagerOwner = scoreManager.owner(); + + vm.startPrank(scoreManagerOwner); + scoreManager.setGroupScore(groups[0], groupScore[0]); + scoreManager.setGroupScore(groups[1], groupScore[1]); + scoreManager.setGroupScore(groups[2], groupScore[2]); + + scoreManager.setValidatorScore(validatorsArray[0], validatorScore[0]); + scoreManager.setValidatorScore(validatorsArray[1], validatorScore[1]); + scoreManager.setValidatorScore(validatorsArray[2], validatorScore[2]); + scoreManager.setValidatorScore(validatorsArray[3], validatorScore[3]); + scoreManager.setValidatorScore(validatorsArray[4], validatorScore[4]); + scoreManager.setValidatorScore(validatorsArray[5], validatorScore[5]); + + vm.stopPrank(); + + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(1, 1, firstElected); + } + + function test_shouldHaveInitialValues() public { + assertEq(epochManager.firstKnownEpoch(), 1); + assertEq(epochManager.getCurrentEpochNumber(), 1); + + // get getEpochProcessingState + ( + uint256 status, + uint256 perValidatorReward, + uint256 totalRewardsVote, + uint256 totalRewardsCommunity, + uint256 totalRewardsCarbonFund + ) = epochManager.getEpochProcessingState(); + assertEq(status, 0); // Not started + assertEq(perValidatorReward, 0); + assertEq(totalRewardsVote, 0); + assertEq(totalRewardsCommunity, 0); + assertEq(totalRewardsCarbonFund, 0); + } + + function test_shouldStartNextEpochProcessing() public { + timeTravel(vm, epochDuration + 1); + + epochManager.startNextEpochProcess(); + + ( + uint256 status, + uint256 perValidatorReward, + uint256 totalRewardsVote, + uint256 totalRewardsCommunity, + uint256 totalRewardsCarbonFund + ) = epochManager.getEpochProcessingState(); + assertEq(status, 1); // Started + assertGt(perValidatorReward, 0, "perValidatorReward"); + assertGt(totalRewardsVote, 0, "totalRewardsVote"); + assertGt(totalRewardsCommunity, 0, "totalRewardsCommunity"); + assertGt(totalRewardsCarbonFund, 0, "totalRewardsCarbonFund"); + } +} + +contract E2E_EpochManager_FinishNextEpochProcess is E2E_EpochManager { + using EnumerableSet for EnumerableSet.AddressSet; + + EnumerableSet.AddressSet internal originalyElected; + + function setUp() public override { + super.setUp(); + activateValidators(); + whenL2(vm); + + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(1, 1, firstElected); + + validatorsArray = getValidators().getRegisteredValidators(); + groups = getValidators().getRegisteredValidatorGroups(); + + address scoreManagerOwner = scoreManager.owner(); + + vm.startPrank(scoreManagerOwner); + scoreManager.setGroupScore(groups[0], groupScore[0]); + scoreManager.setGroupScore(groups[1], groupScore[1]); + scoreManager.setGroupScore(groups[2], groupScore[2]); + + scoreManager.setValidatorScore(validatorsArray[0], validatorScore[0]); + scoreManager.setValidatorScore(validatorsArray[1], validatorScore[1]); + scoreManager.setValidatorScore(validatorsArray[2], validatorScore[2]); + scoreManager.setValidatorScore(validatorsArray[3], validatorScore[3]); + scoreManager.setValidatorScore(validatorsArray[4], validatorScore[4]); + scoreManager.setValidatorScore(validatorsArray[5], validatorScore[5]); + + vm.stopPrank(); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + } + + function test_shouldFinishNextEpochProcessing() public { + address[] memory lessers; + address[] memory greaters; + GroupWithVotes[] memory groupWithVotes; + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + + uint256 currentEpoch = epochManager.getCurrentEpochNumber(); + address[] memory currentlyElected = epochManager.getElectedAccounts(); + for (uint256 i = 0; i < currentlyElected.length; i++) { + originalyElected.add(currentlyElected[i]); + } + + // wait some time before finishing + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + epochManager.finishNextEpochProcess(groups, lessers, greaters); + + assertEq(currentEpoch + 1, epochManager.getCurrentEpochNumber()); + + for (uint256 i = 0; i < currentlyElected.length; i++) { + assertEq(originalyElected.contains(currentlyElected[i]), true); + } + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + // wait some time before finishing + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + assertEq(currentEpoch + 2, epochManager.getCurrentEpochNumber()); + + address[] memory newlyElected2 = epochManager.getElectedAccounts(); + + for (uint256 i = 0; i < currentlyElected.length; i++) { + assertEq(originalyElected.contains(newlyElected2[i]), true); + } + + // add new validator group and validator + (address newValidatorGroup, address newValidator) = registerNewValidatorGroupWithValidator( + 0, + 1 + ); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + groups.push(newValidatorGroup); + validatorsArray.push(newValidator); + + assertEq(epochManager.getElectedAccounts().length, validators.getRegisteredValidators().length); + assertEq(groups.length, validators.getRegisteredValidatorGroups().length); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + assertEq(epochManager.getElectedAccounts().length, validatorsArray.length); + + // lower the number of electable validators + vm.prank(election.owner()); + election.setElectableValidators(1, validatorsArray.length - 1); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + ( + , + uint256 perValidatorReward, + uint256 totalRewardsVoter, + uint256 totalRewardsCommunity, + uint256 totalRewardsCarbonFund + ) = epochManager.getEpochProcessingState(); + + assertEq(perValidatorReward, 0, "perValidatorReward"); + assertEq(totalRewardsVoter, 0, "totalRewardsVoter"); + assertEq(totalRewardsCommunity, 0, "totalRewardsCommunity"); + assertEq(totalRewardsCarbonFund, 0, "totalRewardsCarbonFund"); + + assertEq(epochManager.getElectedAccounts().length, validatorsArray.length - 1); + } + + function clearElectedGroupsHelper() internal { + address[] memory values = electedGroupsHelper.values(); + + for (uint256 i = 0; i < values.length; i++) { + electedGroupsHelper.remove(values[i]); + } + } +} + +contract E2E_GasTest_Setup is E2E_EpochManager { + using EnumerableSet for EnumerableSet.AddressSet; + EnumerableSet.AddressSet internal originalyElected; + + function setUpHelper(uint256 validatorGroupCount, uint256 validatorPerGroupCount) internal { + activateValidators(); + whenL2(vm); + + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(1, 1, firstElected); + + validatorsArray = getValidators().getRegisteredValidators(); + groups = getValidators().getRegisteredValidatorGroups(); + + vm.startPrank(scoreManager.owner()); + scoreManager.setGroupScore(groups[0], groupScore[0]); + scoreManager.setGroupScore(groups[1], groupScore[1]); + scoreManager.setGroupScore(groups[2], groupScore[2]); + + scoreManager.setValidatorScore(validatorsArray[0], validatorScore[0]); + scoreManager.setValidatorScore(validatorsArray[1], validatorScore[1]); + scoreManager.setValidatorScore(validatorsArray[2], validatorScore[2]); + scoreManager.setValidatorScore(validatorsArray[3], validatorScore[3]); + scoreManager.setValidatorScore(validatorsArray[4], validatorScore[4]); + scoreManager.setValidatorScore(validatorsArray[5], validatorScore[5]); + + vm.stopPrank(); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + address[] memory lessers; + address[] memory greaters; + GroupWithVotes[] memory groupWithVotes; + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + + uint256 currentEpoch = epochManager.getCurrentEpochNumber(); + address[] memory currentlyElected = epochManager.getElectedAccounts(); + for (uint256 i = 0; i < currentlyElected.length; i++) { + originalyElected.add(currentlyElected[i]); + } + + // wait some time before finishing + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + epochManager.finishNextEpochProcess(groups, lessers, greaters); + + assertEq(currentEpoch + 1, epochManager.getCurrentEpochNumber()); + + for (uint256 i = 0; i < currentlyElected.length; i++) { + assertEq(originalyElected.contains(currentlyElected[i]), true); + } + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + // wait some time before finishing + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + assertEq(currentEpoch + 2, epochManager.getCurrentEpochNumber()); + + address[] memory newlyElected2 = epochManager.getElectedAccounts(); + + for (uint256 i = 0; i < currentlyElected.length; i++) { + assertEq(originalyElected.contains(newlyElected2[i]), true); + } + + for (uint256 i = 0; i < validatorGroupCount; i++) { + registerNewValidatorGroupWithValidator(i, validatorPerGroupCount); + } + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + + activateValidators(); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + groups = getCurrentlyElectedGroups(); + + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + } +} + +contract E2E_GasTest1_FinishNextEpochProcess is E2E_GasTest_Setup { + function setUp() public override { + super.setUp(); + super.setUpHelper(120, 2); + } + + /** + * @notice Test the gas used by finishNextEpochProcess + This test is trying to measure gas used by finishNextEpochProcess in a real life worst case. We have 126 validators and 123 groups. + There are two main loops in the function, one for calculating rewards and the other for updating the elected validators. + FinishNextEpochProcess is called twice, first time with going from 6 -> 110 validators which consumes approx. 6M gas and the second time with going from 110 -> 110 validators which consumes approx. 19M gas. + */ + function test_shouldFinishNextEpochProcessing_GasTest() public { + address[] memory lessers; + address[] memory greaters; + GroupWithVotes[] memory groupWithVotes; + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + uint256 gasLeftBefore1 = gasleft(); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + uint256 gasLeftAfter1 = gasleft(); + console.log("validator groups: 120"); + console.log("validators per group: 2"); + console.log("finishNextEpochProcess gas used 2: ", gasLeftBefore1 - gasLeftAfter1); + console.log("elected count2: ", epochManager.getElectedAccounts().length); + } +} + +contract E2E_GasTest2_FinishNextEpochProcess is E2E_GasTest_Setup { + function setUp() public override { + super.setUp(); + super.setUpHelper(60, 2); + } + + /** + * @notice Test the gas used by finishNextEpochProcess + This test is trying to measure gas used by finishNextEpochProcess in a real life worst case. We have 126 validators and 123 groups. + There are two main loops in the function, one for calculating rewards and the other for updating the elected validators. + FinishNextEpochProcess is called twice, first time with going from 6 -> 110 validators which consumes approx. 6M gas and the second time with going from 110 -> 110 validators which consumes approx. 19M gas. + */ + function test_shouldFinishNextEpochProcessing_GasTest() public { + address[] memory lessers; + address[] memory greaters; + GroupWithVotes[] memory groupWithVotes; + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + uint256 gasLeftBefore1 = gasleft(); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + uint256 gasLeftAfter1 = gasleft(); + console.log("validator groups: 60"); + console.log("validators per group: 2"); + console.log("finishNextEpochProcess gas used 2: ", gasLeftBefore1 - gasLeftAfter1); + console.log("elected count2: ", epochManager.getElectedAccounts().length); + } +} + +contract E2E_FinishNextEpochProcess_Split is E2E_GasTest_Setup { + using EnumerableSet for EnumerableSet.AddressSet; + + function setUp() public override { + super.setUp(); + + activateValidators(); + whenL2(vm); + + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(1, 1, firstElected); + + validatorsArray = getValidators().getRegisteredValidators(); + groups = getValidators().getRegisteredValidatorGroups(); + + vm.startPrank(scoreManager.owner()); + scoreManager.setGroupScore(groups[0], groupScore[0]); + scoreManager.setGroupScore(groups[1], groupScore[1]); + scoreManager.setGroupScore(groups[2], groupScore[2]); + + scoreManager.setValidatorScore(validatorsArray[0], validatorScore[0]); + scoreManager.setValidatorScore(validatorsArray[1], validatorScore[1]); + scoreManager.setValidatorScore(validatorsArray[2], validatorScore[2]); + scoreManager.setValidatorScore(validatorsArray[3], validatorScore[3]); + scoreManager.setValidatorScore(validatorsArray[4], validatorScore[4]); + scoreManager.setValidatorScore(validatorsArray[5], validatorScore[5]); + + vm.stopPrank(); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + address[] memory lessers; + address[] memory greaters; + GroupWithVotes[] memory groupWithVotes; + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + + uint256 currentEpoch = epochManager.getCurrentEpochNumber(); + address[] memory currentlyElected = epochManager.getElectedAccounts(); + for (uint256 i = 0; i < currentlyElected.length; i++) { + originalyElected.add(currentlyElected[i]); + } + + // wait some time before finishing + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + epochManager.setToProcessGroups(); + for (uint256 i = 0; i < groups.length; i++) { + epochManager.processGroup(groups[i], lessers[i], greaters[i]); + } + + assertEq(currentEpoch + 1, epochManager.getCurrentEpochNumber()); + + for (uint256 i = 0; i < currentlyElected.length; i++) { + assertEq(originalyElected.contains(currentlyElected[i]), true); + } + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + // wait some time before finishing + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.setToProcessGroups(); + for (uint256 i = 0; i < groups.length; i++) { + epochManager.processGroup(groups[i], lessers[i], greaters[i]); + } + // epochManager.finishNextEpochProcess(groups, lessers, greaters); + assertGroupWithVotes(groupWithVotes); + + assertEq(currentEpoch + 2, epochManager.getCurrentEpochNumber()); + + address[] memory newlyElected2 = epochManager.getElectedAccounts(); + + for (uint256 i = 0; i < currentlyElected.length; i++) { + assertEq(originalyElected.contains(newlyElected2[i]), true); + } + uint256 validatorGroupCount = 60; + uint256 validatorPerGroupCount = 2; + + for (uint256 i = 0; i < validatorGroupCount; i++) { + registerNewValidatorGroupWithValidator(i, validatorPerGroupCount); + } + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + + epochManager.setToProcessGroups(); + for (uint256 i = 0; i < groups.length; i++) { + epochManager.processGroup(groups[i], lessers[i], greaters[i]); + } + + activateValidators(); + + timeTravel(vm, epochDuration + 1); + epochManager.startNextEpochProcess(); + + groups = getCurrentlyElectedGroups(); + + timeTravel(vm, epochDuration / 2); + blockTravel(vm, 100); + } + + /** + * @notice Test the gas used by finishNextEpochProcess + This test is trying to measure gas used by finishNextEpochProcess in a real life worst case. We have 126 validators and 123 groups. + There are two main loops in the function, one for calculating rewards and the other for updating the elected validators. + FinishNextEpochProcess is called twice, first time with going from 6 -> 110 validators which consumes approx. 6M gas and the second time with going from 110 -> 110 validators which consumes approx. 19M gas. + */ + function test_shouldFinishNextEpochProcessing_GasTest_Split() public { + address[] memory lessers; + address[] memory greaters; + GroupWithVotes[] memory groupWithVotes; + (lessers, greaters, groupWithVotes) = getLessersAndGreaters(groups); + epochManager.setToProcessGroups(); + + for (uint256 i = 0; i < groups.length; i++) { + uint256 gasLeftBefore1 = gasleft(); + epochManager.processGroup(groups[i], lessers[i], greaters[i]); + uint256 gasLeftAfter1 = gasleft(); + console.log("processGroup gas used: ", gasLeftBefore1 - gasLeftAfter1); + } + } +} diff --git a/packages/protocol/test-sol/devchain/e2e/common/FeeCurrencyDirectory.t.sol b/packages/protocol/test-sol/devchain/e2e/common/FeeCurrencyDirectory.t.sol new file mode 100644 index 00000000000..f613eb2574a --- /dev/null +++ b/packages/protocol/test-sol/devchain/e2e/common/FeeCurrencyDirectory.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import { Devchain } from "@test-sol/devchain/e2e/utils.sol"; + +import "@celo-contracts-8/common/FeeCurrencyDirectory.sol"; + +contract E2EDemo is Test, Devchain { + function test_ShouldAllowOwnerSetCurrencyConfig() public { + address token = address(1); + uint256 intrinsicGas = 21000; + + vm.prank(feeCurrencyDirectory.owner()); + feeCurrencyDirectory.setCurrencyConfig(token, address(sortedOracles), intrinsicGas); + FeeCurrencyDirectory.CurrencyConfig memory config = feeCurrencyDirectory.getCurrencyConfig( + token + ); + + assertEq(config.oracle, address(sortedOracles)); + assertEq(config.intrinsicGas, intrinsicGas); + } +} diff --git a/packages/protocol/test-sol/devchain/e2e/utils.sol b/packages/protocol/test-sol/devchain/e2e/utils.sol new file mode 100644 index 00000000000..0e84cfafa26 --- /dev/null +++ b/packages/protocol/test-sol/devchain/e2e/utils.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "@celo-contracts-8/common/UsingRegistry.sol"; +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import { IEpochManager } from "@celo-contracts/common/interfaces/IEpochManager.sol"; +import { IAccounts } from "@celo-contracts/common/interfaces/IAccounts.sol"; +import { IScoreManager } from "@celo-contracts-8/common/interfaces/IScoreManager.sol"; +import { IValidators } from "@celo-contracts/governance/interfaces/IValidators.sol"; +import { IElection } from "@celo-contracts/governance/interfaces/IElection.sol"; +import { ILockedCelo } from "@celo-contracts/governance/interfaces/ILockedCelo.sol"; + +// All core contracts that are expected to be in the Registry on the devchain +import "@celo-contracts-8/common/FeeCurrencyDirectory.sol"; +import "@celo-contracts/stability/interfaces/ISortedOracles.sol"; +import "@celo-contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; + +import { TestConstants } from "@test-sol/constants.sol"; + +contract Devchain is UsingRegistry, TestConstants { + // Used in exceptional circumstances when a contract is not in UsingRegistry.sol + IRegistry devchainRegistry = IRegistry(REGISTRY_ADDRESS); + + // All core contracts that are expected to be in the Registry on the devchain + ISortedOracles sortedOracles; + FeeCurrencyDirectory feeCurrencyDirectory; + IEpochManager epochManager; + ICeloUnreleasedTreasury celoUnreleasedTreasury; + IValidators validators; + IAccounts accounts; + IScoreManager scoreManager; + IElection election; + ILockedCelo lockedCelo; + + constructor() { + // The following line is required by UsingRegistry.sol + setRegistry(REGISTRY_ADDRESS); + + // Fetch all core contracts that are expeceted to be in the Registry on the devchain + sortedOracles = getSortedOracles(); + feeCurrencyDirectory = FeeCurrencyDirectory( + devchainRegistry.getAddressForStringOrDie("FeeCurrencyDirectory") + ); // FeeCurrencyDirectory is not in UsingRegistry.sol + + epochManager = getEpochManager(); + celoUnreleasedTreasury = getCeloUnreleasedTreasury(); + validators = getValidators(); + accounts = getAccounts(); + scoreManager = IScoreManager(address(getScoreReader())); + election = getElection(); + lockedCelo = getLockedCelo(); + + // TODO: Add missing core contracts below (see list in migrations_sol/constants.sol) + // TODO: Consider asserting that all contracts we expect are available in the Devchain class + // (see list in migrations_sol/constants.sol) + } +} diff --git a/packages/protocol/test-sol/devchain/migration/05Links.sol b/packages/protocol/test-sol/devchain/migration/05Links.sol new file mode 100644 index 00000000000..03ec70443db --- /dev/null +++ b/packages/protocol/test-sol/devchain/migration/05Links.sol @@ -0,0 +1,23 @@ +// This file exists only to force migration tests also compile below imported contracts. +pragma solidity ^0.5.13; + +import "@celo-contracts/governance/BlockchainParameters.sol"; +import "@celo-contracts/governance/DoubleSigningSlasher.sol"; +import "@celo-contracts/governance/DowntimeSlasher.sol"; +import "@celo-contracts/governance/EpochRewards.sol"; +import "@celo-contracts/governance/GovernanceSlasher.sol"; +import "@celo-contracts/governance/LockedGold.sol"; +import "@celo-contracts/common/FeeCurrencyWhitelist.sol"; +import "@celo-contracts/common/Freezer.sol"; +import "@celo-contracts/common/FeeHandler.sol"; +import "@celo-contracts/identity/OdisPayments.sol"; +import "@celo-contracts/identity/Random.sol"; +import "@celo-contracts/common/Registry.sol"; +import "@celo-contracts/common/UniswapFeeHandlerSeller.sol"; +import "@celo-contracts/common/MentoFeeHandlerSeller.sol"; + +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; + +contract BlockchainParametersTest is TestWithUtils { + function test_dummy_test() public {} +} diff --git a/packages/protocol/test-sol/devchain/migration/IntegrationValidators.t.sol b/packages/protocol/test-sol/devchain/migration/IntegrationValidators.t.sol new file mode 100644 index 00000000000..2ba650dffe7 --- /dev/null +++ b/packages/protocol/test-sol/devchain/migration/IntegrationValidators.t.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import { Devchain } from "@test-sol/devchain/e2e/utils.sol"; + +contract IntegrationsValidators is Test, Devchain { + function test_deaffiliateWorskWithEpochManager() public { + vm.prank(election.electValidatorAccounts()[0]); + validators.deaffiliate(); + } +} diff --git a/packages/protocol/test-sol/devchain/migration/Migration.t.sol b/packages/protocol/test-sol/devchain/migration/Migration.t.sol new file mode 100644 index 00000000000..bfe56896280 --- /dev/null +++ b/packages/protocol/test-sol/devchain/migration/Migration.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0 <0.8.20; + +import "celo-foundry-8/Test.sol"; + +import { Utils08 } from "@test-sol/utils08.sol"; +import { TestConstants } from "@test-sol/constants.sol"; +import { MigrationsConstants } from "@migrations-sol/constants.sol"; +import { FeeCurrencyDirectory } from "@celo-contracts-8/common/FeeCurrencyDirectory.sol"; + +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/common/interfaces/IProxy.sol"; +import "@celo-contracts/common/interfaces/ICeloToken.sol"; +import "@celo-contracts/common/interfaces/IAccounts.sol"; +import "@celo-contracts/common/interfaces/IEpochManager.sol"; +import "@celo-contracts/common/interfaces/IEpochManagerEnabler.sol"; +import "@celo-contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; +import "@celo-contracts/governance/interfaces/IElection.sol"; + +import "@celo-contracts/governance/interfaces/IValidators.sol"; + +import "@celo-contracts-8/common/interfaces/IPrecompiles.sol"; +import "@celo-contracts-8/common/interfaces/IScoreManager.sol"; + +import "@openzeppelin/contracts8/token/ERC20/IERC20.sol"; + +contract IntegrationTest is Test, TestConstants, Utils08 { + IRegistry registry = IRegistry(REGISTRY_ADDRESS); + + uint256 constant RESERVE_BALANCE = 69411663406170917420347916; // current as of 08/20/24 + + // function setUp() public virtual {} + + /** + * @notice Removes CBOR encoded metadata from the tail of the deployedBytecode. + * @param data Bytecode including the CBOR encoded tail. + * @return Bytecode without the CBOR encoded metadata. + */ + function removeMetadataFromBytecode(bytes memory data) public pure returns (bytes memory) { + // Ensure the data length is at least enough to contain the length specifier + require(data.length >= 2, "Data too short to contain a valid CBOR length specifier"); + + // Calculate the length of the CBOR encoded section from the last two bytes + uint16 cborLength = uint16(uint8(data[data.length - 2])) * + 256 + + uint16(uint8(data[data.length - 1])); + + // Ensure the length is valid (not greater than the data array length minus 2 bytes for the length field) + require(cborLength <= data.length - 2, "Invalid CBOR length"); + + // Calculate the new length of the data without the CBOR section + uint newLength = data.length - 2 - cborLength; + + // Create a new byte array for the result + bytes memory result = new bytes(newLength); + + // Copy data from the original byte array to the new one, excluding the CBOR section and its length field + for (uint i = 0; i < newLength; i++) { + result[i] = data[i]; + } + + return result; + } +} + +contract RegistryIntegrationTest is IntegrationTest, MigrationsConstants { + IProxy proxy; + + function test_shouldHaveAddressInRegistry() public view { + for (uint256 i = 0; i < contractsInRegistry.length; i++) { + string memory contractName = contractsInRegistry[i]; + address contractAddress = registry.getAddressFor(keccak256(abi.encodePacked(contractName))); + console2.log(contractName, "address in Registry is: ", contractAddress); + assert(contractAddress != address(0)); + } + } + + function test_shouldHaveCorrectBytecode() public { + // Converting contract names to hashes for comparison + bytes32 hashAccount = keccak256(abi.encodePacked("Accounts")); + bytes32 hashElection = keccak256(abi.encodePacked("Election")); + bytes32 hashEscrow = keccak256(abi.encodePacked("Escrow")); + bytes32 hashFederatedAttestations = keccak256(abi.encodePacked("FederatedAttestations")); + bytes32 hashGovernance = keccak256(abi.encodePacked("Governance")); + bytes32 hashSortedOracles = keccak256(abi.encodePacked("SortedOracles")); + bytes32 hashValidators = keccak256(abi.encodePacked("Validators")); + bytes32 hashCeloToken = keccak256(abi.encodePacked("CeloToken")); + bytes32 hashLockedCelo = keccak256(abi.encodePacked("LockedCelo")); + bytes32 hashEpochManager = keccak256(abi.encodePacked("EpochManager")); + + for (uint256 i = 0; i < contractsInRegistry.length; i++) { + // Read name from list of core contracts + string memory contractName = contractsInRegistry[i]; + console2.log("Checking bytecode of:", contractName); + + // Skipping test for contracts that depend on linked libraries + // This is a known limitation in Foundry at the moment: + // Source: https://github.com/foundry-rs/foundry/issues/6120 + bytes32 hashContractName = keccak256(abi.encodePacked(contractName)); + if ( + hashContractName != hashAccount && + hashContractName != hashElection && + hashContractName != hashEscrow && + hashContractName != hashFederatedAttestations && + hashContractName != hashGovernance && + hashContractName != hashSortedOracles && + hashContractName != hashValidators && + hashContractName != hashCeloToken && // TODO: remove once GoldToken contract has been renamed to CeloToken + hashContractName != hashLockedCelo && // TODO: remove once LockedGold contract has been renamed to LockedCelo + hashContractName != hashEpochManager + ) { + // Get proxy address registered in the Registry + address proxyAddress = registry.getAddressForStringOrDie(contractName); + proxy = IProxy(address(uint160(proxyAddress))); + + // Get implementation address + address implementationAddress = proxy._getImplementation(); + + // Get bytecode from deployed contract + bytes memory actualBytecodeWithMetadataOnDevchain = implementationAddress.code; + bytes memory actualBytecodeOnDevchain = removeMetadataFromBytecode( + actualBytecodeWithMetadataOnDevchain + ); + + string memory contractFileName = string(abi.encodePacked(contractName, ".sol")); + // Get bytecode from build artifacts + bytes memory expectedBytecodeWithMetadataFromArtifacts = vm.getDeployedCode( + contractFileName + ); + bytes memory expectedBytecodeFromArtifacts = removeMetadataFromBytecode( + expectedBytecodeWithMetadataFromArtifacts + ); + + // Compare the bytecodes + assertEq( + actualBytecodeOnDevchain, + expectedBytecodeFromArtifacts, + "Bytecode does not match" + ); + } + } + } +} + +contract EpochManagerIntegrationTest is IntegrationTest, MigrationsConstants { + ICeloToken celoToken; + IAccounts accountsContract; + IValidators validatorsContract; + IEpochManager epochManager; + IEpochManagerEnabler epochManagerEnabler; + IScoreManager scoreManager; + IElection election; + ICeloUnreleasedTreasury celoUnreleasedTreasury; + + address reserveAddress; + address unreleasedTreasury; + address randomAddress; + + uint256 firstEpochNumber = 100; + uint256 firstEpochBlock = 100; + address[] firstElected; + address[] validatorsList; + address[] groupList; + + uint256[] groupScore = [5e23, 7e23, 1e24]; + uint256[] validatorScore = [1e23, 1e23, 1e23, 1e23, 1e23, 1e23]; + + function setUp() public { + randomAddress = actor("randomAddress"); + + validatorsContract = IValidators(registry.getAddressForStringOrDie("Validators")); + + election = IElection(registry.getAddressForStringOrDie("Election")); + scoreManager = IScoreManager(registry.getAddressForStringOrDie("ScoreManager")); + unreleasedTreasury = registry.getAddressForStringOrDie("CeloUnreleasedTreasury"); + reserveAddress = registry.getAddressForStringOrDie("Reserve"); + + validatorsList = validatorsContract.getRegisteredValidators(); + groupList = validatorsContract.getRegisteredValidatorGroups(); + + // mint to the reserve + celoToken = ICeloToken(registry.getAddressForStringOrDie("GoldToken")); + + vm.deal(address(0), CELO_SUPPLY_CAP); + vm.prank(address(0)); + celoToken.mint(reserveAddress, RESERVE_BALANCE); + + vm.prank(address(0)); + celoToken.mint(randomAddress, L1_MINTED_CELO_SUPPLY - RESERVE_BALANCE); // mint outstanding l1 supply before L2. + + epochManager = IEpochManager(registry.getAddressForStringOrDie("EpochManager")); + epochManagerEnabler = IEpochManagerEnabler( + registry.getAddressForStringOrDie("EpochManagerEnabler") + ); + } + + function activateValidators() public { + address[] memory registeredValidators = validatorsContract.getRegisteredValidators(); + travelEpochL1(vm); + travelEpochL1(vm); + travelEpochL1(vm); + travelEpochL1(vm); + for (uint256 i = 0; i < registeredValidators.length; i++) { + (, , address validatorGroup, , ) = validatorsContract.getValidator(registeredValidators[i]); + if (election.getPendingVotesForGroup(validatorGroup) == 0) { + continue; + } + vm.startPrank(validatorGroup); + election.activate(validatorGroup); + vm.stopPrank(); + } + } + + function test_Reverts_whenSystemNotInitialized() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.startNextEpochProcess(); + } + + function test_Reverts_WhenEndOfEpochHasNotBeenReached() public { + // fund treasury + vm.prank(address(0)); + celoToken.mint(unreleasedTreasury, L2_INITIAL_STASH_BALANCE); + + uint256 l1EpochNumber = IPrecompiles(address(validatorsContract)).getEpochNumber(); + + vm.prank(address(epochManagerEnabler)); + epochManager.initializeSystem(l1EpochNumber, block.number, validatorsList); + + vm.expectRevert("Epoch is not ready to start"); + epochManager.startNextEpochProcess(); + } + + function test_Reverts_whenAlreadyInitialized() public { + _MockL2Migration(validatorsList); + + vm.prank(address(epochManagerEnabler)); + vm.expectRevert("Epoch system already initialized"); + epochManager.initializeSystem(100, block.number, firstElected); + } + + function test_Reverts_whenTransferingCeloToUnreleasedTreasury() public { + _MockL2Migration(validatorsList); + + blockTravel(vm, 43200); + timeTravel(vm, DAY); + + vm.prank(randomAddress); + + (bool success, ) = address(unreleasedTreasury).call{ value: 50000 ether }(""); + assertFalse(success); + } + + function test_SetsCurrentRewardBlock() public { + _MockL2Migration(validatorsList); + + blockTravel(vm, L2_BLOCK_IN_EPOCH); + timeTravel(vm, DAY); + + epochManager.startNextEpochProcess(); + + (, , , uint256 _currentRewardsBlock) = epochManager.getCurrentEpoch(); + (uint256 status, , , , ) = epochManager.getEpochProcessingState(); + assertEq(_currentRewardsBlock, block.number); + assertEq(status, 1); + } + + function _MockL2Migration(address[] memory _validatorsList) internal { + for (uint256 i = 0; i < _validatorsList.length; i++) { + firstElected.push(_validatorsList[i]); + } + + uint256 l1EpochNumber = IPrecompiles(address(validatorsContract)).getEpochNumber(); + + activateValidators(); + vm.deal(unreleasedTreasury, L2_INITIAL_STASH_BALANCE); + + vm.prank(address(0)); + celoToken.mint(unreleasedTreasury, L2_INITIAL_STASH_BALANCE); + + whenL2(vm); + _setValidatorL2Score(); + + vm.prank(address(epochManagerEnabler)); + + epochManager.initializeSystem(l1EpochNumber, block.number, firstElected); + } + + function _setValidatorL2Score() internal { + address scoreManagerOwner = scoreManager.owner(); + vm.startPrank(scoreManagerOwner); + scoreManager.setGroupScore(groupList[0], groupScore[0]); + scoreManager.setGroupScore(groupList[1], groupScore[1]); + scoreManager.setGroupScore(groupList[2], groupScore[2]); + + scoreManager.setValidatorScore(validatorsList[0], validatorScore[0]); + scoreManager.setValidatorScore(validatorsList[1], validatorScore[1]); + scoreManager.setValidatorScore(validatorsList[2], validatorScore[2]); + scoreManager.setValidatorScore(validatorsList[3], validatorScore[3]); + scoreManager.setValidatorScore(validatorsList[4], validatorScore[4]); + scoreManager.setValidatorScore(validatorsList[5], validatorScore[5]); + + vm.stopPrank(); + } +} diff --git a/packages/protocol/test-sol/governance/network/GovernanceSlasher.t.sol b/packages/protocol/test-sol/governance/network/GovernanceSlasher.t.sol deleted file mode 100644 index a0c5d105592..00000000000 --- a/packages/protocol/test-sol/governance/network/GovernanceSlasher.t.sol +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.5.13; - -import "celo-foundry/Test.sol"; - -import "@celo-contracts/common/Accounts.sol"; -import "@celo-contracts/common/FixidityLib.sol"; -import "@celo-contracts/common/interfaces/IRegistry.sol"; -import "@celo-contracts/governance/Proposals.sol"; -import "@celo-contracts/governance/test/MockLockedGold.sol"; - -import "@celo-contracts/governance/GovernanceSlasher.sol"; - -contract GovernanceSlasherTest is Test { - event SlashingApproved(address indexed account, uint256 amount); - event GovernanceSlashPerformed(address indexed account, uint256 amount); - - IRegistry registry; - Accounts accounts; - MockLockedGold mockLockedGold; - - GovernanceSlasher public governanceSlasher; - address owner; - address nonOwner; - address validator; - address slashedAddress; - address registryAddress = 0x000000000000000000000000000000000000ce10; - - function setUp() public { - owner = address(this); - nonOwner = actor("nonOwner"); - validator = actor("validator"); - slashedAddress = actor("slashedAddress"); - - accounts = new Accounts(true); - mockLockedGold = new MockLockedGold(); - governanceSlasher = new GovernanceSlasher(true); - - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); - registry = IRegistry(registryAddress); - registry.setAddressFor("Accounts", address(accounts)); - registry.setAddressFor("LockedGold", address(mockLockedGold)); - - governanceSlasher.initialize(registryAddress); - mockLockedGold.setAccountTotalLockedGold(validator, 5000); - } -} - -contract GovernanceSlasherTest_initialize is GovernanceSlasherTest { - function test_shouldHaveSetOwner() public { - assertEq(governanceSlasher.owner(), owner); - } - - function test_CanOnlyBeCalledOnce() public { - vm.expectRevert("contract already initialized"); - governanceSlasher.initialize(registryAddress); - } -} - -contract GovernanceSlasherTest_approveSlashingTest is GovernanceSlasherTest { - function test_ShouldSetSlashableAmount() public { - governanceSlasher.approveSlashing(slashedAddress, 1000); - assertEq(governanceSlasher.getApprovedSlashing(slashedAddress), 1000); - } - - function test_ShouldIncrementSlashableAmountWhenApprovedTwice() public { - governanceSlasher.approveSlashing(slashedAddress, 1000); - governanceSlasher.approveSlashing(slashedAddress, 1000); - assertEq(governanceSlasher.getApprovedSlashing(slashedAddress), 2000); - } - - function test_CanOnlyBeCalledByOnwer() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(nonOwner); - governanceSlasher.approveSlashing(slashedAddress, 1000); - } - - function test_EmitsSlashingApprovedEvent() public { - vm.expectEmit(true, true, true, true); - emit SlashingApproved(slashedAddress, 1000); - governanceSlasher.approveSlashing(slashedAddress, 1000); - } -} - -contract GovernanceSlasherTest_slasherSlashTest is GovernanceSlasherTest { - address[] lessers = new address[](0); - address[] greaters = new address[](0); - uint256[] indices = new uint256[](0); - - function test_ShouldFailIfThereIsNothingToSlash() public { - vm.expectRevert("No penalty given by governance"); - governanceSlasher.slash(validator, lessers, greaters, indices); - } - - function test_ShouldDecrementCelo() public { - governanceSlasher.approveSlashing(validator, 1000); - governanceSlasher.slash(validator, lessers, greaters, indices); - assertEq(mockLockedGold.accountTotalLockedGold(validator), 4000); - } - - function test_ShouldHaveSetTheApprovedSlashingToZero() public { - governanceSlasher.approveSlashing(validator, 1000); - governanceSlasher.slash(validator, lessers, greaters, indices); - assertEq(governanceSlasher.getApprovedSlashing(validator), 0); - } - - function test_EmitsGovernanceSlashPerformedEvent() public { - governanceSlasher.approveSlashing(validator, 1000); - vm.expectEmit(true, true, true, true); - emit GovernanceSlashPerformed(validator, 1000); - governanceSlasher.slash(validator, lessers, greaters, indices); - } -} diff --git a/packages/protocol/test-sol/governance/validators/mocks/ValidatorsMockTunnel.sol b/packages/protocol/test-sol/governance/validators/mocks/ValidatorsMockTunnel.sol deleted file mode 100644 index d22b40f300d..00000000000 --- a/packages/protocol/test-sol/governance/validators/mocks/ValidatorsMockTunnel.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; - -import "@celo-contracts/governance/test/ValidatorsMock.sol"; -import { Test as ForgeTest } from "forge-std/Test.sol"; - -contract ValidatorsMockTunnel is ForgeTest { - ValidatorsMock private tunnelValidators; - address validatorContractAddress; - - constructor(address _validatorContractAddress) public { - validatorContractAddress = _validatorContractAddress; - tunnelValidators = ValidatorsMock(validatorContractAddress); - } - - struct InitParams { - address registryAddress; - uint256 groupRequirementValue; - uint256 groupRequirementDuration; - uint256 validatorRequirementValue; - uint256 validatorRequirementDuration; - uint256 validatorScoreExponent; - uint256 validatorScoreAdjustmentSpeed; - } - - struct InitParams2 { - uint256 _membershipHistoryLength; - uint256 _slashingMultiplierResetPeriod; - uint256 _maxGroupSize; - uint256 _commissionUpdateDelay; - uint256 _downtimeGracePeriod; - } - - function MockInitialize( - address sender, - InitParams calldata params, - InitParams2 calldata params2 - ) external returns (bool, bytes memory) { - bytes memory data = abi.encodeWithSignature( - "initialize(address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)", - params.registryAddress, - params.groupRequirementValue, - params.groupRequirementDuration, - params.validatorRequirementValue, - params.validatorRequirementDuration, - params.validatorScoreExponent, - params.validatorScoreAdjustmentSpeed, - params2._membershipHistoryLength, - params2._slashingMultiplierResetPeriod, - params2._maxGroupSize, - params2._commissionUpdateDelay, - params2._downtimeGracePeriod - ); - vm.prank(sender); - (bool success, ) = address(tunnelValidators).call(data); - require(success, "unsuccessful tunnel call"); - } -} diff --git a/packages/protocol/test-sol/integration/CompileValidatorMock.t.sol b/packages/protocol/test-sol/integration/CompileValidatorMock.t.sol new file mode 100644 index 00000000000..7fbc16ca0c1 --- /dev/null +++ b/packages/protocol/test-sol/integration/CompileValidatorMock.t.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import "forge-std/console.sol"; + +// here only to forge compile of ValidatorsMock +import "@test-sol/unit/governance/validators/mocks/ValidatorsMock.sol"; + +contract CompileValidatorMock is Test { + function test_nop() public view { + console.log("nop"); + } +} diff --git a/packages/protocol/test-sol/integration/Integration.t.sol b/packages/protocol/test-sol/integration/Integration.t.sol deleted file mode 100644 index e601de7ff46..00000000000 --- a/packages/protocol/test-sol/integration/Integration.t.sol +++ /dev/null @@ -1,120 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.7 <0.8.20; - -import { Test } from "forge-std-8/Test.sol"; -import "forge-std-8/console2.sol"; - -import { Constants } from "@celo-migrations/constants.sol"; - -import "@celo-contracts/common/interfaces/IRegistry.sol"; -import "@celo-contracts/common/interfaces/IProxy.sol"; - -contract IntegrationTest is Test { - address constant registryAddress = address(0x000000000000000000000000000000000000ce10); - IRegistry registry = IRegistry(registryAddress); - - function setUp() public {} - - /** - * @notice Removes CBOR encoded metadata from the tail of the deployedBytecode. - * @param data Bytecode including the CBOR encoded tail. - * @return Bytecode without the CBOR encoded metadata. - */ - function removeMetadataFromBytecode(bytes memory data) public pure returns (bytes memory) { - // Ensure the data length is at least enough to contain the length specifier - require(data.length >= 2, "Data too short to contain a valid CBOR length specifier"); - - // Calculate the length of the CBOR encoded section from the last two bytes - uint16 cborLength = uint16(uint8(data[data.length - 2])) * - 256 + - uint16(uint8(data[data.length - 1])); - - // Ensure the length is valid (not greater than the data array length minus 2 bytes for the length field) - require(cborLength <= data.length - 2, "Invalid CBOR length"); - - // Calculate the new length of the data without the CBOR section - uint newLength = data.length - 2 - cborLength; - - // Create a new byte array for the result - bytes memory result = new bytes(newLength); - - // Copy data from the original byte array to the new one, excluding the CBOR section and its length field - for (uint i = 0; i < newLength; i++) { - result[i] = data[i]; - } - - return result; - } -} - -contract RegistryIntegrationTest is IntegrationTest, Constants { - IProxy proxy; - - function test_shouldHaveAddressInRegistry() public view { - for (uint256 i = 0; i < contractsInRegistry.length; i++) { - string memory contractName = contractsInRegistry[i]; - address contractAddress = registry.getAddressFor(keccak256(abi.encodePacked(contractName))); - console2.log(contractName, "address in Registry is: ", contractAddress); - assert(contractAddress != address(0)); - } - } - - function test_shouldHaveCorrectBytecode() public { - // Converting contract names to hashes for comparison - bytes32 hashAccount = keccak256(abi.encodePacked("Accounts")); - bytes32 hashElection = keccak256(abi.encodePacked("Election")); - bytes32 hashEscrow = keccak256(abi.encodePacked("Escrow")); - bytes32 hashFederatedAttestations = keccak256(abi.encodePacked("FederatedAttestations")); - bytes32 hashGovernance = keccak256(abi.encodePacked("Governance")); - bytes32 hashSortedOracles = keccak256(abi.encodePacked("SortedOracles")); - bytes32 hashValidators = keccak256(abi.encodePacked("Validators")); - - for (uint256 i = 0; i < contractsInRegistry.length; i++) { - // Read name from list of core contracts - string memory contractName = contractsInRegistry[i]; - console2.log("Checking bytecode of:", contractName); - - // Skipping test for contracts that depend on linked libraries - // This is a known limitation in Foundry at the moment: - // Source: https://github.com/foundry-rs/foundry/issues/6120 - bytes32 hashContractName = keccak256(abi.encodePacked(contractName)); - if ( - hashContractName != hashAccount && - hashContractName != hashElection && - hashContractName != hashEscrow && - hashContractName != hashFederatedAttestations && - hashContractName != hashGovernance && - hashContractName != hashSortedOracles && - hashContractName != hashValidators - ) { - // Get proxy address registered in the Registry - address proxyAddress = registry.getAddressForStringOrDie(contractName); - proxy = IProxy(address(uint160(proxyAddress))); - - // Get implementation address - address implementationAddress = proxy._getImplementation(); - - // Get bytecode from deployed contract - bytes memory actualBytecodeWithMetadataOnDevchain = implementationAddress.code; - bytes memory actualBytecodeOnDevchain = removeMetadataFromBytecode( - actualBytecodeWithMetadataOnDevchain - ); - - // Get bytecode from build artifacts - bytes memory expectedBytecodeWithMetadataFromArtifacts = vm.getDeployedCode( - string(abi.encodePacked(contractName, ".sol")) - ); - bytes memory expectedBytecodeFromArtifacts = removeMetadataFromBytecode( - expectedBytecodeWithMetadataFromArtifacts - ); - - // Compare the bytecodes - assertEq( - actualBytecodeOnDevchain, - expectedBytecodeFromArtifacts, - "Bytecode does not match" - ); - } - } - } -} diff --git a/packages/protocol/test-sol/integration/RevokeCeloAfterL2Transition.sol b/packages/protocol/test-sol/integration/RevokeCeloAfterL2Transition.sol index aac1d94d517..d16c6001b30 100644 --- a/packages/protocol/test-sol/integration/RevokeCeloAfterL2Transition.sol +++ b/packages/protocol/test-sol/integration/RevokeCeloAfterL2Transition.sol @@ -2,13 +2,13 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; -import "celo-foundry/Test.sol"; - import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "@celo-contracts/common/FixidityLib.sol"; import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Accounts.sol"; import "@celo-contracts/common/GoldToken.sol"; +import "@celo-contracts-8/common/interfaces/IPrecompiles.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; import "@celo-contracts/governance/Election.sol"; import "@celo-contracts/governance/LockedGold.sol"; @@ -18,26 +18,22 @@ import "@celo-contracts/stability/test/MockStableToken.sol"; import "@celo-contracts/governance/Election.sol"; import "@celo-contracts/governance/Governance.sol"; -import "@celo-contracts/governance/test/ValidatorsMock.sol"; -import "@test-sol/constants.sol"; import "@test-sol/utils/ECDSAHelper.sol"; -import { Utils } from "@test-sol/utils.sol"; -import { Test as ForgeTest } from "forge-std/Test.sol"; -import "../governance/validators/mocks/ValidatorsMockTunnel.sol"; -import "../governance/voting/mocks/ReleaseGoldMockTunnel.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; +import "@test-sol/unit/governance/validators/mocks/ValidatorsMockTunnel.sol"; +import "@test-sol/unit/governance/voting/mocks/ReleaseGoldMockTunnel.sol"; +import "@test-sol/unit/common/mocks/MockEpochManager.sol"; -contract RevokeCeloAfterL2Transition is Test, Constants, ECDSAHelper, Utils { +contract RevokeCeloAfterL2Transition is TestWithUtils, ECDSAHelper { using FixidityLib for FixidityLib.Fraction; - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; uint256 constant TOTAL_AMOUNT = 1 ether * 1_000_000; - Registry registry; Accounts accounts; MockStableToken stableToken; Election election; ValidatorsMockTunnel public validatorsMockTunnel; - Validators public validators; + IValidators public validators; LockedGold lockedGold; Governance governance; GoldToken goldToken; @@ -168,15 +164,17 @@ contract RevokeCeloAfterL2Transition is Test, Constants, ECDSAHelper, Utils { ) .unwrap(); - address registryAddress = 0x000000000000000000000000000000000000ce10; - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); - registry = Registry(registryAddress); + setupRegistry(); + setupEpochManager(); accounts = new Accounts(true); stableToken = new MockStableToken(); election = new Election(true); lockedGold = new LockedGold(true); - validators = new Validators(true); + address validatorsAddress = actor("Validators"); + deployCodeTo("ValidatorsMock.sol", validatorsAddress); + validators = IValidators(validatorsAddress); + // TODO move to create2 validatorsMockTunnel = new ValidatorsMockTunnel(address(validators)); governance = new Governance(true); goldToken = new GoldToken(true); @@ -189,10 +187,11 @@ contract RevokeCeloAfterL2Transition is Test, Constants, ECDSAHelper, Utils { registry.setAddressFor(ValidatorsContract, address(validators)); registry.setAddressFor(GovernanceContract, address(governance)); registry.setAddressFor(GoldTokenContract, address(goldToken)); + registry.setAddressFor(EpochManagerContract, address(epochManager)); goldToken.initialize(address(registry)); - accounts.initialize(registryAddress); + accounts.initialize(REGISTRY_ADDRESS); releaseGold = new ReleaseGold(true); @@ -213,14 +212,14 @@ contract RevokeCeloAfterL2Transition is Test, Constants, ECDSAHelper, Utils { initialDistributionRatio: 1000, _canValidate: true, _canVote: true, - registryAddress: registryAddress + registryAddress: REGISTRY_ADDRESS }); ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold)); tunnel.MockInitialize(owner, releaseGoldInitParams, releaseGoldInitParams2); election.initialize( - registryAddress, + REGISTRY_ADDRESS, electableValidatorsMin, electableValidatorsMax, maxNumGroupsVotedFor, @@ -245,7 +244,7 @@ contract RevokeCeloAfterL2Transition is Test, Constants, ECDSAHelper, Utils { }); initParams = ValidatorsMockTunnel.InitParams({ - registryAddress: registryAddress, + registryAddress: REGISTRY_ADDRESS, groupRequirementValue: originalGroupLockedGoldRequirements.value, groupRequirementDuration: originalGroupLockedGoldRequirements.duration, validatorRequirementValue: originalValidatorLockedGoldRequirements.value, @@ -284,10 +283,17 @@ contract RevokeCeloAfterL2Transition is Test, Constants, ECDSAHelper, Utils { } function _whenL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); + uint256 l1EpochNumber = IPrecompiles(address(validators)).getEpochNumber(); + + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + + address[] memory _elected = new address[](2); + _elected[0] = actor("firstElected"); + _elected[1] = actor("secondElected"); + epochManager.initializeSystem(l1EpochNumber, block.number, _elected); } - function _registerValidatorGroupHelper(address _group, uint256 numMembers) internal { + function _registerValidatorGroupHelper(address _group, uint256) internal { vm.startPrank(_group); if (!accounts.isAccount(_group)) { accounts.createAccount(); @@ -345,7 +351,7 @@ contract RevokeCeloAfterL2Transition is Test, Constants, ECDSAHelper, Utils { vm.prank(_validator); validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); - validatorRegistrationEpochNumber = validators.getEpochNumber(); + validatorRegistrationEpochNumber = IPrecompiles(address(validators)).getEpochNumber(); return _ecdsaPubKey; } @@ -470,16 +476,13 @@ contract RevokeCeloAfterL2TransitionTest is RevokeCeloAfterL2Transition { address _validator, uint256 signerPk ) internal returns (bytes memory) { - (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( - _validator, - signerPk - ); + (bytes memory _ecdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner(_validator, signerPk); ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(_validator, blsPublicKey, blsPop)); vm.prank(_validator); validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); - validatorRegistrationEpochNumber = validators.getEpochNumber(); + validatorRegistrationEpochNumber = IPrecompiles(address(validators)).getEpochNumber(); return _ecdsaPubKey; } diff --git a/packages/protocol/test-sol/mocks/EpochManagerEnablerMock.sol b/packages/protocol/test-sol/mocks/EpochManagerEnablerMock.sol new file mode 100644 index 00000000000..137d9104522 --- /dev/null +++ b/packages/protocol/test-sol/mocks/EpochManagerEnablerMock.sol @@ -0,0 +1,33 @@ +pragma solidity ^0.8.0; + +import "../../contracts-0.8/common/EpochManagerEnabler.sol"; + +/** + * @title A wrapper around EpochManagerEnabler that exposes internal functions for testing. + */ +contract EpochManagerEnablerMock is EpochManagerEnabler(true) { + address[] validatorSet; + + function setFirstBlockOfEpoch() external { + return _setFirstBlockOfEpoch(); + } + + function addValidator(address validator) external { + validatorSet.push(validator); + } + + // Minimally override core functions from UsingPrecompiles + function numberValidatorsInCurrentSet() public view override returns (uint256) { + return validatorSet.length; + } + + function numberValidatorsInSet(uint256) public view override returns (uint256) { + return validatorSet.length; + } + + function validatorSignerAddressFromCurrentSet( + uint256 index + ) public view override returns (address) { + return validatorSet[index]; + } +} diff --git a/packages/protocol/test-sol/precompiles/EpochSizePrecompile.sol b/packages/protocol/test-sol/precompiles/EpochSizePrecompile.sol index f51959049d5..cdab89b4674 100644 --- a/packages/protocol/test-sol/precompiles/EpochSizePrecompile.sol +++ b/packages/protocol/test-sol/precompiles/EpochSizePrecompile.sol @@ -1,17 +1,27 @@ -// TODO move this to test folder pragma solidity >=0.8.7 <0.8.20; +address constant EPOCH_SIZEPRE_COMPILE_ADDRESS = address(0xff - 7); contract EpochSizePrecompile { - address constant ADDRESS = address(0xff - 7); + address constant ADDRESS = EPOCH_SIZEPRE_COMPILE_ADDRESS; uint256 public constant EPOCH_SIZE = 100; + uint256 public epochSizeSet; receive() external payable {} fallback(bytes calldata) external payable returns (bytes memory) { + // this is required because when the migrations deploy the precompiles + // they don't get constructed + if (epochSizeSet != 0) { + return abi.encodePacked(epochSizeSet); + } return abi.encodePacked(EPOCH_SIZE); } + function setEpochSize(uint256 epochSize) public { + epochSizeSet = epochSize; + } + function getAddress() public pure returns (address) { return ADDRESS; } diff --git a/packages/protocol/test-sol/precompiles/NumberValidatorsInCurrentSetPrecompile.sol b/packages/protocol/test-sol/precompiles/NumberValidatorsInCurrentSetPrecompile.sol new file mode 100644 index 00000000000..44e651a073f --- /dev/null +++ b/packages/protocol/test-sol/precompiles/NumberValidatorsInCurrentSetPrecompile.sol @@ -0,0 +1,32 @@ +// TODO move this to test folder +pragma solidity >=0.8.7 <0.8.20; + +import "forge-std/console.sol"; +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; + +contract NumberValidatorsInCurrentSetPrecompile { + address constant ADDRESS = address(0xff - 6); + + uint256 public NumberOfValidators = 1; + + address internal constant registryAddress = 0x000000000000000000000000000000000000ce10; + + receive() external payable {} + + fallback(bytes calldata) external payable returns (bytes memory) { + return abi.encodePacked(NumberOfValidators); + } + + function setNumberOfValidators() external { + IRegistry registry = IRegistry(registryAddress); + address validatorsAddress = registry.getAddressForString("Validators"); + IValidators validatorsContract = IValidators(validatorsAddress); + address[] memory registeredValidators = validatorsContract.getRegisteredValidators(); + NumberOfValidators = registeredValidators.length; + } + + function getAddress() public pure returns (address) { + return ADDRESS; + } +} diff --git a/packages/protocol/test-sol/precompiles/ValidatorSignerAddressFromCurrentSetPrecompile.sol b/packages/protocol/test-sol/precompiles/ValidatorSignerAddressFromCurrentSetPrecompile.sol new file mode 100644 index 00000000000..f2ed9c7ade8 --- /dev/null +++ b/packages/protocol/test-sol/precompiles/ValidatorSignerAddressFromCurrentSetPrecompile.sol @@ -0,0 +1,56 @@ +// TODO move this to test folder +pragma solidity >=0.8.7 <0.8.20; + +import "forge-std/console.sol"; +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; + +contract ValidatorSignerAddressFromCurrentSetPrecompile { + address constant ADDRESS = address(0xff - 5); + + uint256 public constant EPOCH_SIZE = 100; + + address[] validators; + + address internal constant registryAddress = 0x000000000000000000000000000000000000ce10; + + receive() external payable {} + + fallback(bytes calldata input) external payable returns (bytes memory) { + uint256 index = getUint256FromBytes(input, 0); + return abi.encodePacked(uint256(uint160(validators[index]))); + } + + function getAddress() public pure returns (address) { + return ADDRESS; + } + + function getUint256FromBytes(bytes memory bs, uint256 start) internal pure returns (uint256) { + return uint256(getBytes32FromBytes(bs, start)); + } + + function setValidators() external { + IRegistry registry = IRegistry(registryAddress); + address validatorsAddress = registry.getAddressForString("Validators"); + IValidators validatorsContract = IValidators(validatorsAddress); + address[] memory registeredValidators = validatorsContract.getRegisteredValidators(); + for (uint256 i = 0; i < registeredValidators.length; i++) { + validators.push(registeredValidators[i]); + } + } + + /** + * @notice Converts bytes to bytes32. + * @param bs byte[] data + * @param start offset into byte data to convert + * @return bytes32 data + */ + function getBytes32FromBytes(bytes memory bs, uint256 start) internal pure returns (bytes32) { + require(bs.length >= start + 32, "slicing out of range"); + bytes32 x; + assembly { + x := mload(add(bs, add(start, 32))) + } + return x; + } +} diff --git a/packages/protocol/test-sol/common/Accounts.t.sol b/packages/protocol/test-sol/unit/common/Accounts.t.sol similarity index 97% rename from packages/protocol/test-sol/common/Accounts.t.sol rename to packages/protocol/test-sol/unit/common/Accounts.t.sol index 717bf9d9bc8..d28bc244889 100644 --- a/packages/protocol/test-sol/common/Accounts.t.sol +++ b/packages/protocol/test-sol/unit/common/Accounts.t.sol @@ -2,21 +2,19 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; -import "celo-foundry/Test.sol"; import "@celo-contracts/common/FixidityLib.sol"; -import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Accounts.sol"; import "@celo-contracts/governance/test/MockValidators.sol"; -contract AccountsTest is Test { +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; +import "@test-sol/utils/WhenL2.sol"; + +contract AccountsTest is TestWithUtils { using FixidityLib for FixidityLib.Fraction; - Registry registry; Accounts accounts; MockValidators validators; - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; - string constant name = "Account"; string constant metadataURL = "https://www.celo.org"; string constant otherMetadataURL = "https://clabs.co"; @@ -80,15 +78,11 @@ contract AccountsTest is Test { event PaymentDelegationSet(address indexed beneficiary, uint256 fraction); function setUp() public { - address registryAddress = 0x000000000000000000000000000000000000ce10; - - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); + super.setUp(); accounts = new Accounts(true); validators = new MockValidators(); - registry = Registry(registryAddress); - registry.setAddressFor("Validators", address(validators)); registry.setAddressFor("Accounts", address(accounts)); @@ -99,10 +93,6 @@ contract AccountsTest is Test { (caller2, caller2PK) = actorWithPK("caller2"); } - function _whenL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); - } - function getParsedSignatureOfAddress( address _address, uint256 privateKey @@ -203,6 +193,8 @@ contract AccountsTest is Test { } } +contract AccountsTest_L2 is AccountsTest, WhenL2 {} + contract AccountsTest_createAccount is AccountsTest { function setUp() public { super.setUp(); @@ -221,6 +213,8 @@ contract AccountsTest_createAccount is AccountsTest { } } +contract AccountsTest_createAccount_L2 is AccountsTest_L2, AccountsTest_createAccount {} + contract AccountsTest_setAccountDataEncryptionKey is AccountsTest { function setUp() public { super.setUp(); @@ -256,6 +250,11 @@ contract AccountsTest_setAccountDataEncryptionKey is AccountsTest { } } +contract AccountsTest_setAccountDataEncryptionKey_L2 is + AccountsTest_L2, + AccountsTest_setAccountDataEncryptionKey +{} + contract AccountsTest_setAccount is AccountsTest { function setUp() public { super.setUp(); @@ -342,6 +341,8 @@ contract AccountsTest_setAccount is AccountsTest { } } +contract AccountsTest_setAccount_L2 is AccountsTest_L2, AccountsTest_setAccount {} + contract AccountsTest_setWalletAddress is AccountsTest { function setUp() public { super.setUp(); @@ -392,6 +393,8 @@ contract AccountsTest_setWalletAddress is AccountsTest { } } +contract AccountsTest_setWalletAddress_L2 is AccountsTest_L2, AccountsTest_setWalletAddress {} + contract AccountsTest_setMetadataURL is AccountsTest { function setUp() public { super.setUp(); @@ -416,6 +419,8 @@ contract AccountsTest_setMetadataURL is AccountsTest { } } +contract AccountsTest_setMetadataURL_L2 is AccountsTest_L2, AccountsTest_setMetadataURL {} + contract AccountsTest_batchGetMetadataURL is AccountsTest { function setUp() public { super.setUp(); @@ -466,6 +471,8 @@ contract AccountsTest_batchGetMetadataURL is AccountsTest { } } +contract AccountsTest_batchGetMetadataURL_L2 is AccountsTest_L2, AccountsTest_batchGetMetadataURL {} + contract AccountsTest_addStorageRoot is AccountsTest { function setUp() public { super.setUp(); @@ -512,6 +519,8 @@ contract AccountsTest_addStorageRoot is AccountsTest { } } +contract AccountsTest_addStorageRoot_L2 is AccountsTest_L2, AccountsTest_addStorageRoot {} + contract AccountsTest_removeStorageRoot is AccountsTest { function setUp() public { super.setUp(); @@ -588,6 +597,8 @@ contract AccountsTest_removeStorageRoot is AccountsTest { } } +contract AccountsTest_removeStorageRoot_L2 is AccountsTest_L2, AccountsTest_removeStorageRoot {} + contract AccountsTest_setPaymentDelegation is AccountsTest { address beneficiary = actor("beneficiary"); uint256 fraction = FixidityLib.newFixedFraction(2, 10).unwrap(); @@ -610,13 +621,6 @@ contract AccountsTest_setPaymentDelegation is AccountsTest { assertEq(realFraction, fraction); } - function test_Revert_SetPaymentDelegation_WhenL2() public { - _whenL2(); - accounts.createAccount(); - vm.expectRevert("This method is no longer supported in L2."); - accounts.setPaymentDelegation(beneficiary, fraction); - } - function test_ShouldNotAllowFractionGreaterThan1() public { accounts.createAccount(); vm.expectRevert("Fraction must not be greater than 1"); @@ -637,6 +641,11 @@ contract AccountsTest_setPaymentDelegation is AccountsTest { } } +contract AccountsTest_setPaymentDelegation_L2 is + AccountsTest_L2, + AccountsTest_setPaymentDelegation +{} + contract AccountsTest_deletePaymentDelegation is AccountsTest { address beneficiary = actor("beneficiary"); uint256 fraction = FixidityLib.newFixedFraction(2, 10).unwrap(); @@ -667,6 +676,11 @@ contract AccountsTest_deletePaymentDelegation is AccountsTest { } } +contract AccountsTest_deletePaymentDelegation_L2 is + AccountsTest_L2, + AccountsTest_deletePaymentDelegation +{} + contract AccountsTest_setName is AccountsTest { function setUp() public { super.setUp(); @@ -691,6 +705,8 @@ contract AccountsTest_setName is AccountsTest { } } +contract AccountsTest_setName_L2 is AccountsTest_L2, AccountsTest_setName {} + contract AccountsTest_GenericAuthorization is AccountsTest { address account2 = actor("account2"); address signer; @@ -875,6 +891,11 @@ contract AccountsTest_GenericAuthorization is AccountsTest { } } +contract AccountsTest_GenericAuthorization_L2 is + AccountsTest_L2, + AccountsTest_GenericAuthorization +{} + contract AccountsTest_BackwardCompatibility is AccountsTest { address account = address(this); address otherAccount = actor("otherAccount"); @@ -1542,3 +1563,8 @@ contract AccountsTest_BackwardCompatibility is AccountsTest { helper_ShouldRemoveSigner(Role.Validator, false, true); } } + +contract AccountsTest_BackwardCompatibility_L2 is + AccountsTest_L2, + AccountsTest_BackwardCompatibility +{} diff --git a/packages/protocol/test-sol/common/AddressSortedLinkedListWithMedian.t.sol b/packages/protocol/test-sol/unit/common/AddressSortedLinkedListWithMedian.t.sol similarity index 98% rename from packages/protocol/test-sol/common/AddressSortedLinkedListWithMedian.t.sol rename to packages/protocol/test-sol/unit/common/AddressSortedLinkedListWithMedian.t.sol index 60d5bd64ff7..7365492d293 100644 --- a/packages/protocol/test-sol/common/AddressSortedLinkedListWithMedian.t.sol +++ b/packages/protocol/test-sol/unit/common/AddressSortedLinkedListWithMedian.t.sol @@ -91,7 +91,7 @@ contract AddressSortedLinkedListWithMedianTest_update is AddressSortedLinkedList assertEq(numerators[0], newNumerator, "should have the correct numerator"); } - function test_ShouldRevertIfTheKEyIsNotInTheList() public { + function test_ShouldRevertIfTheKeyIsNotInTheList() public { vm.expectRevert("key not in list"); sortedList.update(key2, newNumerator, address(0), address(0)); } @@ -117,7 +117,7 @@ contract AddressSortedLinkedListWithMedianTest_remove is AddressSortedLinkedList sortedList.insert(key, numerator, address(0), address(0)); } - function test_ShouldRemoveTheELementFromTheList() public { + function test_ShouldRemoveTheElementFromTheList() public { sortedList.remove(key); assertEq(sortedList.contains(key), false, "should not contain the key"); } diff --git a/packages/protocol/test-sol/unit/common/Blockable.t.sol b/packages/protocol/test-sol/unit/common/Blockable.t.sol new file mode 100644 index 00000000000..8eb0c78a124 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/Blockable.t.sol @@ -0,0 +1,98 @@ +pragma solidity ^0.5.13; + +import "celo-foundry/Test.sol"; + +import "@celo-contracts/common/Blockable.sol"; +import "@celo-contracts/common/interfaces/IBlockable.sol"; +import "@celo-contracts/common/interfaces/IBlocker.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; + +contract TestBlocker is IBlocker { + bool public blocked; + + function mockSetBlocked(bool _blocked) public { + blocked = _blocked; + } + + function isBlocked() external view returns (bool) { + return blocked; + } +} + +contract BlockableMock is Blockable, Ownable { + function setBlockedByContract(address _blockedBy) public onlyOwner { + _setBlockedBy(_blockedBy); + } +} + +contract TestBlockable is BlockableMock { + function functionToBeBlocked() public view onlyWhenNotBlocked { + return; + } +} + +contract BlockableTest is Test { + IBlockable blockable; + TestBlocker blocker; + address notOwner; + + event BlockedBySet(address indexed _blockedBy); + + function setUp() public { + blockable = new BlockableMock(); + blocker = new TestBlocker(); + notOwner = actor("notOwner"); + } +} + +contract BlockableTest_setBlockable is BlockableTest { + function test_setBlockable() public { + blockable.setBlockedByContract(address(blocker)); + assert(blockable.getBlockedByContract() == address(blocker)); + } + + function test_Reverts_WhenNotCalledByOwner() public { + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + blockable.setBlockedByContract(address(blocker)); + } + + function test_Emits_BlockedBySet() public { + vm.expectEmit(false, false, false, true); + emit BlockedBySet(address(blocker)); + blockable.setBlockedByContract(address(blocker)); + } +} + +contract BlockableTest_isBlocked is BlockableTest { + function test_isFalse_WhenBlockableNotSet() public view { + assert(blockable.isBlocked() == false); + } + + function test_isBlocked() public { + assertTrue(blockable.isBlocked() == false); + blocker.mockSetBlocked(true); + blockable.setBlockedByContract(address(blocker)); + assertTrue(blockable.isBlocked()); + } +} + +contract BlockableTest_onlyWhenNotBlocked is BlockableTest { + TestBlockable blockableWithFunction; + + function setUp() public { + super.setUp(); + blockableWithFunction = new TestBlockable(); + blockableWithFunction.setBlockedByContract(address(blocker)); + } + + function test_Reverts_WhenBlocked() public { + blocker.mockSetBlocked(true); + vm.expectRevert("Contract is blocked from performing this action"); + blockableWithFunction.functionToBeBlocked(); + } + + function test_callsucceeds_WhenNotBlocked() public view { + blockableWithFunction.functionToBeBlocked(); + } +} diff --git a/packages/protocol/test-sol/common/GoldTokenMock.sol b/packages/protocol/test-sol/unit/common/CeloTokenMock.sol similarity index 95% rename from packages/protocol/test-sol/common/GoldTokenMock.sol rename to packages/protocol/test-sol/unit/common/CeloTokenMock.sol index 535c44272e7..dd0d1db7b19 100644 --- a/packages/protocol/test-sol/common/GoldTokenMock.sol +++ b/packages/protocol/test-sol/unit/common/CeloTokenMock.sol @@ -6,7 +6,7 @@ import "@celo-contracts/common/GoldToken.sol"; /** * @title A mock GoldToken for testing. */ -contract GoldTokenMock is GoldToken(true) { +contract CeloTokenMock is GoldToken(true) { uint8 public constant decimals = 18; mapping(address => uint256) balances; diff --git a/packages/protocol/test-sol/unit/common/CeloUnreleasedTreasury.t.sol b/packages/protocol/test-sol/unit/common/CeloUnreleasedTreasury.t.sol new file mode 100644 index 00000000000..7f5b6ab8179 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/CeloUnreleasedTreasury.t.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; +pragma experimental ABIEncoderV2; + +import "celo-foundry-8/Test.sol"; +import "@celo-contracts/common/FixidityLib.sol"; +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/common/interfaces/ICeloToken.sol"; +import "@celo-contracts/governance/interfaces/IGovernance.sol"; +import { CeloUnreleasedTreasury } from "@celo-contracts-8/common/CeloUnreleasedTreasury.sol"; +import "@celo-contracts-8/common/IsL2Check.sol"; +import { TestConstants } from "@test-sol/constants.sol"; + +import "@test-sol/unit/governance/mock/MockGovernance.sol"; + +contract CeloUnreleasedTreasuryTest is Test, TestConstants, IsL2Check { + using FixidityLib for FixidityLib.Fraction; + + IRegistry registry; + ICeloToken celoToken; + MockGovernance governance; + + CeloUnreleasedTreasury celoUnreleasedTreasury; + + address owner = address(this); + + address celoTokenAddress = actor("celoTokenAddress"); + address epochManagerAddress = actor("epochManagerAddress"); + + address celoDistributionOwner = actor("celoDistributionOwner"); + address communityRewardFund = actor("communityRewardFund"); + address carbonOffsettingPartner = actor("carbonOffsettingPartner"); + + address newPartner = actor("newPartner"); + address randomAddress = actor("randomAddress"); + + uint256 constant DAILY_DISTRIBUTION_AMOUNT = 6748256563599655349558; // 6,748 Celo + + uint256 constant MAX_L2_COMMUNITY_DISTRIBUTION = MAX_L2_DISTRIBUTION / 4; // 26.8 million Celo + uint256 constant MAX_L2_CARBON_FUND_DISTRIBUTION = MAX_L2_DISTRIBUTION / 1000; // 107,297 Celo + + uint256 constant L2_FIFTEEN_YEAR_CELO_SUPPLY = + L1_MINTED_CELO_SUPPLY + MAX_L2_COMMUNITY_DISTRIBUTION + MAX_L2_CARBON_FUND_DISTRIBUTION; + + uint256 constant l2StartTime = 1715808537; // Arbitary later date (May 15 2024) + uint256 constant communityRewardFraction = FIXED1 / 4; // 25% + uint256 constant carbonOffsettingFraction = FIXED1 / 1000; // 0.1% + uint256 constant newCommunityRewardFraction = FIXED1 / 2; // 50% + uint256 constant newCarbonOffsettingFraction = FIXED1 / 500; // 0.2% + + event CommunityRewardFractionSet(uint256 fraction); + event CarbonOffsettingFundSet(address indexed partner, uint256 fraction); + + function setUp() public virtual { + setUpL1(); + + // Setup L2 after minting L1 supply. + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + } + + function setUpL1() public { + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); + registry = IRegistry(REGISTRY_ADDRESS); + + deployCodeTo("GoldToken.sol", abi.encode(true), celoTokenAddress); + celoToken = ICeloToken(celoTokenAddress); + celoToken.initialize(REGISTRY_ADDRESS); + // Using a mock contract, as foundry does not allow for library linking when using deployCodeTo + governance = new MockGovernance(); + + registry.setAddressFor("CeloToken", address(celoToken)); + + registry.setAddressFor("Governance", address(governance)); + registry.setAddressFor("EpochManager", address(epochManagerAddress)); + + vm.deal(address(0), CELO_SUPPLY_CAP); + assertEq(celoToken.allocatedSupply(), 0, "starting total supply not zero."); + // Mint L1 supply + vm.prank(address(0)); + celoToken.mint(randomAddress, L1_MINTED_CELO_SUPPLY); + assertEq(celoToken.allocatedSupply(), L1_MINTED_CELO_SUPPLY, "total supply incorrect."); + } + + function newCeloUnreleasedTreasury() internal { + vm.warp(block.timestamp + l2StartTime); + vm.prank(celoDistributionOwner); + celoUnreleasedTreasury = new CeloUnreleasedTreasury(true); + registry.setAddressFor("CeloUnreleasedTreasury", address(celoUnreleasedTreasury)); + + vm.deal(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); + + vm.prank(celoDistributionOwner); + celoUnreleasedTreasury.initialize(REGISTRY_ADDRESS); + } +} + +contract CeloUnreleasedTreasuryTest_initialize is CeloUnreleasedTreasuryTest { + function setUp() public override { + super.setUp(); + vm.warp(block.timestamp + l2StartTime); + + vm.prank(celoDistributionOwner); + celoUnreleasedTreasury = new CeloUnreleasedTreasury(true); + registry.setAddressFor("CeloUnreleasedTreasury", address(celoUnreleasedTreasury)); + vm.prank(celoDistributionOwner); + celoUnreleasedTreasury.initialize(REGISTRY_ADDRESS); + } + + function test_ShouldSetAnOwnerToCeloUnreleasedTreasuryInstance() public { + assertEq(celoUnreleasedTreasury.owner(), celoDistributionOwner); + } + + function test_ShouldSetRegistryAddressToCeloUnreleasedTreasuryInstance() public { + assertEq(address(celoUnreleasedTreasury.registry()), REGISTRY_ADDRESS); + } + + function test_Reverts_WhenRegistryIsTheNullAddress() public { + celoUnreleasedTreasury = new CeloUnreleasedTreasury(true); + registry.setAddressFor("CeloUnreleasedTreasury", address(celoUnreleasedTreasury)); + vm.expectRevert("Cannot register the null address"); + celoUnreleasedTreasury.initialize(address(0)); + } + + function test_Reverts_WhenReceivingNativeTokens() public { + (bool success, ) = address(celoUnreleasedTreasury).call{ value: 1 ether }(""); + assertFalse(success); + + address payable payableAddress = payable((address(celoUnreleasedTreasury))); + + bool success2 = payableAddress.send(1 ether); + assertFalse(success2); + + vm.expectRevert(); + payableAddress.transfer(1 ether); + } +} + +contract CeloUnreleasedTreasuryTest_release is CeloUnreleasedTreasuryTest { + function setUp() public override { + super.setUp(); + newCeloUnreleasedTreasury(); + } + + function test_ShouldTransferToRecepientAddress() public { + uint256 _balanceBefore = randomAddress.balance; + vm.prank(epochManagerAddress); + + celoUnreleasedTreasury.release(randomAddress, 4); + uint256 _balanceAfter = randomAddress.balance; + assertGt(_balanceAfter, _balanceBefore); + } + + function test_Reverts_WhenCalledByOtherThanEpochManager() public { + vm.prank(randomAddress); + + vm.expectRevert("Only the EpochManager contract can call this function."); + celoUnreleasedTreasury.release(randomAddress, 4); + } +} +contract CeloUnreleasedTreasuryTest_getRemainingBalanceToRelease is CeloUnreleasedTreasuryTest { + uint256 _startingBalance; + function setUp() public override { + super.setUp(); + newCeloUnreleasedTreasury(); + _startingBalance = address(celoUnreleasedTreasury).balance; + } + + function test_ShouldReturnContractBalanceBeforeFirstRelease() public { + uint256 _remainingBalance = celoUnreleasedTreasury.getRemainingBalanceToRelease(); + + assertEq(_startingBalance, _remainingBalance); + } + + function test_ShouldReturnRemainingBalanceToReleaseAfterFirstRelease() public { + vm.prank(epochManagerAddress); + + celoUnreleasedTreasury.release(randomAddress, 4); + uint256 _remainingBalance = celoUnreleasedTreasury.getRemainingBalanceToRelease(); + assertEq(_remainingBalance, _startingBalance - 4); + } + + function test_RemainingBalanceToReleaseShouldRemainUnchangedAfterCeloTransferBackToContract() + public + { + vm.prank(epochManagerAddress); + + celoUnreleasedTreasury.release(randomAddress, 4); + uint256 _remainingBalanceBeforeTransfer = celoUnreleasedTreasury.getRemainingBalanceToRelease(); + assertEq(_remainingBalanceBeforeTransfer, _startingBalance - 4); + // set the contract balance to mock a CELO token transfer + vm.deal(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); + uint256 _remainingBalanceAfterTransfer = celoUnreleasedTreasury.getRemainingBalanceToRelease(); + assertEq(_remainingBalanceAfterTransfer, _remainingBalanceBeforeTransfer); + } +} diff --git a/packages/protocol/test-sol/unit/common/EpochManager.t.sol b/packages/protocol/test-sol/unit/common/EpochManager.t.sol new file mode 100644 index 00000000000..b1667767500 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/EpochManager.t.sol @@ -0,0 +1,1039 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import "@celo-contracts-8/common/mocks/EpochManager_WithMocks.sol"; +import "@celo-contracts-8/stability/test/MockStableToken.sol"; +import "@celo-contracts-8/common/test/MockCeloToken.sol"; +import "@celo-contracts/common/interfaces/ICeloToken.sol"; +import "@celo-contracts-8/common/ScoreManager.sol"; +import { ICeloUnreleasedTreasury } from "@celo-contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; + +import { TestConstants } from "@test-sol/constants.sol"; +import { Utils08 } from "@test-sol/utils08.sol"; + +import "@celo-contracts/stability/test/MockSortedOracles.sol"; + +import "@celo-contracts/common/interfaces/IRegistry.sol"; + +import { IMockValidators } from "@celo-contracts-8/governance/test/IMockValidators.sol"; + +import { EpochRewardsMock08 } from "@celo-contracts-8/governance/test/EpochRewardsMock.sol"; +import { MockElection } from "@celo-contracts/governance/test/MockElection.sol"; + +import { MockAccounts } from "@celo-contracts-8/common/mocks/MockAccounts.sol"; +import { ValidatorsMock } from "@test-sol/unit/governance/validators/mocks/ValidatorsMock.sol"; +import { MockCeloUnreleasedTreasury } from "@celo-contracts-8/common/test/MockCeloUnreleasedTreasury.sol"; +import { console } from "forge-std/console.sol"; + +contract EpochManagerTest is Test, TestConstants, Utils08 { + EpochManager_WithMocks epochManager; + MockSortedOracles sortedOracles; + + MockStableToken08 stableToken; + EpochRewardsMock08 epochRewards; + MockElection election; + MockAccounts accounts; + IMockValidators validators; + + address epochManagerEnabler; + address carbonOffsettingPartner; + address communityRewardFund; + address reserveAddress; + address scoreManagerAddress; + address accountsAddress; + + uint256 firstEpochNumber = 100; + uint256 firstEpochBlock = 100; + uint256 epochDuration = DAY; + address[] firstElected; + + IRegistry registry; + MockCeloToken08 celoToken; + MockCeloUnreleasedTreasury celoUnreleasedTreasury; + ScoreManager scoreManager; + + uint256 celoAmountForRate = 1e24; + uint256 stableAmountForRate = 2 * celoAmountForRate; + + uint256 validator1Reward = 42e18; + uint256 validator2Reward = 43e18; + + address validator1; + uint256 validator1PK; + address validator2; + uint256 validator2PK; + + address group = actor("group"); + + event ValidatorEpochPaymentDistributed( + address indexed validator, + uint256 validatorPayment, + address indexed group, + uint256 groupPayment, + address indexed beneficiary, + uint256 delegatedPayment + ); + event EpochProcessingStarted(uint256 indexed epochNumber); + event EpochDurationSet(uint256 indexed newEpochDuration); + event OracleAddressSet(address indexed newOracleAddress); + event GroupMarkedForProcessing(address indexed group, uint256 indexed epochNumber); + event GroupProcessed(address indexed group, uint256 indexed epochNumber); + + function setUp() public virtual { + epochManager = new EpochManager_WithMocks(); + sortedOracles = new MockSortedOracles(); + epochRewards = new EpochRewardsMock08(); + validators = IMockValidators(actor("validators05")); + stableToken = new MockStableToken08(); + celoToken = new MockCeloToken08(); + celoUnreleasedTreasury = new MockCeloUnreleasedTreasury(); + election = new MockElection(); + accounts = new MockAccounts(); + + (validator1, validator1PK) = actorWithPK(vm, "validator1"); + (validator2, validator2PK) = actorWithPK(vm, "validator2"); + + firstElected.push(validator1); + firstElected.push(validator2); + + scoreManagerAddress = actor("scoreManagerAddress"); + accountsAddress = actor("accountsAddress"); + + reserveAddress = actor("reserve"); + + epochManagerEnabler = actor("epochManagerEnabler"); + carbonOffsettingPartner = actor("carbonOffsettingPartner"); + communityRewardFund = actor("communityRewardFund"); + + deployCodeTo("MockRegistry.sol", abi.encode(false), REGISTRY_ADDRESS); + deployCodeTo("ScoreManager.sol", abi.encode(false), scoreManagerAddress); + deployCodeTo("Accounts.sol", abi.encode(false), accountsAddress); + deployCodeTo("MockValidators.sol", abi.encode(false), address(validators)); + + registry = IRegistry(REGISTRY_ADDRESS); + scoreManager = ScoreManager(scoreManagerAddress); + + registry.setAddressFor(EpochManagerContract, address(epochManager)); + registry.setAddressFor(EpochManagerEnablerContract, epochManagerEnabler); + registry.setAddressFor(SortedOraclesContract, address(sortedOracles)); + registry.setAddressFor(GovernanceContract, communityRewardFund); + registry.setAddressFor(EpochRewardsContract, address(epochRewards)); + registry.setAddressFor(ValidatorsContract, address(validators)); + registry.setAddressFor(ScoreManagerContract, address(scoreManager)); + registry.setAddressFor(StableTokenContract, address(stableToken)); + registry.setAddressFor(CeloUnreleasedTreasuryContract, address(celoUnreleasedTreasury)); + registry.setAddressFor(CeloTokenContract, address(celoToken)); + registry.setAddressFor(ReserveContract, reserveAddress); + registry.setAddressFor(ElectionContract, address(election)); + registry.setAddressFor(AccountsContract, address(accounts)); + + celoToken.setTotalSupply(CELO_SUPPLY_CAP); + vm.deal(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); + celoToken.setBalanceOf(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); + + celoUnreleasedTreasury.setRegistry(REGISTRY_ADDRESS); + + sortedOracles.setMedianRate(address(stableToken), stableAmountForRate); + + scoreManager.setValidatorScore(actor("validator1"), 1); + + epochManager.initialize(REGISTRY_ADDRESS, 10); + epochRewards.setCarbonOffsettingPartner(carbonOffsettingPartner); + + blockTravel(vm, firstEpochBlock); + + validators.setEpochRewards(validator1, validator1Reward); + validators.setEpochRewards(validator2, validator2Reward); + } + + function initializeEpochManagerSystem() public { + validators.setValidatorGroup(group); + validators.setValidator(validator1); + accounts.setValidatorSigner(validator1, validator1); + validators.setValidator(validator2); + accounts.setValidatorSigner(validator2, validator2); + + address[] memory members = new address[](2); + members[0] = validator1; + members[1] = validator2; + validators.setMembers(group, members); + + election.setElectedValidators(members); + + deployCodeTo("MockRegistry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + + travelNL2Epoch(vm, 1); + } + + function getGroupsWithLessersAndGreaters() + public + view + returns (address[] memory, address[] memory, address[] memory) + { + address[] memory groups = new address[](1); + groups[0] = group; + + address[] memory lessers = new address[](1); + lessers[0] = address(0); + + address[] memory greaters = new address[](1); + greaters[0] = address(0); + + return (groups, lessers, greaters); + } + + function _travelAndProcess_N_L2Epoch(uint256 n) public { + for (uint256 i = 0; i < n; i++) { + travelNL2Epoch(vm, 1); + epochManager.startNextEpochProcess(); + + ( + address[] memory groups, + address[] memory lessers, + address[] memory greaters + ) = getGroupsWithLessersAndGreaters(); + + epochManager.finishNextEpochProcess(groups, lessers, greaters); + } + } +} + +contract EpochManagerTest_initialize is EpochManagerTest { + function test_initialize() public virtual { + assertEq(address(epochManager.registry()), REGISTRY_ADDRESS); + assertEq(epochManager.epochDuration(), 10); + assertEq(epochManager.oracleAddress(), address(sortedOracles)); + } + + function test_Reverts_WhenAlreadyInitialized() public virtual { + vm.expectRevert("contract already initialized"); + epochManager.initialize(REGISTRY_ADDRESS, 10); + } +} + +contract EpochManagerTest_initializeSystem is EpochManagerTest { + function test_processCanBeStarted() public virtual { + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + ( + uint256 _firstEpochBlock, + uint256 _lastEpochBlock, + uint256 _startTimestamp, + uint256 _currentRewardsBlock + ) = epochManager.getCurrentEpoch(); + assertGt(epochManager.getElectedAccounts().length, 0); + assertEq(epochManager.firstKnownEpoch(), firstEpochNumber); + assertEq(_firstEpochBlock, firstEpochBlock); + assertEq(_lastEpochBlock, 0); + assertEq(_startTimestamp, block.timestamp); + assertEq(_currentRewardsBlock, 0); + assertEq(epochManager.getElectedAccounts(), firstElected); + } + + function test_Reverts_processCannotBeStartedAgain() public virtual { + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + vm.prank(epochManagerEnabler); + vm.expectRevert("Epoch system already initialized"); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + } + + function test_Reverts_WhenSystemInitializedByOtherContract() public virtual { + vm.expectRevert("msg.sender is not Enabler"); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + } +} + +contract EpochManagerTest_startNextEpochProcess is EpochManagerTest { + function test_Reverts_whenSystemNotInitialized() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.startNextEpochProcess(); + } + + function test_Reverts_WhenEndOfEpochHasNotBeenReached() public { + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + + vm.expectRevert("Epoch is not ready to start"); + epochManager.startNextEpochProcess(); + } + + function test_Reverts_WhenEpochProcessingAlreadyStarted() public { + initializeEpochManagerSystem(); + + epochManager.startNextEpochProcess(); + vm.expectRevert("Epoch process is already started"); + epochManager.startNextEpochProcess(); + } + + function test_Emits_EpochProcessingStartedEvent() public { + initializeEpochManagerSystem(); + + vm.expectEmit(true, true, true, true); + emit EpochProcessingStarted(firstEpochNumber); + epochManager.startNextEpochProcess(); + } + + function test_SetsTheEpochRewardBlock() public { + initializeEpochManagerSystem(); + + epochManager.startNextEpochProcess(); + (, , , uint256 _currentRewardsBlock) = epochManager.getCurrentEpoch(); + assertEq(_currentRewardsBlock, block.number); + } + + function test_SetsTheEpochRewardAmounts() public { + initializeEpochManagerSystem(); + + epochManager.startNextEpochProcess(); + ( + uint256 status, + uint256 perValidatorReward, + uint256 totalRewardsVoter, + uint256 totalRewardsCommunity, + uint256 totalRewardsCarbonFund + ) = epochManager.getEpochProcessingState(); + assertEq(status, 1); + assertEq(perValidatorReward, 5); + assertEq(totalRewardsVoter, 6); + assertEq(totalRewardsCommunity, 7); + assertEq(totalRewardsCarbonFund, 8); + } + + function test_ShouldMintTotalValidatorStableRewardsToEpochManager() public { + initializeEpochManagerSystem(); + epochManager.startNextEpochProcess(); + + assertEq(validators.mintedStable(), validator1Reward + validator2Reward); + } + + function test_ShouldReleaseCorrectAmountToReserve() public { + initializeEpochManagerSystem(); + epochManager.startNextEpochProcess(); + uint256 reserveBalanceAfter = celoToken.balanceOf(reserveAddress); + assertEq( + reserveBalanceAfter, + (stableAmountForRate * (validator1Reward + validator2Reward)) / 1e24 + ); + } +} + +contract EpochManagerTest_setEpochDuration is EpochManagerTest { + uint256 newEpochDuration = 5 * DAY; + + function test_setsNewEpochDuration() public { + initializeEpochManagerSystem(); + epochManager.setEpochDuration(newEpochDuration); + assertEq(epochManager.epochDuration(), newEpochDuration); + } + + function test_Emits_EpochDurationSetEvent() public { + initializeEpochManagerSystem(); + + vm.expectEmit(true, true, true, true); + emit EpochDurationSet(newEpochDuration); + epochManager.setEpochDuration(newEpochDuration); + } + + function test_Reverts_WhenIsOnEpochProcess() public { + initializeEpochManagerSystem(); + epochManager.startNextEpochProcess(); + vm.expectRevert("Cannot change epoch duration during processing."); + epochManager.setEpochDuration(newEpochDuration); + } + + function test_Reverts_WhenNewEpochDurationIsZero() public { + initializeEpochManagerSystem(); + + vm.expectRevert("New epoch duration must be greater than zero."); + epochManager.setEpochDuration(0); + } +} + +contract EpochManagerTest_setOracleAddress is EpochManagerTest { + address newOracleAddress = actor("newOarcle"); + + function test_setsNewOracleAddress() public { + initializeEpochManagerSystem(); + epochManager.setOracleAddress(newOracleAddress); + assertEq(epochManager.oracleAddress(), newOracleAddress); + } + + function test_Emits_OracleAddressSetEvent() public { + initializeEpochManagerSystem(); + + vm.expectEmit(true, true, true, true); + emit OracleAddressSet(newOracleAddress); + epochManager.setOracleAddress(newOracleAddress); + } + + function test_Reverts_WhenIsOnEpochProcess() public { + initializeEpochManagerSystem(); + epochManager.startNextEpochProcess(); + vm.expectRevert("Cannot change oracle address during epoch processing."); + epochManager.setOracleAddress(newOracleAddress); + } + + function test_Reverts_WhenNewOracleAddressIsZero() public { + initializeEpochManagerSystem(); + + vm.expectRevert("Cannot set address zero as the Oracle."); + epochManager.setOracleAddress(address(0)); + } + + function test_Reverts_WhenNewOracleAddressIsunchanged() public { + initializeEpochManagerSystem(); + + vm.expectRevert("Oracle address cannot be the same."); + epochManager.setOracleAddress(address(sortedOracles)); + } +} + +contract EpochManagerTest_sendValidatorPayment is EpochManagerTest { + address signer1 = actor("signer1"); + address signer2 = actor("signer2"); + address beneficiary = actor("beneficiary"); + + uint256 paymentAmount = 4 ether; + uint256 quarterOfPayment = paymentAmount / 4; + uint256 halfOfPayment = paymentAmount / 2; + uint256 threeQuartersOfPayment = (paymentAmount / 4) * 3; + uint256 twentyFivePercent = 250000000000000000000000; + uint256 fiftyPercent = 500000000000000000000000; + + uint256 epochManagerBalanceBefore; + + function setUp() public override { + super.setUp(); + + validators.setValidatorGroup(group); + validators.setValidator(validator1); + accounts.setValidatorSigner(validator1, signer1); + validators.setValidator(validator2); + accounts.setValidatorSigner(validator2, signer2); + + address[] memory members = new address[](2); + members[0] = validator1; + members[1] = validator2; + validators.setMembers(group, members); + + vm.prank(epochManagerEnabler); + epochManager.initializeSystem(firstEpochNumber, firstEpochBlock, firstElected); + + stableToken.mint(address(epochManager), paymentAmount * 2); + epochManagerBalanceBefore = stableToken.balanceOf(address(epochManager)); + epochManager._setPaymentAllocation(validator1, paymentAmount); + } + + function test_sendsCUsdFromEpochManagerToValidator() public { + epochManager.sendValidatorPayment(validator1); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, paymentAmount); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore - paymentAmount); + } + + function test_sendsCUsdFromEpochManagerToValidatorAndGroup() public { + validators.setCommission(group, twentyFivePercent); + + epochManager.sendValidatorPayment(validator1); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 groupBalanceAfter = stableToken.balanceOf(group); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, threeQuartersOfPayment); + assertEq(groupBalanceAfter, quarterOfPayment); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore - paymentAmount); + } + + function test_sendsCUsdFromEpochManagerToValidatorAndBeneficiary() public { + accounts.setPaymentDelegationFor(validator1, beneficiary, twentyFivePercent); + + epochManager.sendValidatorPayment(validator1); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 beneficiaryBalanceAfter = stableToken.balanceOf(beneficiary); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, threeQuartersOfPayment); + assertEq(beneficiaryBalanceAfter, quarterOfPayment); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore - paymentAmount); + } + + function test_sendsCUsdFromEpochManagerToValidatorAndGroupAndBeneficiary() public { + validators.setCommission(group, fiftyPercent); + accounts.setPaymentDelegationFor(validator1, beneficiary, fiftyPercent); + + epochManager.sendValidatorPayment(validator1); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 groupBalanceAfter = stableToken.balanceOf(group); + uint256 beneficiaryBalanceAfter = stableToken.balanceOf(beneficiary); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, quarterOfPayment); + assertEq(groupBalanceAfter, halfOfPayment); + assertEq(beneficiaryBalanceAfter, quarterOfPayment); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore - paymentAmount); + } + + function test_emitsAValidatorEpochPaymentDistributedEvent() public { + validators.setCommission(group, fiftyPercent); + accounts.setPaymentDelegationFor(validator1, beneficiary, fiftyPercent); + + vm.expectEmit(true, true, true, true, address(epochManager)); + emit ValidatorEpochPaymentDistributed( + validator1, + quarterOfPayment, + group, + halfOfPayment, + beneficiary, + quarterOfPayment + ); + epochManager.sendValidatorPayment(validator1); + } + + function test_doesNothingIfNotAllocated() public { + validators.setCommission(group, fiftyPercent); + accounts.setPaymentDelegationFor(validator2, beneficiary, fiftyPercent); + + epochManager.sendValidatorPayment(validator2); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 groupBalanceAfter = stableToken.balanceOf(group); + uint256 beneficiaryBalanceAfter = stableToken.balanceOf(beneficiary); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, 0); + assertEq(groupBalanceAfter, 0); + assertEq(beneficiaryBalanceAfter, 0); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore); + } + + function test_doesntAllowDoubleSending() public { + epochManager.sendValidatorPayment(validator1); + epochManager.sendValidatorPayment(validator1); + + uint256 validatorBalanceAfter = stableToken.balanceOf(validator1); + uint256 epochManagerBalanceAfter = stableToken.balanceOf(address(epochManager)); + + assertEq(validatorBalanceAfter, paymentAmount); + assertEq(epochManagerBalanceAfter, epochManagerBalanceBefore - paymentAmount); + } +} + +contract EpochManagerTest_finishNextEpochProcess is EpochManagerTest { + address signer1 = actor("signer1"); + address signer2 = actor("signer2"); + + address validator3 = actor("validator3"); + address validator4 = actor("validator4"); + + address group2 = actor("group2"); + + address[] elected; + + uint256 groupEpochRewards = 44e18; + + function setUp() public override { + super.setUp(); + + validators.setValidatorGroup(group); + validators.setValidator(validator1); + accounts.setValidatorSigner(validator1, signer1); + validators.setValidator(validator2); + accounts.setValidatorSigner(validator2, signer2); + + validators.setValidatorGroup(group2); + validators.setValidator(validator3); + validators.setValidator(validator4); + + address[] memory members = new address[](3); + members[0] = validator1; + members[1] = validator2; + validators.setMembers(group, members); + members[0] = validator3; + members[1] = validator4; + validators.setMembers(group2, members); + + vm.prank(epochManagerEnabler); + initializeEpochManagerSystem(); + + elected = epochManager.getElectedAccounts(); + + election.setGroupEpochRewardsBasedOnScore(group, groupEpochRewards); + } + + function test_Reverts_WhenNotStarted() public { + address[] memory groups = new address[](0); + + vm.expectRevert("Epoch process is not started"); + epochManager.finishNextEpochProcess(groups, groups, groups); + } + + function test_Reverts_WhenGroupsDoNotMatch() public { + address[] memory groups = new address[](0); + epochManager.startNextEpochProcess(); + vm.expectRevert("number of groups does not match"); + epochManager.finishNextEpochProcess(groups, groups, groups); + } + + function test_Reverts_WhenGroupsNotFromElected() public { + address[] memory groups = new address[](1); + groups[0] = group2; + epochManager.startNextEpochProcess(); + vm.expectRevert("group not from current elected set"); + epochManager.finishNextEpochProcess(groups, groups, groups); + } + + function test_TransfersToCommunityAndCarbonOffsetting() public { + ( + address[] memory groups, + address[] memory lessers, + address[] memory greaters + ) = getGroupsWithLessersAndGreaters(); + + epochManager.startNextEpochProcess(); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + + assertEq(celoToken.balanceOf(communityRewardFund), epochRewards.totalRewardsCommunity()); + assertEq(celoToken.balanceOf(carbonOffsettingPartner), epochRewards.totalRewardsCarbonFund()); + } + + function test_TransfersToValidatorGroup() public { + ( + address[] memory groups, + address[] memory lessers, + address[] memory greaters + ) = getGroupsWithLessersAndGreaters(); + + epochManager.startNextEpochProcess(); + epochManager.finishNextEpochProcess(groups, lessers, greaters); + + assertEq(election.distributedEpochRewards(group), groupEpochRewards); + } + + function test_SetsNewlyElectedCorrectly() public { + ( + address[] memory groups, + address[] memory lessers, + address[] memory greaters + ) = getGroupsWithLessersAndGreaters(); + + epochManager.startNextEpochProcess(); + + address[] memory newElected = new address[](2); + newElected[0] = validator3; + newElected[1] = validator4; + election.setElectedValidators(newElected); + + epochManager.finishNextEpochProcess(groups, lessers, greaters); + + address[] memory afterElected = epochManager.getElectedAccounts(); + + for (uint256 i = 0; i < newElected.length; i++) { + assertEq(newElected[i], afterElected[i]); + } + } + + function test_Emits_GroupProcessedEvent() public { + ( + address[] memory groups, + address[] memory lessers, + address[] memory greaters + ) = getGroupsWithLessersAndGreaters(); + + epochManager.startNextEpochProcess(); + + for (uint i = 0; i < groups.length; i++) { + vm.expectEmit(true, true, true, true); + emit GroupProcessed(groups[i], firstEpochNumber); + } + + epochManager.finishNextEpochProcess(groups, lessers, greaters); + } +} + +contract EpochManagerTest_setToProcessGroups is EpochManagerTest { + address signer1 = actor("signer1"); + address signer2 = actor("signer2"); + + address validator3 = actor("validator3"); + address validator4 = actor("validator4"); + + address group2 = actor("group2"); + + address[] elected; + + uint256 groupEpochRewards = 44e18; + + function setUp() public override { + super.setUp(); + + validators.setValidatorGroup(group); + validators.setValidator(validator1); + accounts.setValidatorSigner(validator1, signer1); + validators.setValidator(validator2); + accounts.setValidatorSigner(validator2, signer2); + + validators.setValidatorGroup(group2); + validators.setValidator(validator3); + validators.setValidator(validator4); + + address[] memory members = new address[](3); + members[0] = validator1; + members[1] = validator2; + validators.setMembers(group, members); + members[0] = validator3; + members[1] = validator4; + validators.setMembers(group2, members); + + vm.prank(epochManagerEnabler); + initializeEpochManagerSystem(); + + elected = epochManager.getElectedAccounts(); + + election.setGroupEpochRewardsBasedOnScore(group, groupEpochRewards); + } + + function test_Reverts_WhenNotStarted() public { + vm.expectRevert("Epoch process is not started"); + epochManager.setToProcessGroups(); + } + + function test_setsToProcessGroups() public { + (address[] memory groups, , ) = getGroupsWithLessersAndGreaters(); + + epochManager.startNextEpochProcess(); + epochManager.setToProcessGroups(); + + assertEq(EpochManager(address(epochManager)).toProcessGroups(), groups.length); + } + + function test_setsGroupRewards() public { + (address[] memory groups, , ) = getGroupsWithLessersAndGreaters(); + epochManager.startNextEpochProcess(); + epochManager.setToProcessGroups(); + + for (uint256 i = 0; i < groups.length; i++) { + assertEq(EpochManager(address(epochManager)).processedGroups(group), groupEpochRewards); + } + } + + function test_Emits_GroupMarkedForProcessingEvent() public { + (address[] memory groups, , ) = getGroupsWithLessersAndGreaters(); + + epochManager.startNextEpochProcess(); + + for (uint i = 0; i < groups.length; i++) { + vm.expectEmit(true, true, true, true); + emit GroupMarkedForProcessing(groups[i], firstEpochNumber); + } + + epochManager.setToProcessGroups(); + } +} + +contract EpochManagerTest_processGroup is EpochManagerTest { + address signer1 = actor("signer1"); + address signer2 = actor("signer2"); + address signer3 = actor("signer3"); + address signer4 = actor("signer4"); + + address validator3 = actor("validator3"); + address validator4 = actor("validator4"); + + address group2 = actor("group2"); + + address[] elected; + + uint256 groupEpochRewards = 44e18; + + function setUp() public override { + super.setUp(); + + validators.setValidatorGroup(group); + validators.setValidator(validator1); + accounts.setValidatorSigner(validator1, signer1); + validators.setValidator(validator2); + accounts.setValidatorSigner(validator2, signer2); + + validators.setValidatorGroup(group2); + validators.setValidator(validator3); + validators.setValidator(validator4); + + address[] memory members = new address[](3); + members[0] = validator1; + members[1] = validator2; + validators.setMembers(group, members); + members[0] = validator3; + members[1] = validator4; + validators.setMembers(group2, members); + + vm.prank(epochManagerEnabler); + initializeEpochManagerSystem(); + + elected = epochManager.getElectedAccounts(); + + election.setGroupEpochRewardsBasedOnScore(group, groupEpochRewards); + } + + function test_Reverts_WhenNotStarted() public { + vm.expectRevert("Indivudual epoch process is not started"); + epochManager.processGroup(group, address(0), address(0)); + } + + function test_Reverts_WhenGroupNotInToProcessGroups() public { + epochManager.startNextEpochProcess(); + epochManager.setToProcessGroups(); + vm.expectRevert("group not from current elected set"); + epochManager.processGroup(group2, address(0), address(0)); + } + + function test_ProcessesGroup() public { + epochManager.startNextEpochProcess(); + epochManager.setToProcessGroups(); + epochManager.processGroup(group, address(0), address(0)); + + (uint256 status, , , , ) = epochManager.getEpochProcessingState(); + assertEq(status, 0); + } + + function test_TransfersToCommunityAndCarbonOffsetting() public { + epochManager.startNextEpochProcess(); + epochManager.setToProcessGroups(); + epochManager.processGroup(group, address(0), address(0)); + + assertEq(celoToken.balanceOf(communityRewardFund), epochRewards.totalRewardsCommunity()); + assertEq(celoToken.balanceOf(carbonOffsettingPartner), epochRewards.totalRewardsCarbonFund()); + } + + function test_TransfersToValidatorGroup() public { + epochManager.startNextEpochProcess(); + epochManager.setToProcessGroups(); + epochManager.processGroup(group, address(0), address(0)); + + assertEq(election.distributedEpochRewards(group), groupEpochRewards); + } + + function test_SetsNewlyElectedCorrectly() public { + ( + address[] memory groups, + address[] memory lessers, + address[] memory greaters + ) = getGroupsWithLessersAndGreaters(); + + epochManager.startNextEpochProcess(); + + address[] memory newElected = new address[](2); + newElected[0] = validator3; + newElected[1] = validator4; + election.setElectedValidators(newElected); + + address[] memory signers = new address[](2); + signers[0] = signer3; + signers[1] = signer4; + accounts.setValidatorSigner(validator3, signer3); + accounts.setValidatorSigner(validator4, signer4); + + epochManager.setToProcessGroups(); + + for (uint256 i = 0; i < groups.length; i++) { + epochManager.processGroup(groups[i], lessers[i], greaters[i]); + } + + address[] memory afterElected = epochManager.getElectedAccounts(); + + for (uint256 i = 0; i < newElected.length; i++) { + assertEq(newElected[i], afterElected[i]); + } + + address[] memory afterSigners = epochManager.getElectedSigners(); + assertEq(afterSigners.length, signers.length); + for (uint256 i = 0; i < signers.length; i++) { + assertEq(signers[i], afterSigners[i]); + } + } + + function test_Emits_GroupProcessed() public { + epochManager.startNextEpochProcess(); + epochManager.setToProcessGroups(); + vm.expectEmit(true, true, true, true); + emit GroupProcessed(group, firstEpochNumber); + epochManager.processGroup(group, address(0), address(0)); + } +} + +contract EpochManagerTest_getEpochByNumber is EpochManagerTest { + function test_shouldReturnTheEpochInfoOfSpecifiedEpoch() public { + uint256 numberOfEpochsToTravel = 9; + + initializeEpochManagerSystem(); + uint256 _startingEpochNumber = epochManager.getCurrentEpochNumber(); + + (uint256 startingEpochFirstBlock, , uint256 startingEpochStartTimestamp, ) = epochManager + .getCurrentEpoch(); + + _travelAndProcess_N_L2Epoch(numberOfEpochsToTravel); + + ( + uint256 _firstBlock, + uint256 _lastBlock, + uint256 _startTimestamp, + uint256 _rewardBlock + ) = epochManager.getEpochByNumber(_startingEpochNumber + numberOfEpochsToTravel); + + assertEq( + startingEpochFirstBlock + (L2_BLOCK_IN_EPOCH * (numberOfEpochsToTravel + 1)) + 1, + _firstBlock + ); + assertEq(_lastBlock, 0); + assertEq(startingEpochStartTimestamp + (DAY * (numberOfEpochsToTravel + 1)), _startTimestamp); + assertEq(_rewardBlock, 0); + } + + function test_ReturnsHistoricalEpochInfoAfter_N_Epochs() public { + initializeEpochManagerSystem(); + uint256 _startingEpochNumber = epochManager.getCurrentEpochNumber(); + uint256 numberOfEpochsToTravel = 7; + ( + uint256 _startingEpochFirstBlock, + uint256 _startingLastBlock, + uint256 _startingStartTimestamp, + uint256 _startingRewardBlock + ) = epochManager.getCurrentEpoch(); + + _travelAndProcess_N_L2Epoch(numberOfEpochsToTravel); + + ( + uint256 _initialFirstBlock, + uint256 _initialLastBlock, + uint256 _initialStartTimestamp, + uint256 _initialRewardBlock + ) = epochManager.getEpochByNumber(_startingEpochNumber); + + assertEq(_initialFirstBlock, _startingEpochFirstBlock); + assertEq(_initialLastBlock, _startingLastBlock + (L2_BLOCK_IN_EPOCH * 2) + firstEpochBlock); + assertEq(_initialStartTimestamp, _startingStartTimestamp); + assertEq( + _initialRewardBlock, + _startingRewardBlock + (L2_BLOCK_IN_EPOCH * 2) + firstEpochBlock + 1 + ); + } + + function test_ReturnsZeroForFutureEpochs() public { + initializeEpochManagerSystem(); + ( + uint256 _firstBlock, + uint256 _lastBlock, + uint256 _startTimestamp, + uint256 _rewardBlock + ) = epochManager.getEpochByNumber(500); + + assertEq(_firstBlock, 0); + assertEq(_lastBlock, 0); + assertEq(_startTimestamp, 0); + assertEq(_rewardBlock, 0); + } +} + +contract EpochManagerTest_getEpochNumberOfBlock is EpochManagerTest { + function test_ShouldRetreiveTheCorrectBlockNumberOfTheEpoch() public { + initializeEpochManagerSystem(); + assertEq(epochManager.getEpochNumberOfBlock(firstEpochBlock), firstEpochNumber); + } + + function test_Reverts_WhenL1() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.getEpochNumberOfBlock(firstEpochBlock); + } +} + +contract EpochManagerTest_getEpochByBlockNumber is EpochManagerTest { + function test_ShouldRetreiveTheCorrectEpochInfoOfGivenBlock() public { + initializeEpochManagerSystem(); + + _travelAndProcess_N_L2Epoch(2); + + (uint256 _firstBlock, uint256 _lastBlock, , ) = epochManager.getEpochByBlockNumber( + firstEpochBlock + (3 * L2_BLOCK_IN_EPOCH) + ); + assertEq(_firstBlock, firstEpochBlock + 1 + (2 * L2_BLOCK_IN_EPOCH)); + assertEq(_lastBlock, firstEpochBlock + 1 + (3 * L2_BLOCK_IN_EPOCH) - 1); + } + + function test_Reverts_WhenL1() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.getEpochNumberOfBlock(firstEpochBlock); + } +} + +contract EpochManagerTest_numberOfElectedInCurrentSet is EpochManagerTest { + function test_ShouldRetreiveTheNumberOfElected() public { + initializeEpochManagerSystem(); + assertEq(epochManager.numberOfElectedInCurrentSet(), 2); + } + + function test_Reverts_WhenL1() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.numberOfElectedInCurrentSet(); + } +} + +contract EpochManagerTest_getElectedAccounts is EpochManagerTest { + function test_ShouldRetreiveThelistOfElectedAccounts() public { + initializeEpochManagerSystem(); + assertEq(epochManager.getElectedAccounts(), firstElected); + } + + function test_Reverts_WhenL1() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.getElectedAccounts(); + } +} + +contract EpochManagerTest_getElectedAccountByIndex is EpochManagerTest { + function test_ShouldRetreiveThecorrectValidator() public { + initializeEpochManagerSystem(); + assertEq(epochManager.getElectedAccountByIndex(0), validator1); + } + + function test_Reverts_WhenL1() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.getElectedAccountByIndex(0); + } +} +contract EpochManagerTest_getElectedSigners is EpochManagerTest { + function test_ShouldRetreiveTheElectedSigners() public { + initializeEpochManagerSystem(); + address[] memory electedSigners = new address[](firstElected.length); + electedSigners[0] = accounts.getValidatorSigner(firstElected[0]); + electedSigners[1] = accounts.getValidatorSigner(firstElected[1]); + assertEq(epochManager.getElectedSigners(), electedSigners); + } + + function test_Reverts_WhenL1() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.getElectedSigners(); + } +} +contract EpochManagerTest_getElectedSignerByIndex is EpochManagerTest { + function test_ShouldRetreiveThecorrectElectedSigner() public { + initializeEpochManagerSystem(); + address[] memory electedSigners = new address[](firstElected.length); + + electedSigners[1] = accounts.getValidatorSigner(firstElected[1]); + assertEq(epochManager.getElectedSignerByIndex(1), electedSigners[1]); + } + + function test_Reverts_WhenL1() public { + vm.expectRevert("Epoch system not initialized"); + epochManager.getElectedSignerByIndex(1); + } +} diff --git a/packages/protocol/test-sol/unit/common/EpochManagerEnabler.t.sol b/packages/protocol/test-sol/unit/common/EpochManagerEnabler.t.sol new file mode 100644 index 00000000000..3e91fc2e121 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/EpochManagerEnabler.t.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import "@celo-contracts-8/common/EpochManager.sol"; + +import { EpochManagerEnablerMock } from "@test-sol/mocks/EpochManagerEnablerMock.sol"; + +import { CeloUnreleasedTreasury } from "@celo-contracts-8/common/CeloUnreleasedTreasury.sol"; +import { ICeloUnreleasedTreasury } from "@celo-contracts/common/interfaces/ICeloUnreleasedTreasury.sol"; +import { IAccounts } from "@celo-contracts/common/interfaces/IAccounts.sol"; + +import { TestConstants } from "@test-sol/constants.sol"; +import { Utils08 } from "@test-sol/utils08.sol"; + +import "@celo-contracts/common/interfaces/IRegistry.sol"; + +import { EpochRewardsMock08 } from "@celo-contracts-8/governance/test/EpochRewardsMock.sol"; +import { ValidatorsMock } from "@test-sol/unit/governance/validators/mocks/ValidatorsMock.sol"; +import { MockCeloUnreleasedTreasury } from "@celo-contracts-8/common/test/MockCeloUnreleasedTreasury.sol"; +import "@celo-contracts-8/common/test/MockCeloToken.sol"; + +contract EpochManagerEnablerTest is Test, TestConstants, Utils08 { + EpochManager epochManager; + EpochManagerEnablerMock epochManagerEnabler; + MockCeloUnreleasedTreasury celoUnreleasedTreasury; + MockCeloToken08 celoToken; + + IRegistry registry; + IAccounts accounts; + + address accountsAddress; + address nonOwner; + address oracle; + + uint256 epochDuration = DAY; + uint256 numberValidators = 100; + + event LastKnownEpochNumberSet(uint256 lastKnownEpochNumber); + event LastKnownFirstBlockOfEpochSet(uint256 lastKnownFirstBlockOfEpoch); + event LastKnownElectedAccountsSet(); + + function setUp() public virtual { + ph.setEpochSize(17280); + epochManager = new EpochManager(true); + epochManagerEnabler = new EpochManagerEnablerMock(); + celoToken = new MockCeloToken08(); + + celoUnreleasedTreasury = new MockCeloUnreleasedTreasury(); + + accountsAddress = actor("accountsAddress"); + + nonOwner = actor("nonOwner"); + oracle = actor("oracle"); + + deployCodeTo("MockRegistry.sol", abi.encode(false), REGISTRY_ADDRESS); + deployCodeTo("Accounts.sol", abi.encode(false), accountsAddress); + + registry = IRegistry(REGISTRY_ADDRESS); + accounts = IAccounts(accountsAddress); + + registry.setAddressFor(EpochManagerContract, address(epochManager)); + registry.setAddressFor(EpochManagerEnablerContract, address(epochManagerEnabler)); + registry.setAddressFor(AccountsContract, address(accounts)); + registry.setAddressFor(CeloTokenContract, address(celoToken)); + registry.setAddressFor(SortedOraclesContract, oracle); + registry.setAddressFor(CeloUnreleasedTreasuryContract, address(celoUnreleasedTreasury)); + + celoToken.setTotalSupply(CELO_SUPPLY_CAP); + celoToken.setBalanceOf(address(celoUnreleasedTreasury), L2_INITIAL_STASH_BALANCE); + + epochManagerEnabler.initialize(REGISTRY_ADDRESS); + epochManager.initialize(REGISTRY_ADDRESS, epochDuration); + + _setupValidators(); + travelEpochL1(vm); + travelEpochL1(vm); + } + + function _setupValidators() internal { + for (uint256 i = 0; i < numberValidators; i++) { + vm.prank(vm.addr(i + 1)); + accounts.createAccount(); + + epochManagerEnabler.addValidator(vm.addr(i + 1)); + } + } +} + +contract EpochManagerEnablerTest_initialize is EpochManagerEnablerTest { + function test_initialize() public { + assertEq(address(epochManagerEnabler.registry()), REGISTRY_ADDRESS); + } + + function test_Reverts_WhenAlreadyInitialized() public virtual { + vm.expectRevert("contract already initialized"); + epochManagerEnabler.initialize(REGISTRY_ADDRESS); + } +} + +contract EpochManagerEnablerTest_initEpochManager is EpochManagerEnablerTest { + function test_CanBeCalledByAnyone() public { + epochManagerEnabler.captureEpochAndValidators(); + + whenL2(vm); + vm.prank(nonOwner); + epochManagerEnabler.initEpochManager(); + + assertGt(epochManager.getElectedAccounts().length, 0); + assertTrue(epochManager.systemAlreadyInitialized()); + } + + function test_Reverts_ifEpochAndValidatorsAreNotCaptured() public { + whenL2(vm); + vm.expectRevert("lastKnownEpochNumber not set."); + + epochManagerEnabler.initEpochManager(); + } + + function test_Reverts_whenL1() public { + vm.expectRevert("This method is not supported in L1."); + + epochManagerEnabler.initEpochManager(); + } +} + +contract EpochManagerEnablerTest_captureEpochAndValidators is EpochManagerEnablerTest { + function test_Reverts_whenL2() public { + whenL2(vm); + vm.expectRevert("This method is no longer supported in L2."); + epochManagerEnabler.captureEpochAndValidators(); + } + + function test_shouldSetLastKnownElectedAccounts() public { + epochManagerEnabler.captureEpochAndValidators(); + + assertEq(epochManagerEnabler.getlastKnownElectedAccounts().length, numberValidators); + } + + function test_shouldSetLastKnownEpochNumber() public { + epochManagerEnabler.captureEpochAndValidators(); + + assertEq(epochManagerEnabler.lastKnownEpochNumber(), 3); + } + + function test_shouldSetLastKnownFirstBlockOfEpoch() public { + epochManagerEnabler.captureEpochAndValidators(); + + assertEq(epochManagerEnabler.lastKnownFirstBlockOfEpoch(), 17280 * 2); + } + + function test_Emits_LastKnownEpochNumberSet() public { + vm.expectEmit(true, true, true, true); + emit LastKnownEpochNumberSet(3); + + epochManagerEnabler.captureEpochAndValidators(); + } + + function test_Emits_LastKnownElectedAccountsSet() public { + vm.expectEmit(true, true, true, true); + emit LastKnownElectedAccountsSet(); + + epochManagerEnabler.captureEpochAndValidators(); + } + + function test_Emits_LastKnownFirstBlockOfEpochSet() public { + vm.expectEmit(true, true, true, true); + emit LastKnownFirstBlockOfEpochSet(34560); + + epochManagerEnabler.captureEpochAndValidators(); + } +} + +contract EpochManagerEnablerTest_getFirstBlockOfEpoch is EpochManagerEnablerTest { + function test_blockIsEpockBlock() public { + vm.roll(27803520); + epochManagerEnabler.setFirstBlockOfEpoch(); + assertEq(epochManagerEnabler.lastKnownFirstBlockOfEpoch(), 27803520); + } + + function test_blockIsNotEpochBlock() public { + vm.roll(27817229); + epochManagerEnabler.setFirstBlockOfEpoch(); + assertEq(epochManagerEnabler.lastKnownFirstBlockOfEpoch(), 27803520); + } +} diff --git a/packages/protocol/test-sol/common/ExtractFunctionSignature.t.sol b/packages/protocol/test-sol/unit/common/ExtractFunctionSignature.t.sol similarity index 100% rename from packages/protocol/test-sol/common/ExtractFunctionSignature.t.sol rename to packages/protocol/test-sol/unit/common/ExtractFunctionSignature.t.sol diff --git a/packages/protocol/test-sol/common/FeeCurrencyDirectory.t.sol b/packages/protocol/test-sol/unit/common/FeeCurrencyDirectory.t.sol similarity index 83% rename from packages/protocol/test-sol/common/FeeCurrencyDirectory.t.sol rename to packages/protocol/test-sol/unit/common/FeeCurrencyDirectory.t.sol index 06d01e8106d..77348b65969 100644 --- a/packages/protocol/test-sol/common/FeeCurrencyDirectory.t.sol +++ b/packages/protocol/test-sol/unit/common/FeeCurrencyDirectory.t.sol @@ -2,14 +2,16 @@ pragma solidity >=0.8.7 <0.8.20; import "celo-foundry-8/Test.sol"; -import "../../contracts-0.8/common/FeeCurrencyDirectory.sol"; -import "../../contracts-0.8/common/mocks/MockOracle.sol"; +import "@celo-contracts-8/common/FeeCurrencyDirectory.sol"; +import "@celo-contracts-8/common/mocks/MockOracle.sol"; contract FeeCurrencyDirectoryTestBase is Test { FeeCurrencyDirectory directory; MockOracle oracle; address nonOwner; address owner; + event CurrencyConfigSet(address indexed token, address indexed oracle, uint256 intrinsicGas); + event CurrencyRemoved(address indexed token); function setUp() public virtual { owner = address(this); @@ -33,6 +35,16 @@ contract TestSetCurrencyConfig is FeeCurrencyDirectoryTestBase { assertEq(config.intrinsicGas, intrinsicGas); } + function test_Emits_CurrencyConfigSetEvent() public { + address token = address(1); + uint256 intrinsicGas = 21000; + + vm.expectEmit(true, true, true, true); + emit CurrencyConfigSet(token, address(oracle), intrinsicGas); + + directory.setCurrencyConfig(token, address(oracle), intrinsicGas); + } + function test_Reverts_WhenNonOwnerSetsCurrencyConfig() public { address token = address(2); uint256 intrinsicGas = 21000; @@ -78,6 +90,13 @@ contract TestRemoveCurrencies is FeeCurrencyDirectoryTestBase { assertEq(config.oracle, address(0)); } + function test_Emits_CurrencyRemovedEvent() public { + address token = address(4); + vm.expectEmit(true, true, true, true); + emit CurrencyRemoved(token); + directory.removeCurrencies(token, 0); + } + function test_Reverts_WhenNonOwnerRemovesCurrencies() public { address token = address(4); vm.prank(nonOwner); diff --git a/packages/protocol/test-sol/common/FeeCurrencyWhitelist.t.sol b/packages/protocol/test-sol/unit/common/FeeCurrencyWhitelist.t.sol similarity index 55% rename from packages/protocol/test-sol/common/FeeCurrencyWhitelist.t.sol rename to packages/protocol/test-sol/unit/common/FeeCurrencyWhitelist.t.sol index 152873d76d4..6dae11cb09f 100644 --- a/packages/protocol/test-sol/common/FeeCurrencyWhitelist.t.sol +++ b/packages/protocol/test-sol/unit/common/FeeCurrencyWhitelist.t.sol @@ -2,9 +2,11 @@ pragma solidity ^0.5.13; import "celo-foundry/Test.sol"; -import "../../contracts/common/FeeCurrencyWhitelist.sol"; +import "@celo-contracts/common/FeeCurrencyWhitelist.sol"; -contract FeeCurrencyWhitelistTest is Test { +import { TestConstants } from "@test-sol/constants.sol"; + +contract FeeCurrencyWhitelistTest is Test, TestConstants { FeeCurrencyWhitelist feeCurrencyWhitelist; address nonOwner; address owner; @@ -16,6 +18,10 @@ contract FeeCurrencyWhitelistTest is Test { feeCurrencyWhitelist = new FeeCurrencyWhitelist(true); feeCurrencyWhitelist.initialize(); } + + function _whenL2() public { + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + } } contract FeeCurrencyWhitelistInitialize is FeeCurrencyWhitelistTest { @@ -43,6 +49,12 @@ contract FeeCurrencyWhitelistAddToken is FeeCurrencyWhitelistTest { vm.prank(nonOwner); feeCurrencyWhitelist.addToken(address(1)); } + + function test_Reverts_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + feeCurrencyWhitelist.addToken(address(1)); + } } contract FeeCurrencyWhitelistRemoveToken is FeeCurrencyWhitelistTest { @@ -71,4 +83,49 @@ contract FeeCurrencyWhitelistRemoveToken is FeeCurrencyWhitelistTest { vm.prank(nonOwner); feeCurrencyWhitelist.removeToken(address(2), 1); } + + function test_Reverts_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + feeCurrencyWhitelist.removeToken(address(2), 1); + } +} + +contract FeeCurrencyWhitelist_whitelist is FeeCurrencyWhitelistTest { + function setUp() public { + super.setUp(); + feeCurrencyWhitelist.addToken(address(1)); + } + + function test_ShouldRetrieveAToken() public { + address token = feeCurrencyWhitelist.whitelist(0); + assertEq(token, address(1)); + } + + function test_Reverts_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + feeCurrencyWhitelist.whitelist(0); + } +} + +contract FeeCurrencyWhitelist_getWhitelist is FeeCurrencyWhitelistTest { + function setUp() public { + super.setUp(); + feeCurrencyWhitelist.addToken(address(1)); + feeCurrencyWhitelist.addToken(address(2)); + } + + function test_ShouldRetrieveAToken() public { + address[] memory tokens = feeCurrencyWhitelist.getWhitelist(); + assertEq(tokens.length, 2); + assertEq(tokens[0], address(1)); + assertEq(tokens[1], address(2)); + } + + function test_Reverts_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + feeCurrencyWhitelist.getWhitelist(); + } } diff --git a/packages/protocol/test-sol/unit/common/FeeHandler.t.sol b/packages/protocol/test-sol/unit/common/FeeHandler.t.sol new file mode 100644 index 00000000000..e3757e488d7 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/FeeHandler.t.sol @@ -0,0 +1,1351 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; +pragma experimental ABIEncoderV2; + +// Refactor this test, make it easy to generate +// will have to support more fees + +import "@celo-contracts/common/FeeHandler.sol"; + +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; +import "@test-sol/utils/WhenL2.sol"; + +import { Exchange } from "@mento-core/contracts/Exchange.sol"; +import { StableToken } from "@mento-core/contracts/StableToken.sol"; +import "@celo-contracts/common/FixidityLib.sol"; +import "@celo-contracts/common/Freezer.sol"; +import "@celo-contracts/common/GoldToken.sol"; +import "@celo-contracts/common/FeeCurrencyWhitelist.sol"; +import "@celo-contracts/common/MentoFeeHandlerSeller.sol"; +import "@celo-contracts/common/UniswapFeeHandlerSeller.sol"; +import "@celo-contracts/uniswap/test/MockUniswapV2Router02.sol"; +import "@celo-contracts/uniswap/test/MockUniswapV2Factory.sol"; +import "@celo-contracts/uniswap/test/MockERC20.sol"; +import "@celo-contracts/stability/test/MockSortedOracles.sol"; +import "@mento-core/test/mocks/MockReserve.sol"; +import "@celo-contracts/common/ProxyFactory.sol"; +import "@celo-contracts/governance/GovernanceApproverMultiSig.sol"; + +contract FeeHandlerTest is TestWithUtils { + using FixidityLib for FixidityLib.Fraction; + + event BeneficiaryAdded(address beneficiary); + event BeneficiaryFractionSet(address beneficiary, uint256 fraction); + event BeneficiaryNameSet(address beneficiary, string name); + + FeeHandler feeHandler; + + GoldToken celoToken; + MockSortedOracles mockSortedOracles; + MockReserve mockReserve; + + Freezer freezer; + MockERC20 tokenA; + + MockUniswapV2Router02 uniswapRouter; + MockUniswapV2Router02 uniswapRouter2; + MockUniswapV2Factory uniswapFactory; + MockUniswapV2Factory uniswapFactory2; + + FeeCurrencyWhitelist feeCurrencyWhitelist; + + MentoFeeHandlerSeller mentoSeller; + UniswapFeeHandlerSeller uniswapFeeHandlerSeller; + + Exchange exchangeUSD; + Exchange exchangeEUR; + StableToken stableToken; + StableToken stableTokenEUR; + + address EXAMPLE_BENEFICIARY_ADDRESS = 0x2A486910DBC72cACcbb8d0e1439C96b03B2A4699; + address OTHER_BENEFICIARY_ADDRESS = 0x2A486910dBc72CACCBB8D0E1439c96B03b2A4610; + + address owner = address(this); + address user = actor("user"); + address celoUnreleasedTreasury = actor("CeloUnreleasedTreasury"); + + uint256 celoAmountForRate = 1e24; + uint256 stableAmountForRate = 2 * celoAmountForRate; + uint256 spread; + uint256 reserveFraction; + uint256 maxSlippage; + uint256 initialReserveBalance = 1e22; + + uint8 decimals = 18; + uint256 updateFrequency = 60 * 60; + uint256 minimumReports = 2; + address op; + + event SoldAndBurnedToken(address token, uint256 value); + event DailyLimitSet(address tokenAddress, uint256 newLimit); + event DailyLimitHit(address token, uint256 burning); + event MaxSlippageSet(address token, uint256 maxSlippage); + event DailySellLimitUpdated(uint256 amount); + event FeeBeneficiarySet(address newBeneficiary); + event BurnFractionSet(uint256 fraction); + event TokenAdded(address tokenAddress, address handlerAddress); + event TokenRemoved(address tokenAddress); + + function setUp() public { + super.setUp(); + vm.warp(YEAR); // foundry starts block.timestamp at 0, which leads to underflow errors in Uniswap contracts + op = actor("op"); + + spread = FixidityLib.newFixedFraction(3, 1000).unwrap(); + reserveFraction = FixidityLib.newFixedFraction(5, 100).unwrap(); + maxSlippage = FixidityLib.newFixedFraction(1, 100).unwrap(); + + celoToken = new GoldToken(true); + mockReserve = new MockReserve(); + stableToken = new StableToken(true); + stableTokenEUR = new StableToken(true); + registry = IRegistry(REGISTRY_ADDRESS); + feeHandler = new FeeHandler(true); + freezer = new Freezer(true); + feeCurrencyWhitelist = new FeeCurrencyWhitelist(true); + mentoSeller = new MentoFeeHandlerSeller(true); + uniswapFeeHandlerSeller = new UniswapFeeHandlerSeller(true); + + tokenA = new MockERC20(); + + feeCurrencyWhitelist.initialize(); + registry.setAddressFor("FeeCurrencyWhitelist", address(feeCurrencyWhitelist)); + registry.setAddressFor("Freezer", address(freezer)); + registry.setAddressFor("GoldToken", address(celoToken)); + registry.setAddressFor("CeloToken", address(celoToken)); + registry.setAddressFor("Reserve", address(mockReserve)); + registry.setAddressFor("CeloUnreleasedTreasury", celoUnreleasedTreasury); + + mockReserve.setGoldToken(address(celoToken)); + mockReserve.addToken(address(stableToken)); + mockReserve.addToken(address(stableTokenEUR)); + + address[] memory tokenAddresses; + uint256[] memory newMininumReports; + + mentoSeller.initialize(address(registry), tokenAddresses, newMininumReports); + celoToken.initialize(address(registry)); + stableToken.initialize( + "Celo Dollar", + "cUSD", + decimals, + address(registry), + FIXED1, + WEEK, + new address[](0), + new uint256[](0), + "Exchange" + ); + + stableTokenEUR.initialize( + "Celo Euro", + "cEUR", + decimals, + address(registry), + FIXED1, + WEEK, + new address[](0), + new uint256[](0), + "ExchangeEUR" + ); + + mockSortedOracles = new MockSortedOracles(); + registry.setAddressFor("SortedOracles", address(mockSortedOracles)); + + mockSortedOracles.setMedianRate(address(stableToken), stableAmountForRate); + mockSortedOracles.setMedianTimestampToNow(address(stableToken)); + mockSortedOracles.setNumRates(address(stableToken), 2); + + mockSortedOracles.setMedianRate(address(stableTokenEUR), stableAmountForRate); + mockSortedOracles.setMedianTimestampToNow(address(stableTokenEUR)); + mockSortedOracles.setNumRates(address(stableTokenEUR), 2); + + fundReserve(); + + exchangeUSD = new Exchange(true); + exchangeUSD.initialize( + address(registry), + "StableToken", + spread, + reserveFraction, + updateFrequency, + minimumReports + ); + + exchangeEUR = new Exchange(true); + exchangeEUR.initialize( + address(registry), + "StableTokenEUR", + spread, + reserveFraction, + updateFrequency, + minimumReports + ); + + registry.setAddressFor("StableToken", address(stableToken)); + registry.setAddressFor("Exchange", address(exchangeUSD)); + registry.setAddressFor("StableTokenEUR", address(stableTokenEUR)); + registry.setAddressFor("ExchangeEUR", address(exchangeEUR)); + + exchangeUSD.activateStable(); + exchangeEUR.activateStable(); + + feeHandler.initialize( + REGISTRY_ADDRESS, + EXAMPLE_BENEFICIARY_ADDRESS, + 0, + new address[](0), + new address[](0), + new uint256[](0), + new uint256[](0) + ); + } + + function fundReserve() public { + celoToken.transfer(address(mockReserve), initialReserveBalance); + } +} + +contract FeeHandlerTest_L2 is WhenL2, FeeHandlerTest {} + +contract FeeHandlerTest_Initialize is FeeHandlerTest { + function test_Reverts_WhenAlreadyInitialized() public { + vm.expectRevert("contract already initialized"); + feeHandler.initialize( + REGISTRY_ADDRESS, + EXAMPLE_BENEFICIARY_ADDRESS, + 0, + new address[](0), + new address[](0), + new uint256[](0), + new uint256[](0) + ); + } + + function test_registryAddressSet() public { + assertEq(address(feeHandler.registry()), REGISTRY_ADDRESS); + } + + function test_FeeBeneficiarySet() public { + assertEq(feeHandler.carbonFeeBeneficiary(), EXAMPLE_BENEFICIARY_ADDRESS); + } +} + +contract FeeHandlerTest_SetCarbonFraction is FeeHandlerTest { + event CarbonFractionSet(uint256 fraction); + + function test_Reverts_WhenCallerNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + feeHandler.setCarbonFraction(100); + } + + function test_Reverts_WhenFractionsGreaterThanOne() public { + vm.expectRevert("New cargon fraction can't be greather than 1"); + feeHandler.setCarbonFraction(FixidityLib.newFixedFraction(3, 2).unwrap()); + } + + function test_WhenOtherBeneficiaryWouldAddToOne() public { + feeHandler.addOtherBeneficiary( + op, + (20 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + + feeHandler.setCarbonFraction(FixidityLib.newFixedFraction(8, 10).unwrap()); + } + + function test_setsCarbonFraction() public { + feeHandler.setCarbonFraction(FixidityLib.newFixedFraction(80, 100).unwrap()); + assertEq( + feeHandler.getCarbonFraction(), + FixidityLib.newFixedFraction(80, 100).unwrap(), + "Burn fraction should be set" + ); + } + + function test_ShouldEmitBurnFractionSet() public { + vm.expectEmit(true, true, true, true); + emit CarbonFractionSet(FixidityLib.newFixedFraction(80, 100).unwrap()); + feeHandler.setCarbonFraction(FixidityLib.newFixedFraction(80, 100).unwrap()); + } +} + +contract FeeHandlerTest_SetCarbonFraction_L2 is + FeeHandlerTest_L2, + FeeHandlerTest_SetCarbonFraction +{} + +// TODO change beneficiary allocation +contract FeeHandlerTest_changeOtherBeneficiaryAllocation is FeeHandlerTest { + function setUp() public { + super.setUp(); + feeHandler.addOtherBeneficiary( + op, + (20 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + } + + function test_changedSucsesfully() public { + feeHandler.changeOtherBeneficiaryAllocation(op, (30 * 1e24) / 100); + (uint256 fraction, , ) = feeHandler.getOtherBeneficiariesInfo(op); + assertEq(fraction, (30 * 1e24) / 100); + } + + function test_Reverts_WHenBeneficiaryNotExists() public { + vm.expectRevert("Beneficiary not found"); + feeHandler.changeOtherBeneficiaryAllocation(actor("notExists"), (30 * 1e24) / 100); + } + + function test_Emit() public { + vm.expectEmit(true, true, true, true); + emit BeneficiaryFractionSet(op, (30 * 1e24) / 100); + feeHandler.changeOtherBeneficiaryAllocation(op, (30 * 1e24) / 100); + } + + function test_Reverts_WhenCallerNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + feeHandler.changeOtherBeneficiaryAllocation(op, (30 * 1e24) / 100); + } +} + +contract FeeHandlerTest_changeOtherBeneficiaryAllocation_L2 is + FeeHandlerTest_L2, + FeeHandlerTest_changeOtherBeneficiaryAllocation +{} + +contract FeeHandlerTest_SetHandler is FeeHandlerTest { + function test_Reverts_WhenCallerNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + feeHandler.setHandler(address(stableToken), address(mentoSeller)); + } + + function test_SetsHandler() public { + feeHandler.setHandler(address(stableToken), address(mentoSeller)); + assertEq( + feeHandler.getTokenHandler(address(stableToken)), + address(mentoSeller), + "Handler should be set" + ); + } +} + +contract FeeHandlerTest_SetHandler_L2 is FeeHandlerTest_L2, FeeHandlerTest_SetHandler {} + +contract FeeHandlerTest_AddToken is FeeHandlerTest { + function test_Reverts_WhenCallerNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + feeHandler.addToken(address(stableToken), address(mentoSeller)); + } + + function test_AddsToken() public { + feeHandler.addToken(address(stableToken), address(mentoSeller)); + address[] memory expectedActiveTokens = new address[](1); + expectedActiveTokens[0] = address(stableToken); + assertEq(feeHandler.getActiveTokens(), expectedActiveTokens); + assertTrue(feeHandler.getTokenActive(address(stableToken))); + assertEq(feeHandler.getTokenHandler(address(stableToken)), address(mentoSeller)); + } + + function test_ShouldEmitTokenAdded() public { + vm.expectEmit(true, true, true, true); + emit TokenAdded(address(stableToken), address(mentoSeller)); + feeHandler.addToken(address(stableToken), address(mentoSeller)); + } +} + +contract FeeHandlerTest_AddToken_L2 is FeeHandlerTest_L2, FeeHandlerTest_AddToken {} + +contract FeeHandlerTest_RemoveToken is FeeHandlerTest { + function test_Reverts_WhenCallerNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + feeHandler.removeToken(address(stableToken)); + } + + function test_RemovesToken() public { + feeHandler.addToken(address(stableToken), address(mentoSeller)); + feeHandler.removeToken(address(stableToken)); + assertFalse(feeHandler.getTokenActive(address(stableToken))); + assertEq(feeHandler.getActiveTokens().length, 0); + assertEq(feeHandler.getTokenHandler(address(stableToken)), address(0)); + } + + function test_Emits_TokenRemoved() public { + feeHandler.addToken(address(stableToken), address(mentoSeller)); + vm.expectEmit(true, true, true, true); + emit TokenRemoved(address(stableToken)); + feeHandler.removeToken(address(stableToken)); + } +} + +contract FeeHandlerTest_RemoveToken_L2 is FeeHandlerTest_L2, FeeHandlerTest_RemoveToken {} + +contract FeeHandlerTest_DeactivateAndActivateToken is FeeHandlerTest { + function test_Reverts_WhenActivateCallerNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + feeHandler.deactivateToken(address(stableToken)); + } + + function test_Reverts_WhenDeactivateCallerNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + feeHandler.activateToken(address(stableToken)); + } + + function test_DeactivateAndActivateToken() public { + feeHandler.addToken(address(stableToken), address(mentoSeller)); + feeHandler.deactivateToken(address(stableToken)); + assertFalse(feeHandler.getTokenActive(address(stableToken))); + assertEq(feeHandler.getActiveTokens().length, 0); + + feeHandler.activateToken(address(stableToken)); + assertTrue(feeHandler.getTokenActive(address(stableToken))); + address[] memory expectedActiveTokens = new address[](1); + expectedActiveTokens[0] = address(stableToken); + assertEq(feeHandler.getActiveTokens(), expectedActiveTokens); + } +} + +contract FeeHandlerTest_DeactivateAndActivateToken_L2 is + FeeHandlerTest_L2, + FeeHandlerTest_DeactivateAndActivateToken +{} + +contract FeeHandlerTest_SetFeeBeneficiary is FeeHandlerTest { + function test_Reverts_WhenCallerNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + feeHandler.setCarbonFeeBeneficiary(OTHER_BENEFICIARY_ADDRESS); + } + + function test_ShouldEmitFeeBeneficiarySet() public { + vm.expectEmit(true, true, true, true); + emit FeeBeneficiarySet(OTHER_BENEFICIARY_ADDRESS); + feeHandler.setCarbonFeeBeneficiary(OTHER_BENEFICIARY_ADDRESS); + } + + function test_SetsAddressCorrectly() public { + feeHandler.setCarbonFeeBeneficiary(OTHER_BENEFICIARY_ADDRESS); + assertEq(feeHandler.carbonFeeBeneficiary(), OTHER_BENEFICIARY_ADDRESS); + } +} + +contract FeeHandlerTest_SetFeeBeneficiary_L2 is + FeeHandlerTest_L2, + FeeHandlerTest_SetFeeBeneficiary +{} + +contract FeeHandlerTestAbstract is FeeHandlerTest { + function addAndActivateToken(address token, address handler) public { + feeHandler.addToken(token, handler); + } + + function setCarbonFraction(uint256 numerator, uint256 denominator) internal { + feeHandler.setCarbonFraction(FixidityLib.newFixedFraction(numerator, denominator).unwrap()); + } + + function fundFeeHandlerStable( + uint256 stableAmount, + address stableTokenAddress, + address exchangeAddress + ) internal { + vm.prank(address(exchangeAddress)); + StableToken(stableTokenAddress).mint(address(feeHandler), stableAmount); + } + + function setMaxSlippage(address stableTokenAddress, uint256 slippage) internal { + feeHandler.setMaxSplippage(stableTokenAddress, slippage); + } + + function fundFeeHandlerWithCelo() public { + uint256 celoAmount = 1e18; + celoToken.transfer(address(feeHandler), celoAmount); + } +} + +contract FeeHandlerTestAbstract_L2 is FeeHandlerTest_L2, FeeHandlerTestAbstract {} + +contract FeeHandlerTest_AddOtherBeneficiary is FeeHandlerTestAbstract { + // TODO only owner + function test_addsSucsesfully() public { + feeHandler.addOtherBeneficiary( + op, + (20 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + + assertEq(feeHandler.getOtherBeneficiariesAddresses().length, 1); + (uint256 fraction, string memory name, ) = feeHandler.getOtherBeneficiariesInfo(op); + assertEq(fraction, (20 * 1e24) / 100); + assertEq(name, "OP revenue share"); + } + + function test_SetsWhenBurningFractionWouldBeZero() public { + setCarbonFraction(20, 100); + feeHandler.addOtherBeneficiary( + op, + (80 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + + assertFalse(feeHandler.shouldBurn()); + } + + function test_Reverts_WhenaddingSameTokenTwice() public { + feeHandler.addOtherBeneficiary( + op, + (80 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + vm.expectRevert("Beneficiary already exists"); + feeHandler.addOtherBeneficiary( + op, + (80 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + } + + function test_Reverts_WhenCallerNotOwner() public { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + feeHandler.addOtherBeneficiary( + op, + (80 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + } + + function test_Emmit() public { + vm.expectEmit(true, true, true, true); + emit BeneficiaryFractionSet(op, (80 * 1e24) / 100); + vm.expectEmit(true, true, true, true); + emit BeneficiaryNameSet(op, "OP revenue share"); + vm.expectEmit(true, true, true, true); + emit BeneficiaryAdded(op); + feeHandler.addOtherBeneficiary( + op, + (80 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + } +} + +contract FeeHandlerTest_AddOtherBeneficiary_L2 is + FeeHandlerTestAbstract_L2, + FeeHandlerTest_AddOtherBeneficiary +{} + +contract FeeHandlerTest_Distribute is FeeHandlerTestAbstract { + function setUp() public { + super.setUp(); + setCarbonFraction(20, 100); + setMaxSlippage(address(stableToken), FIXED1); + } + + function test_Reverts_WhenNotActive() public { + vm.expectRevert("Token needs to be active"); + feeHandler.distribute(address(stableToken)); + } + + function test_Reverts_WhenFrozen() public { + addAndActivateToken(address(stableToken), address(mentoSeller)); + freezer.freeze(address(feeHandler)); + vm.expectRevert("can't call when contract is frozen"); + feeHandler.distribute(address(stableToken)); + } + + function test_DoesntDistributeWhenToDistributeIsZero() public { + fundFeeHandlerStable(1e18, address(stableToken), address(exchangeUSD)); + addAndActivateToken(address(stableToken), address(mentoSeller)); + // If we uncomment this the test should fail + // feeHandler.sell(address(stableToken)); + vm.recordLogs(); + feeHandler.distribute(address(stableToken)); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 2); + } + + function test_DoesntDistributeWhenBalanceIsZero() public { + addAndActivateToken(address(stableToken), address(mentoSeller)); + vm.recordLogs(); + feeHandler.distribute(address(stableToken)); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); // TODO figure out why this is 1 and the above is 2 + } + + function test_Distribute() public { + fundFeeHandlerStable(1e18, address(stableToken), address(exchangeUSD)); + addAndActivateToken(address(stableToken), address(mentoSeller)); + feeHandler.sell(address(stableToken)); + + feeHandler.distribute(address(stableToken)); + + assertEq(stableToken.balanceOf(address(feeHandler)), 0); + assertEq(stableToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); + } + + function test_distributesWithoutBurn() public { + fundFeeHandlerStable(1e18, address(stableToken), address(exchangeUSD)); + addAndActivateToken(address(stableToken), address(mentoSeller)); + + feeHandler.distribute(address(stableToken)); + + assertEq(stableToken.balanceOf(address(feeHandler)), 8e17); + assertEq(stableToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); + } + + function test_WhenBurnFractionIsZero() public { + setCarbonFraction(100, 100); + fundFeeHandlerStable(1e18, address(stableToken), address(exchangeUSD)); + addAndActivateToken(address(stableToken), address(mentoSeller)); + + feeHandler.distribute(address(stableToken)); + assertEq(stableToken.balanceOf(address(feeHandler)), 0); + } +} + +contract FeeHandlerTest_Distribute_L2 is FeeHandlerTestAbstract_L2, FeeHandlerTest_Distribute {} + +contract FeeHandlerTest_Distribute_WhenOtherBeneficiaries is FeeHandlerTestAbstract { + function setUp() public { + super.setUp(); + setCarbonFraction(20, 100); + setMaxSlippage(address(stableToken), FIXED1); + fundFeeHandlerStable(1e18, address(stableToken), address(exchangeUSD)); + addAndActivateToken(address(stableToken), address(mentoSeller)); + + feeHandler.addOtherBeneficiary( + op, + (20 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + } + + function test_DistributeOP() public { + feeHandler.sell(address(stableToken)); + + assertEq(stableToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 0); // Make sure the balance is zero at the beginning + feeHandler.distribute(address(stableToken)); + + assertEq(stableToken.balanceOf(address(feeHandler)), 0); + assertEq(stableToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); + assertEq(stableToken.balanceOf(op), 2e17); + } + + function test_DistributeOP_WhenOneMoreBeneficiary() public { + address otherBeneficiary = actor("otherBeneficiary"); + feeHandler.addOtherBeneficiary( + otherBeneficiary, + (30 * 1e24) / 100, // TODO use fixidity + "otherBeneficiary " + ); + + feeHandler.sell(address(stableToken)); + assertEq(stableToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 0); // Make sure the balance is zero at the beginning + feeHandler.distribute(address(stableToken)); + + assertEq(feeHandler.getTotalFractionOfOtherBeneficiariesAndCarbon(), 7e23); + assertEq(feeHandler.getBurnFraction(), 3e23); + + assertApproxEqAbs(stableToken.balanceOf(address(feeHandler)), 0, 10); + assertApproxEqAbs(stableToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17, 1); + assertApproxEqAbs(stableToken.balanceOf(op), 2e17, 1); + assertApproxEqAbs(stableToken.balanceOf(otherBeneficiary), 3e17, 1); + } +} + +contract FeeHandlerTest_Distribute_WhenOtherBeneficiaries_L2 is + FeeHandlerTestAbstract_L2, + FeeHandlerTest_Distribute_WhenOtherBeneficiaries +{} + +contract FeeHandlerTest_BurnCelo is FeeHandlerTestAbstract { + function setUp() public { + super.setUp(); + setCarbonFraction(20, 100); + addAndActivateToken(address(stableToken), address(mentoSeller)); + fundFeeHandlerWithCelo(); + } + + function test_BurnsCorrectly() public { + feeHandler.burnCelo(); + assertEq(celoToken.balanceOf(address(feeHandler)), 2e17); + assertEq(celoToken.getBurnedAmount(), 8e17); + } + + function test_DoesntBurnPendingDistribution() public { + feeHandler.burnCelo(); + assertEq(celoToken.getBurnedAmount(), 8e17); + // this is the amount pending distribution + assertEq(celoToken.balanceOf(address(feeHandler)), 2e17); + + feeHandler.burnCelo(); + assertEq(celoToken.getBurnedAmount(), 8e17); + // amount pending distribution should not be changed by second burn + assertEq(celoToken.balanceOf(address(feeHandler)), 2e17); + } + + function test_DistributesCorrectlyAfterBurn() public { + feeHandler.burnCelo(); + assertEq(celoToken.balanceOf(address(feeHandler)), 2e17); + + feeHandler.distribute(address(celoToken)); + assertEq(celoToken.balanceOf(address(feeHandler)), 0); + assertEq(celoToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); + } +} + +contract FeeHandlerTest_BurnCelo_L2 is FeeHandlerTestAbstract_L2, FeeHandlerTest_BurnCelo {} + +contract FeeHandlerTest_SellMentoTokensAbstract is FeeHandlerTestAbstract { + function setUp() public { + super.setUp(); + setCarbonFraction(20, 100); + setMaxSlippage(address(stableToken), FIXED1); + } +} + +contract FeeHandlerTest_SellMentoTokensAbstract_L2 is + FeeHandlerTestAbstract_L2, + FeeHandlerTest_SellMentoTokensAbstract +{} + +contract FeeHandlerTest_SellMentoTokens_WhenTokenEnabled is FeeHandlerTest_SellMentoTokensAbstract { + function setUp() public { + super.setUp(); + addAndActivateToken(address(stableToken), address(mentoSeller)); + } + + function test_Reverts_WhenFrozen() public { + freezer.freeze(address(feeHandler)); + vm.expectRevert("can't call when contract is frozen"); + feeHandler.sell(address(stableToken)); + } + + function test_WontSellWhenBalanceLow() public { + fundFeeHandlerStable(feeHandler.MIN_BURN(), address(stableToken), address(exchangeUSD)); + uint256 balanceBefore = stableToken.balanceOf(address(feeHandler)); + feeHandler.sell(address(stableToken)); + assertEq(stableToken.balanceOf(address(feeHandler)), balanceBefore); + } + + function resetLimit() internal { + skip(DAY); + } + + function test_ResetSellLimitDaily() public { + fundFeeHandlerStable(3000, address(stableToken), address(exchangeUSD)); + + feeHandler.setDailySellLimit(address(stableToken), 1000); + feeHandler.sell(address(stableToken)); + assertEq(stableToken.balanceOf(address(feeHandler)), 2000); + resetLimit(); + feeHandler.sell(address(stableToken)); + assertEq(stableToken.balanceOf(address(feeHandler)), 1000); + } + + function test_DoesntSellWhenBiggerThanLimit() public { + fundFeeHandlerStable(3000, address(stableToken), address(exchangeUSD)); + feeHandler.setDailySellLimit(address(stableToken), 1000); + feeHandler.sell(address(stableToken)); + assertEq(stableToken.balanceOf(address(feeHandler)), 2000); + // selling again shouldn't do anything + feeHandler.sell(address(stableToken)); + assertEq(stableToken.balanceOf(address(feeHandler)), 2000); + } + + function test_HitLimitDoesntAffectAccounting() public { + fundFeeHandlerStable(3000, address(stableToken), address(exchangeUSD)); + feeHandler.setDailySellLimit(address(stableToken), 1000); + + feeHandler.sell(address(stableToken)); + assertEq(stableToken.balanceOf(address(feeHandler)), 2000); + + assertEq(feeHandler.getTokenToDistribute(address(stableToken)), 600); + + resetLimit(); + + feeHandler.sell(address(stableToken)); + assertEq(feeHandler.getTokenToDistribute(address(stableToken)), 600); + assertEq(stableToken.balanceOf(address(feeHandler)), 1000); + + resetLimit(); + + feeHandler.sell(address(stableToken)); + assertEq(feeHandler.getTokenToDistribute(address(stableToken)), 600); + assertEq(stableToken.balanceOf(address(feeHandler)), 600); + } + + function test_setDistributionAndBurnAmountsDoesntAffectBurn() public { + fundFeeHandlerStable(3000, address(stableToken), address(exchangeUSD)); + feeHandler.setDailySellLimit(address(stableToken), 1000); + + feeHandler.setDistributionAndBurnAmounts(address(stableToken)); + feeHandler.sell(address(stableToken)); + + assertEq(stableToken.balanceOf(address(feeHandler)), 2000); + assertEq(feeHandler.getTokenToDistribute(address(stableToken)), 600); + } + + function test_Sell_WhenOtherTokenHitLimit() public { + fundFeeHandlerStable(3000, address(stableToken), address(exchangeUSD)); + feeHandler.setDailySellLimit(address(stableToken), 1000); + feeHandler.sell(address(stableToken)); + assertEq(stableToken.balanceOf(address(feeHandler)), 2000); + // selling again shouldn't do anything + feeHandler.sell(address(stableToken)); + assertEq(stableToken.balanceOf(address(feeHandler)), 2000); + + // check that the daily limit of one + // doesn't influence the other + uint256 celoAmount = 1e18; + celoToken.approve(address(exchangeEUR), celoAmount); + exchangeEUR.sell(celoAmount, 0, true); + uint256 stableAmount = 3000; + feeHandler.setMaxSplippage(address(stableTokenEUR), FIXED1); + stableTokenEUR.transfer(address(feeHandler), stableAmount); + feeHandler.addToken(address(stableTokenEUR), address(mentoSeller)); + feeHandler.activateToken(address(stableTokenEUR)); + feeHandler.setDailySellLimit(address(stableTokenEUR), 1000); + feeHandler.sell(address(stableTokenEUR)); + + assertEq(stableTokenEUR.balanceOf(address(feeHandler)), 2000); + } + + function test_WhenBurnFractionIsZero() public { + setCarbonFraction(100, 100); + fundFeeHandlerStable(3000, address(stableToken), address(exchangeUSD)); + feeHandler.sell(address(stableToken)); + + assertEq(stableToken.balanceOf(address(feeHandler)), 3000); + } + + function test_SellsWithMento() public { + fundFeeHandlerStable(1e18, address(stableToken), address(exchangeUSD)); + assertEq(feeHandler.getPastBurnForToken(address(stableToken)), 0); + uint256 expectedCeloAmount = exchangeUSD.getBuyTokenAmount(8e17, false); + feeHandler.sell(address(stableToken)); + assertEq(feeHandler.getPastBurnForToken(address(stableToken)), 8e17); + assertEq(stableToken.balanceOf(address(feeHandler)), 2e17); + assertEq(feeHandler.getTokenToDistribute(address(stableToken)), 2e17); + assertEq(feeHandler.getCeloToBeBurned(), expectedCeloAmount); + } + + function test_Reverts_WhenNotEnoughReports() public { + fundFeeHandlerStable(1e18, address(stableToken), address(exchangeUSD)); + mentoSeller.setMinimumReports(address(stableToken), 3); + vm.expectRevert("Number of reports for token not enough"); + feeHandler.sell(address(stableToken)); + } + + function test_DoesntSellBalanceToDistribute() public { + fundFeeHandlerStable(1e18, address(stableToken), address(exchangeUSD)); + feeHandler.sell(address(stableToken)); + uint256 balanceBefore = stableToken.balanceOf(address(feeHandler)); + feeHandler.sell(address(stableToken)); + assertEq(stableToken.balanceOf(address(feeHandler)), balanceBefore); + } +} + +contract FeeHandlerTest_SellMentoTokens_WhenTokenEnabled_L2 is + FeeHandlerTest_SellMentoTokensAbstract_L2, + FeeHandlerTest_SellMentoTokens_WhenTokenEnabled +{} + +contract FeeHandlerTest_SellMentoTokens_WhenTokenNotEnabled is + FeeHandlerTest_SellMentoTokensAbstract +{ + function test_Reverts_WhenSelling() public { + fundFeeHandlerStable(3000, address(stableToken), address(exchangeUSD)); + vm.expectRevert("Token needs to be active to sell"); + feeHandler.sell(address(stableToken)); + } +} + +contract FeeHandlerTest_SellMentoTokens_WhenTokenNotEnabled_L2 is + FeeHandlerTest_SellMentoTokensAbstract_L2, + FeeHandlerTest_SellMentoTokens_WhenTokenNotEnabled +{} + +contract FeeHandlerTest_SellNonMentoTokens is FeeHandlerTestAbstract { + uint256 deadline; + + function setUp() public { + super.setUp(); + setCarbonFraction(20, 100); + setMaxSlippage(address(stableToken), FIXED1); + setMaxSlippage(address(tokenA), FixidityLib.newFixedFraction(99, 100).unwrap()); + addAndActivateToken(address(tokenA), address(uniswapFeeHandlerSeller)); + setUpUniswap(); + setUpOracles(); + } + + function setUpUniswap() public { + uniswapFactory = new MockUniswapV2Factory(address(0)); + bytes32 initCodePairHash = uniswapFactory.INIT_CODE_PAIR_HASH(); + uniswapRouter = new MockUniswapV2Router02( + address(uniswapFactory), + address(0), + initCodePairHash + ); + + uniswapFactory2 = new MockUniswapV2Factory(address(0)); + uniswapRouter2 = new MockUniswapV2Router02( + address(uniswapFactory2), + address(0), + initCodePairHash + ); + uniswapFeeHandlerSeller.initialize(address(registry), new address[](0), new uint256[](0)); + uniswapFeeHandlerSeller.setRouter(address(tokenA), address(uniswapRouter)); + } + + modifier setUpLiquidity(uint256 toMint, uint256 toTransfer) { + deadline = block.timestamp + 100; + tokenA.mint(address(feeHandler), toMint); + tokenA.mint(user, toMint); + celoToken.transfer(user, toMint); + + vm.startPrank(user); + tokenA.approve(address(uniswapRouter), toTransfer); + celoToken.approve(address(uniswapRouter), toTransfer); + uniswapRouter.addLiquidity( + address(tokenA), + address(celoToken), + toTransfer, + toTransfer, + toTransfer, + toTransfer, + user, + deadline + ); + vm.stopPrank(); + _; + } + + function setUpOracles() public { + uniswapFeeHandlerSeller.setMinimumReports(address(tokenA), 1); + mockSortedOracles.setMedianRate(address(tokenA), celoAmountForRate); + mockSortedOracles.setNumRates(address(tokenA), 2); + } + + function test_Reverts_WhenNotEnoughReports() public setUpLiquidity(1e19, 5e18) { + mockSortedOracles.setNumRates(address(tokenA), 0); + vm.expectRevert("Number of reports for token not enough"); + feeHandler.sell(address(tokenA)); + assertEq(tokenA.balanceOf(address(feeHandler)), 1e19); + } + + function test_SellWorksWithReports() public setUpLiquidity(1e19, 5e18) { + feeHandler.sell(address(tokenA)); + assertEq(tokenA.balanceOf(address(feeHandler)), 2e18); + } + + function test_Reverts_WhenOracleSlippageIsHigh() public setUpLiquidity(1e19, 5e18) { + mockSortedOracles.setMedianRate(address(tokenA), 300 * celoAmountForRate); + + vm.expectRevert("UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"); + feeHandler.sell(address(tokenA)); + } + + function test_UniswapTrade() public setUpLiquidity(1e19, 5e18) { + // Make sure our uniswap mock works + uint256 balanceBeforeA = tokenA.balanceOf(user); + uint256 balanceBeforeCelo = celoToken.balanceOf(user); + + vm.startPrank(user); + tokenA.approve(address(uniswapRouter), 1e18); + address[] memory tokenAddresses = new address[](2); + tokenAddresses[0] = address(tokenA); + tokenAddresses[1] = address(celoToken); + uniswapRouter.swapExactTokensForTokens(1e18, 0, tokenAddresses, user, deadline); + vm.stopPrank(); + + // simple directional check + assertGt(balanceBeforeA, tokenA.balanceOf(user)); + assertGt(celoToken.balanceOf(user), balanceBeforeCelo); + } + + function test_SellsNonMentoTokens() public setUpLiquidity(1e19, 5e18) { + assertEq(tokenA.balanceOf(address(feeHandler)), 1e19); + feeHandler.sell(address(tokenA)); + assertEq(tokenA.balanceOf(address(feeHandler)), 2e18); + } + + function test_Reverts_WhenSlippageIsTooHigh() public setUpLiquidity(1e19, 5e18) { + feeHandler.setMaxSplippage(address(tokenA), maxSlippage); + vm.expectRevert("UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"); + feeHandler.sell(address(tokenA)); + assertEq(tokenA.balanceOf(address(feeHandler)), 1e19); + } + + function test_TriesToGetBestRateWithManyExchanges() public setUpLiquidity(2e19, 5e18) { + // Setup second uniswap exchange + uniswapFeeHandlerSeller.setRouter(address(tokenA), address(uniswapRouter2)); + uint256 toTransfer2 = 1e19; // this is higher than toTransfer1 (5e18) set in modifier + vm.startPrank(user); + tokenA.approve(address(uniswapRouter2), toTransfer2); + celoToken.approve(address(uniswapRouter2), toTransfer2); + uniswapRouter2.addLiquidity( + address(tokenA), + address(celoToken), + toTransfer2, + toTransfer2, + toTransfer2, + toTransfer2, + user, + deadline + ); + vm.stopPrank(); + + address[] memory tokenAddresses = new address[](2); + tokenAddresses[0] = address(tokenA); + tokenAddresses[1] = address(celoToken); + + uint256 quote1before = uniswapRouter.getAmountsOut(1e18, tokenAddresses)[1]; + uint256 quote2before = uniswapRouter2.getAmountsOut(1e18, tokenAddresses)[1]; + + // safety check + assertEq(tokenA.balanceOf(address(feeHandler)), 2e19); + + feeHandler.sell(address(tokenA)); + + // Exchange should have occurred on uniswap2 because it has more liquidity. + // After the exchange, uniswap2 has less Celo liquidity than it did before, + // so the quote for tokenA (denominated in Celo) is lower. + uint256 quote1after = uniswapRouter.getAmountsOut(1e18, tokenAddresses)[1]; + uint256 quote2after = uniswapRouter.getAmountsOut(1e18, tokenAddresses)[1]; + assertEq(quote1before, quote1after); // uniswap1 quote should be untouched, since liquidity hasn't changed + assertGt(quote2before, quote2after); // uniswap2 quoute should be lower, since it now has more tokenA per Celo + assertEq(tokenA.balanceOf(address(feeHandler)), 4e18); // check that it burned + } +} + +contract FeeHandlerTest_SellNonMentoTokens_L2 is + FeeHandlerTestAbstract_L2, + FeeHandlerTest_SellNonMentoTokens +{} + +contract FeeHandlerTest_HandleCelo is FeeHandlerTestAbstract { + function setUp() public { + super.setUp(); + setCarbonFraction(20, 100); + fundFeeHandlerWithCelo(); + } + + function test_HandleCelo() public { + feeHandler.handle(address(celoToken)); + assertEq(celoToken.getBurnedAmount(), 8e17); + assertEq(celoToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); + } + + function test_HandleCelo_WhenThereAreMoreBeneficiaries() public { + feeHandler.addOtherBeneficiary( + op, + (20 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + + feeHandler.handle(address(celoToken)); + assertEq(celoToken.getBurnedAmount(), 6e17); + assertEq(celoToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); + assertEq(celoToken.balanceOf(op), 2e17); + } + + function test_HandleCelo_WhenThereAreMoreTwoOtherBeneficiaries() public { + feeHandler.addOtherBeneficiary( + op, + (20 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + address otherBeneficiary = actor("otherBeneficiary"); + feeHandler.addOtherBeneficiary( + otherBeneficiary, + (30 * 1e24) / 100, // TODO use fixidity + "otherBeneficiary " + ); + + assertEq(feeHandler.getTotalFractionOfOtherBeneficiariesAndCarbon(), 7e23); + assertEq(feeHandler.getBurnFraction(), 3e23); + + feeHandler.handle(address(celoToken)); + assertEq(celoToken.getBurnedAmount(), 3e17); + assertApproxEqAbs(celoToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17, 1); + assertApproxEqAbs(celoToken.balanceOf(op), 2e17, 1); + assertApproxEqAbs(celoToken.balanceOf(otherBeneficiary), 3e17, 1); + } +} + +contract FeeHandlerTest_HandleCelo_L2 is FeeHandlerTestAbstract_L2, FeeHandlerTest_HandleCelo {} + +contract FeeHandlerTest_HandleMentoTokens is FeeHandlerTestAbstract { + function setUp() public { + super.setUp(); + setCarbonFraction(20, 100); + setMaxSlippage(address(stableToken), FIXED1); + } + + function test_Reverts_WhenTokenNotAdded() public { + vm.expectRevert("Token needs to be active to sell"); + feeHandler.handle(address(stableToken)); + } + + function test_HandleStable() public { + fundFeeHandlerStable(1e18, address(stableToken), address(exchangeUSD)); + addAndActivateToken(address(stableToken), address(mentoSeller)); + feeHandler.handle(address(stableToken)); + assertEq(feeHandler.getPastBurnForToken(address(stableToken)), 8e17); + assertEq(stableToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); + // Number is not exactly 0.8/2 because of slippage in the Mento exchange + assertEq( + celoToken.balanceOf(address(0x000000000000000000000000000000000000dEaD)), + 398482170620712919 + ); + + assertEq(stableToken.balanceOf(address(feeHandler)), 0); + } +} + +contract FeeHandlerTest_HandleMentoTokens_L2 is + FeeHandlerTestAbstract_L2, + FeeHandlerTest_HandleMentoTokens +{} + +contract FeeHandlerTest_HandleAll is FeeHandlerTestAbstract { + function setUp() public { + super.setUp(); + setCarbonFraction(20, 100); + setMaxSlippage(address(stableToken), FIXED1); + setMaxSlippage(address(stableTokenEUR), FIXED1); + feeHandler.addToken(address(stableToken), address(mentoSeller)); + feeHandler.addToken(address(stableTokenEUR), address(mentoSeller)); + fundFeeHandlerStable(1e18, address(stableToken), address(exchangeUSD)); + fundFeeHandlerStable(1e18, address(stableTokenEUR), address(exchangeEUR)); + } + + function test_BurnsWithMento() public { + uint256 previousCeloBurn = celoToken.getBurnedAmount(); + assertEq(feeHandler.getPastBurnForToken(address(stableToken)), 0); + assertEq(feeHandler.getPastBurnForToken(address(stableTokenEUR)), 0); + + feeHandler.handleAll(); + + assertEq(feeHandler.getPastBurnForToken(address(stableToken)), 8e17); + assertEq(feeHandler.getPastBurnForToken(address(stableTokenEUR)), 8e17); + assertEq(stableToken.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); + assertEq(stableTokenEUR.balanceOf(EXAMPLE_BENEFICIARY_ADDRESS), 2e17); + + // everything should have been burned or distributed + assertEq(feeHandler.getTokenToDistribute(address(stableToken)), 0); + assertEq(feeHandler.getTokenToDistribute(address(stableTokenEUR)), 0); + + // celo burn is non zero + assertTrue(celoToken.getBurnedAmount() > previousCeloBurn); + } +} + +contract FeeHandlerTest_HandleAll_L2 is FeeHandlerTestAbstract_L2, FeeHandlerTest_HandleAll {} + +contract FeeHandlerTest_Transfer is FeeHandlerTest { + modifier mintToken(uint256 amount) { + tokenA.mint(address(feeHandler), amount); + _; + } + + function test_Reverts_WhenCallerNotOwner() public mintToken(1e18) { + vm.prank(user); + vm.expectRevert("Ownable: caller is not the owner"); + feeHandler.transfer(address(tokenA), user, 1e18); + } + + function test_CanTakeFundsOut() public mintToken(1e18) { + feeHandler.transfer(address(tokenA), user, 1e18); + assertEq(tokenA.balanceOf(user), 1e18); + } +} + +contract FeeHandlerTest_Transfer_L2 is FeeHandlerTest_L2, FeeHandlerTest_Transfer {} + +contract FeeHandlerTest_SetDailySellLimit is FeeHandlerTest { + uint256 newCeloAmountForRate; + + function setUp() public { + super.setUp(); + newCeloAmountForRate = celoAmountForRate * 2; + } + + function test_Reverts_WhenCallerNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(user); + feeHandler.setDailySellLimit(address(stableToken), celoAmountForRate); + } + + function test_SetsDailySellLimit() public { + feeHandler.setDailySellLimit(address(stableToken), newCeloAmountForRate); + assertEq(feeHandler.getTokenDailySellLimit(address(stableToken)), newCeloAmountForRate); + } + + function test_Emits_DailyLimitSet() public { + vm.expectEmit(true, true, true, true); + emit DailyLimitSet(address(stableToken), newCeloAmountForRate); + feeHandler.setDailySellLimit(address(stableToken), newCeloAmountForRate); + } +} + +contract FeeHandlerTest_SetDailySellLimit_L2 is + FeeHandlerTest_L2, + FeeHandlerTest_SetDailySellLimit +{} + +contract FeeHandlerTest_SetMaxSlippage is FeeHandlerTest { + uint256 newMaxSlipapge; + + function setUp() public { + super.setUp(); + newMaxSlipapge = maxSlippage * 2; + } + + function test_Reverts_WhenCallerNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(user); + feeHandler.setMaxSplippage(address(stableToken), maxSlippage); + } + + function test_SetsMaxSlippage() public { + feeHandler.setMaxSplippage(address(stableToken), newMaxSlipapge); + assertEq(feeHandler.getTokenMaxSlippage(address(stableToken)), newMaxSlipapge); + } + + function test_Emits_MaxSlippageSet() public { + vm.expectEmit(true, true, true, true); + emit MaxSlippageSet(address(stableToken), maxSlippage); + feeHandler.setMaxSplippage(address(stableToken), maxSlippage); + } +} + +contract FeeHandlerTest_SetMaxSlippage_L2 is FeeHandlerTest_L2, FeeHandlerTest_SetMaxSlippage {} + +contract FeeHandlerTest_RemoveOtherBeneficiary is FeeHandlerTestAbstract { + event BeneficiaryRemoved(address beneficiary); + function setUp() public { + super.setUp(); + feeHandler.addOtherBeneficiary( + op, + (20 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + } + + function test_removedSucsesfully() public { + feeHandler.removeOtherBeneficiary(op); + assertEq(feeHandler.getOtherBeneficiariesAddresses().length, 0); + vm.expectRevert("Beneficiary not found"); + feeHandler.getOtherBeneficiariesInfo(op); + + setCarbonFraction(20, 100); + assertEq( + feeHandler.getTotalFractionOfOtherBeneficiariesAndCarbon(), + 0.2e24, + "Allocation should only be carbon" + ); + } + + function test_Emits_BeneficiaryRemoved() public { + vm.expectEmit(true, true, true, true); + emit BeneficiaryRemoved(op); + feeHandler.removeOtherBeneficiary(op); + } + + function test_Reverts_WhenCallerNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(user); + feeHandler.removeOtherBeneficiary(op); + } +} + +contract FeeHandlerTest_RemoveOtherBeneficiary_L2 is + FeeHandlerTestAbstract_L2, + FeeHandlerTest_RemoveOtherBeneficiary +{} + +contract FeeHandlerTest_SetBeneficiaryFraction is FeeHandlerTestAbstract { + function setUp() public { + super.setUp(); + feeHandler.addOtherBeneficiary( + op, + (20 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + } + + function test_setFractionSucsesfully() public { + feeHandler.setBeneficiaryFraction(op, (30 * 1e24) / 100); + (uint256 fraction, , ) = feeHandler.getOtherBeneficiariesInfo(op); + assertEq(fraction, (30 * 1e24) / 100); + } + + function test_WhenFractionWouldBeZero() public { + feeHandler.setBeneficiaryFraction(op, (80 * 1e24) / 100); + } + + function test_Emits_BeneficiaryFractionSet() public { + vm.expectEmit(true, true, true, true); + emit BeneficiaryFractionSet(op, (30 * 1e24) / 100); + feeHandler.setBeneficiaryFraction(op, (30 * 1e24) / 100); + } + + function test_Reverts_WhenCallerNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(user); + feeHandler.setBeneficiaryFraction(op, (30 * 1e24) / 100); + } +} + +contract FeeHandlerTest_SetBeneficiaryFraction_L2 is + FeeHandlerTestAbstract_L2, + FeeHandlerTest_SetBeneficiaryFraction +{} + +contract FeeHandlerTest_SetBeneficiaryName is FeeHandlerTestAbstract { + function setUp() public { + super.setUp(); + feeHandler.addOtherBeneficiary( + op, + (20 * 1e24) / 100, // TODO use fixidity + "OP revenue share" + ); + } + + function test_setNameSucsesfully() public { + feeHandler.setBeneficiaryName(op, "OP revenue share updated"); + (, string memory name, ) = feeHandler.getOtherBeneficiariesInfo(op); + assertEq(name, "OP revenue share updated"); + } + + function test_Reverts_WhenBeneficiaryNotFound() public { + vm.expectRevert("Beneficiary not found"); + feeHandler.setBeneficiaryName(actor("otherBeneficiary"), "OP revenue share updated"); + } + + function test_Emits_BeneficiaryNameSet() public { + vm.expectEmit(true, true, true, true); + emit BeneficiaryNameSet(op, "OP revenue share updated"); + feeHandler.setBeneficiaryName(op, "OP revenue share updated"); + } + + function test_Reverts_WhenCallerNotOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(user); + feeHandler.setBeneficiaryName(op, "OP revenue share updated"); + } +} + +contract FeeHandlerTest_SetBeneficiaryName_L2 is + FeeHandlerTestAbstract_L2, + FeeHandlerTest_SetBeneficiaryName +{} diff --git a/packages/protocol/test-sol/unit/common/FeeHandlerSeller.t.sol b/packages/protocol/test-sol/unit/common/FeeHandlerSeller.t.sol new file mode 100644 index 00000000000..235fae18f01 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/FeeHandlerSeller.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; +pragma experimental ABIEncoderV2; + +// Helper contracts + +import { CeloTokenMock } from "@test-sol/unit/common/CeloTokenMock.sol"; +import { FeeHandlerSeller } from "@celo-contracts/common/FeeHandlerSeller.sol"; +import { MentoFeeHandlerSeller } from "@celo-contracts/common/MentoFeeHandlerSeller.sol"; +import { UniswapFeeHandlerSeller } from "@celo-contracts/common/UniswapFeeHandlerSeller.sol"; + +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; +import "@test-sol/utils/WhenL2.sol"; + +contract FeeHandlerSellerTest is TestWithUtils { + // Actors + address RECEIVER_ADDRESS = actor("Arbitrary Receiver"); + address NON_OWNER_ADDRESS = actor("Arbitrary Non-Owner"); + + // Contract instances + CeloTokenMock celoToken; // Using mock token to work around missing transfer precompile + FeeHandlerSeller mentoFeeHandlerSeller; + FeeHandlerSeller uniswapFeeHandlerSeller; + + address oracle; + address sortedOracles; + + // Helper data structures + FeeHandlerSeller[] feeHandlerSellerInstances; + + event OracleAddressSet(address _token, address _oracle); + + function setUp() public { + super.setUp(); + + celoToken = new CeloTokenMock(); + oracle = actor("oracle"); + sortedOracles = actor("sortedOracles"); + + registry.setAddressFor("SortedOracles", sortedOracles); + + mentoFeeHandlerSeller = new MentoFeeHandlerSeller(true); + uniswapFeeHandlerSeller = new UniswapFeeHandlerSeller(true); + + feeHandlerSellerInstances.push(mentoFeeHandlerSeller); + feeHandlerSellerInstances.push(uniswapFeeHandlerSeller); + } +} + +contract FeeHandlerSellerTest_L2 is WhenL2, FeeHandlerSellerTest {} + +contract FeeHandlerSellerTest_Transfer is FeeHandlerSellerTest { + uint256 constant ZERO_CELOTOKEN = 0; + uint256 constant ONE_CELOTOKEN = 1e18; + + function test_FeeHandlerSeller_ShouldTransfer_WhenCalledByOwner() public { + for (uint256 i = 0; i < feeHandlerSellerInstances.length; i++) { + celoToken.setBalanceOf(RECEIVER_ADDRESS, ZERO_CELOTOKEN); // Reset balance of receiver + assertEq( + celoToken.balanceOf(RECEIVER_ADDRESS), + ZERO_CELOTOKEN, + "Balance of receiver should be 0 at start" + ); + celoToken.setBalanceOf(address(feeHandlerSellerInstances[i]), ONE_CELOTOKEN); // Faucet contract + assertEq( + celoToken.balanceOf(address(feeHandlerSellerInstances[i])), + ONE_CELOTOKEN, + "Balance of contract should be 1 at start" + ); + + vm.prank(feeHandlerSellerInstances[i].owner()); + feeHandlerSellerInstances[i].transfer(address(celoToken), ONE_CELOTOKEN, RECEIVER_ADDRESS); + + assertEq( + celoToken.balanceOf(RECEIVER_ADDRESS), + ONE_CELOTOKEN, + "Balance of receiver should be 1 after transfer" + ); + assertEq( + celoToken.balanceOf(address(feeHandlerSellerInstances[i])), + ZERO_CELOTOKEN, + "Balance of contract should be 0 after transfer" + ); + } + } + + function test_FeeHandlerSeller_ShouldRevert_WhenCalledByNonOwner() public { + for (uint256 i = 0; i < feeHandlerSellerInstances.length; i++) { + vm.prank(NON_OWNER_ADDRESS); + + vm.expectRevert("Ownable: caller is not the owner"); + feeHandlerSellerInstances[i].transfer(address(celoToken), ONE_CELOTOKEN, RECEIVER_ADDRESS); + } + } +} + +contract FeeHandlerSellerTest_Transfer_L2 is + FeeHandlerSellerTest_L2, + FeeHandlerSellerTest_Transfer +{} + +contract FeeHandlerSellerTest_SetMinimumReports is FeeHandlerSellerTest { + address ARBITRARY_TOKEN_ADDRESS = actor("Arbitrary Token Address"); + uint256 constant ARBITRARY_NR_OF_MINIMUM_REPORTS = 15; + + function test_SetMinimumReports_ShouldSucceedWhen_CalledByOwner() public { + for (uint256 i = 0; i < feeHandlerSellerInstances.length; i++) { + vm.prank(feeHandlerSellerInstances[i].owner()); + + feeHandlerSellerInstances[i].setMinimumReports( + ARBITRARY_TOKEN_ADDRESS, + ARBITRARY_NR_OF_MINIMUM_REPORTS + ); + + assertEq( + feeHandlerSellerInstances[i].minimumReports(ARBITRARY_TOKEN_ADDRESS), + ARBITRARY_NR_OF_MINIMUM_REPORTS, + "Number of minimum reports don't match" + ); + } + } + + function test_SetMinimumReports_ShouldRevertWhen_CalledByNonOwner() public { + for (uint256 i = 0; i < feeHandlerSellerInstances.length; i++) { + vm.prank(NON_OWNER_ADDRESS); + + vm.expectRevert("Ownable: caller is not the owner"); + feeHandlerSellerInstances[i].setMinimumReports( + ARBITRARY_TOKEN_ADDRESS, + ARBITRARY_NR_OF_MINIMUM_REPORTS + ); + } + } +} + +contract FeeHandlerSellerTest_SetMinimumReports_L2 is + FeeHandlerSellerTest_L2, + FeeHandlerSellerTest_SetMinimumReports +{} + +contract FeeHandlerSellerTest_setOracleAddress is FeeHandlerSellerTest { + function test_Reverts_WhenCalledByNonOwner() public { + vm.prank(NON_OWNER_ADDRESS); + vm.expectRevert("Ownable: caller is not the owner"); + uniswapFeeHandlerSeller.setOracleAddress(address(celoToken), oracle); + } + + function test_SetsCorrectly() public { + uniswapFeeHandlerSeller.setOracleAddress(address(celoToken), oracle); + assertEq(uniswapFeeHandlerSeller.getOracleAddress(address(celoToken)), oracle); + } + + function test_DefaultIsSortedOracles() public { + uniswapFeeHandlerSeller.initialize(REGISTRY_ADDRESS, new address[](0), new uint256[](0)); + assertEq(uniswapFeeHandlerSeller.getOracleAddress(address(celoToken)), sortedOracles); + } + + function test_Emits_OracleAddressSet() public { + vm.expectEmit(false, false, false, true); + emit OracleAddressSet(address(celoToken), oracle); + uniswapFeeHandlerSeller.setOracleAddress(address(celoToken), oracle); + } +} + +contract FeeHandlerSellerTest_setOracleAddress_L2 is + FeeHandlerSellerTest_L2, + FeeHandlerSellerTest_setOracleAddress +{} diff --git a/packages/protocol/test-sol/common/Fixidity.t.sol b/packages/protocol/test-sol/unit/common/Fixidity.t.sol similarity index 100% rename from packages/protocol/test-sol/common/Fixidity.t.sol rename to packages/protocol/test-sol/unit/common/Fixidity.t.sol diff --git a/packages/protocol/test-sol/common/GasPriceMinimum.t.sol b/packages/protocol/test-sol/unit/common/GasPriceMinimum.t.sol similarity index 74% rename from packages/protocol/test-sol/common/GasPriceMinimum.t.sol rename to packages/protocol/test-sol/unit/common/GasPriceMinimum.t.sol index dfb55121d78..8a732e34bd1 100644 --- a/packages/protocol/test-sol/common/GasPriceMinimum.t.sol +++ b/packages/protocol/test-sol/unit/common/GasPriceMinimum.t.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.7 <0.8.20; import "celo-foundry-8/Test.sol"; +import { TestConstants } from "@test-sol/constants.sol"; import "@celo-contracts/common/FixidityLib.sol"; @@ -11,7 +12,7 @@ import "@celo-contracts/stability/test/MockSortedOracles.sol"; import "@celo-contracts-8/common/GasPriceMinimum.sol"; -contract GasPriceMinimumTest is Test { +contract GasPriceMinimumTest is Test, TestConstants { using FixidityLib for FixidityLib.Fraction; IRegistry registry; @@ -27,8 +28,6 @@ contract GasPriceMinimumTest is Test { FixidityLib.Fraction targetDensityFraction = FixidityLib.newFixedFraction(5, 10); uint256 adjustmentSpeed = FixidityLib.newFixedFraction(5, 10).unwrap(); FixidityLib.Fraction adjustmentSpeedFraction = FixidityLib.newFixedFraction(5, 10); - address registryAddress = 0x000000000000000000000000000000000000ce10; - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; event TargetDensitySet(uint256 targetDensity); event GasPriceMinimumFloorSet(uint256 gasPriceMinimumFloor); @@ -41,7 +40,7 @@ contract GasPriceMinimumTest is Test { nonOwner = actor("nonOwner"); celoToken = actor("CeloToken"); - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); // deployCodeTo("SortedOracles.sol", abi.encode(true), sortedOracleAddress); // fails with `data did not match any variant of untagged enum Bytecode at line 822 column 3]` @@ -49,14 +48,14 @@ contract GasPriceMinimumTest is Test { gasPriceMinimum = new GasPriceMinimum(true); - registry = IRegistry(registryAddress); + registry = IRegistry(REGISTRY_ADDRESS); registry.setAddressFor("GasPriceMinimum", address(gasPriceMinimum)); registry.setAddressFor("SortedOracles", address(sortedOracles)); registry.setAddressFor("GoldToken", celoToken); gasPriceMinimum.initialize( - registryAddress, + REGISTRY_ADDRESS, gasPriceMinimumFloor, targetDensity, adjustmentSpeed, @@ -65,7 +64,7 @@ contract GasPriceMinimumTest is Test { } function _whenL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); } } @@ -89,7 +88,7 @@ contract GasPriceMinimumTest_initialize is GasPriceMinimumTest { function test_shouldRevertWhenCalledAgain() public { vm.expectRevert("contract already initialized"); gasPriceMinimum.initialize( - registryAddress, + REGISTRY_ADDRESS, gasPriceMinimumFloor, targetDensity, adjustmentSpeed, @@ -200,7 +199,7 @@ contract GasPriceMinimumTest_setGasPriceMinimumFloor is GasPriceMinimumTest { } } -contract GasPriceMinimumTest_setUpdatedGasPriceMinimum is GasPriceMinimumTest { +contract GasPriceMinimumTest_getUpdatedGasPriceMinimum is GasPriceMinimumTest { using FixidityLib for FixidityLib.Fraction; uint256 nonce = 0; @@ -298,4 +297,95 @@ contract GasPriceMinimumTest_setUpdatedGasPriceMinimum is GasPriceMinimumTest { assertEq(actualUpdatedGasPriceMinimum, expectedUpdatedGasPriceMinimum); } } + + function test_shouldRevert_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + gasPriceMinimum.getUpdatedGasPriceMinimum(0, 1); + } +} + +contract GasPriceMinimumTest_gasPriceMinimumFloor is GasPriceMinimumTest { + function test_shouldReturnTheGasPriceMinimumFloor() public { + uint256 gasPriceMinFloor = gasPriceMinimum.gasPriceMinimumFloor(); + assertEq(gasPriceMinFloor, gasPriceMinimumFloor); + } + + function test_shouldRevert_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + gasPriceMinimum.gasPriceMinimumFloor(); + } +} + +contract GasPriceMinimumTest_targetDensity is GasPriceMinimumTest { + function test_shouldReturnTheTargetDensity() public { + uint256 realTargetDensity = gasPriceMinimum.targetDensity(); + assertEq(realTargetDensity, targetDensity); + } + + function test_shouldRevert_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + gasPriceMinimum.targetDensity(); + } +} + +contract GasPriceMinimumTest_adjustmentSpeed is GasPriceMinimumTest { + function test_shouldReturnTheAdjustementSpeed() public { + uint256 realAdjustementSpeed = gasPriceMinimum.adjustmentSpeed(); + assertEq(realAdjustementSpeed, adjustmentSpeed); + } + + function test_shouldRevert_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + gasPriceMinimum.adjustmentSpeed(); + } +} + +contract GasPriceMinimumTest_baseFeeOpCodeActivationBlock is GasPriceMinimumTest { + uint256 baseFeeOpCodeActivationBlock = 123; + + function setUp() public override { + super.setUp(); + gasPriceMinimum.setBaseFeeOpCodeActivationBlock(baseFeeOpCodeActivationBlock); + } + + function test_shouldReturnTheBaseFeeOpCodeActivationBlock() public { + uint256 realBaseFeeOpCodeActivationBlock = gasPriceMinimum.baseFeeOpCodeActivationBlock(); + assertEq(realBaseFeeOpCodeActivationBlock, baseFeeOpCodeActivationBlock); + } + + function test_shouldRevert_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + gasPriceMinimum.baseFeeOpCodeActivationBlock(); + } +} + +contract GasPriceMinimumTest_gasPriceMinimum is GasPriceMinimumTest { + function test_shouldReturnTheGasPriceMinimum() public { + uint256 realGasPriceMinimum = gasPriceMinimum.gasPriceMinimum(); + assertEq(realGasPriceMinimum, 100); + } + + function test_shouldRevert_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + gasPriceMinimum.gasPriceMinimum(); + } +} + +contract GasPriceMinimumTest_getGasPriceMinimum is GasPriceMinimumTest { + function test_shouldReturnTheGasPriceMinimum() public { + uint256 realGasPriceMinimum = gasPriceMinimum.getGasPriceMinimum(address(0)); + assertEq(realGasPriceMinimum, 100); + } + + function test_shouldRevert_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + gasPriceMinimum.getGasPriceMinimum(address(0)); + } } diff --git a/packages/protocol/test-sol/unit/common/GoldToken.t.sol b/packages/protocol/test-sol/unit/common/GoldToken.t.sol new file mode 100644 index 00000000000..8d3cbf9cb64 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/GoldToken.t.sol @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; + +import "@celo-contracts/common/GoldToken.sol"; + +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; +import "@test-sol/utils/WhenL2.sol"; + +contract CeloTokenTest is TestWithUtils { + GoldToken celoToken; + + uint256 constant ONE_CELOTOKEN = 1000000000000000000; + address receiver; + address sender; + address randomAddress; + address celoTokenOwner; + address celoUnreleasedTreasuryAddress; + + event Transfer(address indexed from, address indexed to, uint256 value); + event TransferComment(string comment); + + function setUp() public { + super.setUp(); + celoTokenOwner = actor("celoTokenOwner"); + celoUnreleasedTreasuryAddress = actor("celoUnreleasedTreasury"); + deployCodeTo("CeloUnreleasedTreasury.sol", abi.encode(false), celoUnreleasedTreasuryAddress); + + vm.prank(celoTokenOwner); + celoToken = new GoldToken(true); + vm.prank(celoTokenOwner); + celoToken.setRegistry(REGISTRY_ADDRESS); + registry.setAddressFor(CeloUnreleasedTreasuryContract, celoUnreleasedTreasuryAddress); + receiver = actor("receiver"); + sender = actor("sender"); + randomAddress = actor("random"); + + vm.prank(address(0)); + celoToken.mint(receiver, ONE_CELOTOKEN); // Increase total supply. + vm.prank(address(0)); + celoToken.mint(sender, ONE_CELOTOKEN); + vm.prank(address(0)); + celoToken.mint(randomAddress, L1_MINTED_CELO_SUPPLY - (2 * ONE_CELOTOKEN)); // Increase total supply. + + vm.deal(receiver, ONE_CELOTOKEN); + vm.deal(sender, ONE_CELOTOKEN); + vm.deal(randomAddress, L1_MINTED_CELO_SUPPLY - (2 * ONE_CELOTOKEN)); // Increases balance. + + // This step is required, as `vm.prank` funds the address, + // and causes a safeMath overflow when getting the circulating supply. + vm.deal(address(0), 0); + } +} + +contract CeloTokenTest_PreL2 is CeloTokenTest { + function setUp() public { + super.setUp(); + + vm.prank(address(0)); + celoToken.mint(celoUnreleasedTreasuryAddress, L2_INITIAL_STASH_BALANCE); + vm.deal(celoUnreleasedTreasuryAddress, L2_INITIAL_STASH_BALANCE); + + vm.deal(address(0), 0); + } +} +contract CeloTokenTest_L2 is CeloTokenTest_PreL2, WhenL2 {} + +contract CeloTokenTest_general is CeloTokenTest { + function test_name() public { + assertEq(celoToken.name(), "Celo native asset"); + } + + function test_symbol() public { + assertEq(celoToken.symbol(), "CELO"); + } + + function test_decimals() public { + assertEq(uint256(celoToken.decimals()), 18); + } + + function test_balanceOf() public { + assertEq(celoToken.balanceOf(receiver), receiver.balance); + } + + function test_approve() public { + vm.prank(sender); + celoToken.approve(receiver, ONE_CELOTOKEN); + assertEq(celoToken.allowance(sender, receiver), ONE_CELOTOKEN); + } + + function test_increaseAllowance() public { + vm.prank(sender); + celoToken.increaseAllowance(receiver, ONE_CELOTOKEN); + vm.prank(sender); + celoToken.increaseAllowance(receiver, ONE_CELOTOKEN); + assertEq(celoToken.allowance(sender, receiver), ONE_CELOTOKEN * 2); + } + + function test_decreaseAllowance() public { + vm.prank(sender); + celoToken.approve(receiver, ONE_CELOTOKEN * 2); + vm.prank(sender); + celoToken.decreaseAllowance(receiver, ONE_CELOTOKEN); + assertEq(celoToken.allowance(sender, receiver), ONE_CELOTOKEN); + } + + function test_allowance() public { + vm.prank(sender); + celoToken.approve(receiver, ONE_CELOTOKEN); + assertEq(celoToken.allowance(sender, receiver), ONE_CELOTOKEN); + } +} + +contract CeloTokenTest_general_L2 is CeloTokenTest_L2, CeloTokenTest_general {} + +contract CeloTokenTest_transfer is CeloTokenTest { + function setUp() public { + super.setUp(); + } + + function test_ShouldTransferBalanceFromOneUserToAnother() public { + uint256 startBalanceFrom = celoToken.balanceOf(sender); + uint256 startBalanceTo = celoToken.balanceOf(receiver); + vm.prank(sender); + celoToken.transfer(receiver, ONE_CELOTOKEN); + assertEq(sender.balance, startBalanceFrom - ONE_CELOTOKEN); + assertEq(receiver.balance, startBalanceTo + ONE_CELOTOKEN); + } + + function test_ShouldTransferBalanceWithAComment() public { + string memory comment = "tacos at lunch"; + uint256 startBalanceFrom = celoToken.balanceOf(sender); + uint256 startBalanceTo = celoToken.balanceOf(receiver); + vm.prank(sender); + vm.expectEmit(true, true, true, true); + emit Transfer(sender, receiver, ONE_CELOTOKEN); + vm.expectEmit(true, true, true, true); + emit TransferComment(comment); + celoToken.transferWithComment(receiver, ONE_CELOTOKEN, comment); + assertEq(sender.balance, startBalanceFrom - ONE_CELOTOKEN); + assertEq(receiver.balance, startBalanceTo + ONE_CELOTOKEN); + } + + function test_ShouldNotAllowToTransferToNullAddress() public { + vm.prank(sender); + vm.expectRevert(); + celoToken.transfer(address(0), ONE_CELOTOKEN); + } + + function test_Succeeds_whenTransferingToCeloUnreleasedTreasury() public { + vm.prank(sender); + uint256 balanceBefore = celoToken.balanceOf(celoUnreleasedTreasuryAddress); + + celoToken.transfer(celoUnreleasedTreasuryAddress, ONE_CELOTOKEN); + uint256 balanceAfter = celoToken.balanceOf(celoUnreleasedTreasuryAddress); + assertGt(balanceAfter, balanceBefore); + } + + function test_FailsWhenNativeTransferingToCeloUnreleasedTreasury() public payable { + (bool success, ) = address(uint160(celoUnreleasedTreasuryAddress)).call.value(ONE_CELOTOKEN)( + "" + ); + + assertFalse(success); + + bool sent = address(uint160(celoUnreleasedTreasuryAddress)).send(ONE_CELOTOKEN); + assertFalse(sent); + } +} + +contract CeloTokenTest_transfer_L2 is CeloTokenTest_L2, CeloTokenTest_transfer {} + +contract CeloTokenTest_transferFrom is CeloTokenTest { + function setUp() public { + super.setUp(); + vm.prank(sender); + celoToken.approve(receiver, ONE_CELOTOKEN); + } + + function test_ShouldTransferBalanceFromOneUserToAnother() public { + uint256 startBalanceFrom = celoToken.balanceOf(sender); + uint256 startBalanceTo = celoToken.balanceOf(receiver); + vm.prank(receiver); + celoToken.transferFrom(sender, receiver, ONE_CELOTOKEN); + assertEq(sender.balance, startBalanceFrom - ONE_CELOTOKEN); + assertEq(receiver.balance, startBalanceTo + ONE_CELOTOKEN); + } + + function test_Reverts_WhenTransferToNullAddress() public { + vm.prank(receiver); + vm.expectRevert(); + celoToken.transferFrom(sender, address(0), ONE_CELOTOKEN); + } + + function test_Succeeds_whenTransferingToCeloUnreleasedTreasury() public { + uint256 balanceBefore = celoToken.balanceOf(celoUnreleasedTreasuryAddress); + vm.prank(receiver); + celoToken.transferFrom(sender, celoUnreleasedTreasuryAddress, ONE_CELOTOKEN); + uint256 balanceAfter = celoToken.balanceOf(celoUnreleasedTreasuryAddress); + assertGt(balanceAfter, balanceBefore); + } + + function test_Reverts_WhenTransferMoreThanSenderHas() public { + uint256 value = sender.balance + ONE_CELOTOKEN * 4; + + vm.prank(receiver); + vm.expectRevert(); + celoToken.transferFrom(sender, receiver, value); + } + + function test_Reverts_WhenTransferringMoreThanTheSpenderIsAllowed() public { + vm.prank(receiver); + vm.expectRevert(); + celoToken.transferFrom(sender, receiver, ONE_CELOTOKEN + 1); + } +} + +contract CeloTokenTest_transferFrom_L2 is CeloTokenTest_L2, CeloTokenTest_transferFrom {} + +contract CeloTokenTest_burn is CeloTokenTest { + uint256 startBurn; + address burnAddress = address(0x000000000000000000000000000000000000dEaD); + + function setUp() public { + super.setUp(); + startBurn = celoToken.getBurnedAmount(); + } + + function test_burn_address_starts_with_zero_balance() public { + assertEq(celoToken.balanceOf(burnAddress), 0); + } + + function test_burn_starts_as_start_burn_amount() public { + assertEq(celoToken.getBurnedAmount(), startBurn); + } + + function test_burn_amount_eq_the_balance_of_the_burn_address() public { + assertEq(celoToken.getBurnedAmount(), celoToken.balanceOf(burnAddress)); + } + + function test_returns_right_burn_amount() public { + celoToken.burn(ONE_CELOTOKEN); + assertEq(celoToken.getBurnedAmount(), ONE_CELOTOKEN + startBurn); + } +} + +contract CeloTokenTest_burn_L2 is CeloTokenTest_L2, CeloTokenTest_burn {} + +contract CeloTokenTest_mint is CeloTokenTest { + function test_Reverts_whenCalledByOtherThanVm() public { + vm.prank(celoTokenOwner); + vm.expectRevert("Only VM can call"); + celoToken.mint(receiver, ONE_CELOTOKEN); + + vm.prank(celoUnreleasedTreasuryAddress); + vm.expectRevert("Only VM can call"); + celoToken.mint(receiver, ONE_CELOTOKEN); + } + + function test_Should_increaseCeloTokenTotalSupplyWhencalledByVm() public { + uint256 celoTokenSupplyBefore = celoToken.totalSupply(); + vm.prank(address(0)); + celoToken.mint(receiver, ONE_CELOTOKEN); + uint256 celoTokenSupplyAfter = celoToken.totalSupply(); + assertGt(celoTokenSupplyAfter, celoTokenSupplyBefore); + } + + function test_Emits_TransferEvent() public { + vm.prank(address(0)); + vm.expectEmit(true, true, true, true); + emit Transfer(address(0), receiver, ONE_CELOTOKEN); + celoToken.mint(receiver, ONE_CELOTOKEN); + } +} + +contract CeloTokenTest_mint_L2 is CeloTokenTest_L2 { + function test_Reverts_whenL2() public { + vm.expectRevert("This method is no longer supported in L2."); + vm.prank(celoUnreleasedTreasuryAddress); + celoToken.mint(receiver, ONE_CELOTOKEN); + vm.expectRevert("This method is no longer supported in L2."); + vm.prank(address(0)); + celoToken.mint(receiver, ONE_CELOTOKEN); + } +} + +contract CeloTokenTest_increaseSupply is CeloTokenTest { + function test_ShouldIncreaseTotalSupply() public { + uint256 celoTokenSupplyBefore = celoToken.totalSupply(); + vm.prank(address(0)); + celoToken.increaseSupply(ONE_CELOTOKEN); + uint256 celoTokenSupplyAfter = celoToken.totalSupply(); + assertGt(celoTokenSupplyAfter, celoTokenSupplyBefore); + } + + function test_Reverts_WhenCalledByOtherThanVm() public { + vm.prank(celoTokenOwner); + vm.expectRevert("Only VM can call"); + celoToken.increaseSupply(ONE_CELOTOKEN); + } +} + +contract CeloTokenTest_increaseSupply_L2 is CeloTokenTest_L2 { + function test_Reverts_WhenL2() public { + vm.prank(celoTokenOwner); + vm.expectRevert("This method is no longer supported in L2."); + celoToken.increaseSupply(ONE_CELOTOKEN); + } +} + +contract CeloTokenTest_circulatingSupply is CeloTokenTest { + function test_ShouldMatchCirculatingSupply_WhenNoBurn() public { + assertEq(celoToken.circulatingSupply(), celoToken.allocatedSupply()); + assertEq(celoToken.circulatingSupply(), L1_MINTED_CELO_SUPPLY); + } + + function test_ShouldDecreaseCirculatingSupply_WhenThereWasBurn() public { + vm.prank(randomAddress); + celoToken.burn(ONE_CELOTOKEN); + assertEq(celoToken.circulatingSupply(), L1_MINTED_CELO_SUPPLY - ONE_CELOTOKEN); + assertEq(celoToken.circulatingSupply(), celoToken.allocatedSupply() - ONE_CELOTOKEN); + } +} + +contract CeloTokenTest_circulatingSupply_L2 is CeloTokenTest_L2, CeloTokenTest_circulatingSupply { + function test_ShouldBeLessThanTheTotalSupply() public { + assertLt(celoToken.circulatingSupply(), celoToken.totalSupply()); + } +} + +contract CeloTokenTest_AllocatedSupply is CeloTokenTest { + function test_ShouldReturnTotalSupply() public { + assertEq(celoToken.allocatedSupply(), L1_MINTED_CELO_SUPPLY); + assertEq(celoToken.allocatedSupply(), celoToken.totalSupply()); + } +} + +contract CeloTokenTest_AllocatedSupply_L2 is CeloTokenTest_L2 { + function test_ShouldReturnTotalSupplyMinusCeloUnreleasedTreasuryBalance() public { + assertEq(celoToken.allocatedSupply(), CELO_SUPPLY_CAP - L2_INITIAL_STASH_BALANCE); + assertEq(celoToken.allocatedSupply(), celoToken.totalSupply() - L2_INITIAL_STASH_BALANCE); + } + + function test_ShouldReturnTotalSupplyWhenCeloUnreleasedTreasuryHasReleasedAllBalance() public { + deal(celoUnreleasedTreasuryAddress, 0); + assertEq(celoToken.allocatedSupply(), celoToken.totalSupply()); + } +} + +contract CeloTokenTest_TotalSupply is CeloTokenTest { + function test_ShouldReturnL1MintedSupply() public { + assertEq(celoToken.totalSupply(), L1_MINTED_CELO_SUPPLY); + } +} + +contract CeloTokenTest_TotalSupply_L2 is CeloTokenTest_L2 { + function test_ShouldReturnSupplyCap_WhenL2() public { + assertEq(celoToken.totalSupply(), CELO_SUPPLY_CAP); + } +} diff --git a/packages/protocol/test-sol/common/Heap.t.sol b/packages/protocol/test-sol/unit/common/Heap.t.sol similarity index 100% rename from packages/protocol/test-sol/common/Heap.t.sol rename to packages/protocol/test-sol/unit/common/Heap.t.sol diff --git a/packages/protocol/test-sol/unit/common/Import05.t.sol b/packages/protocol/test-sol/unit/common/Import05.t.sol new file mode 100644 index 00000000000..48be42e45fb --- /dev/null +++ b/packages/protocol/test-sol/unit/common/Import05.t.sol @@ -0,0 +1,8 @@ +pragma solidity ^0.5.13; + +// this file only exists so that foundry compiles this contracts +import "@mento-core/contracts/StableToken.sol"; +import "@mento-core/contracts/StableTokenBRL.sol"; +import "@mento-core/contracts/StableTokenEUR.sol"; + +contract Import05 {} diff --git a/packages/protocol/test-sol/unit/common/ImportPrecompiles.t.sol b/packages/protocol/test-sol/unit/common/ImportPrecompiles.t.sol new file mode 100644 index 00000000000..a976098c20a --- /dev/null +++ b/packages/protocol/test-sol/unit/common/ImportPrecompiles.t.sol @@ -0,0 +1,9 @@ +pragma solidity >=0.8.7 <0.8.20; + +// this file only exists so that foundry compiles this contracts +import "@test-sol/precompiles/ProofOfPossesionPrecompile.sol"; +import "@test-sol/precompiles/EpochSizePrecompile.sol"; +import "@test-sol/precompiles/NumberValidatorsInCurrentSetPrecompile.sol"; +import "@test-sol/precompiles/ValidatorSignerAddressFromCurrentSetPrecompile.sol"; + +contract ImportPrecompiles {} diff --git a/packages/protocol/test-sol/common/IsL2Check.t.sol b/packages/protocol/test-sol/unit/common/IsL2Check.t.sol similarity index 70% rename from packages/protocol/test-sol/common/IsL2Check.t.sol rename to packages/protocol/test-sol/unit/common/IsL2Check.t.sol index fece179a17d..d68069dd341 100644 --- a/packages/protocol/test-sol/common/IsL2Check.t.sol +++ b/packages/protocol/test-sol/unit/common/IsL2Check.t.sol @@ -2,19 +2,24 @@ pragma solidity >=0.8.7 <0.8.20; import "celo-foundry-8/Test.sol"; +import { TestConstants } from "@test-sol/constants.sol"; // Contract to test import "@celo-contracts-8/common/IsL2Check.sol"; contract IsL2Test is IsL2Check { + // example function function onlyL1Function() public view onlyL1 returns (bool) { return true; } -} -contract IsL2CheckBase is Test { - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; + // expose the function for testing + function isL2_public() public view returns (bool) { + return super.isL2(); + } +} +contract IsL2CheckBase is Test, TestConstants { IsL2Test isL2Check; function setUp() public virtual { @@ -22,23 +27,23 @@ contract IsL2CheckBase is Test { } function helper_WhenProxyAdminAddressIsSet() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); } } contract IsL2Check_IsL2Test is IsL2CheckBase { function test_IsL2() public { - assertFalse(isL2Check.isL2()); + assertFalse(isL2Check.isL2_public()); } function test_IsL2_WhenProxyAdminSet() public { helper_WhenProxyAdminAddressIsSet(); - assertTrue(isL2Check.isL2()); + assertTrue(isL2Check.isL2_public()); } function test_IsL1_WhenProxyAdminSet() public { helper_WhenProxyAdminAddressIsSet(); - assertFalse(!isL2Check.isL2()); + assertFalse(!isL2Check.isL2_public()); } } diff --git a/packages/protocol/test-sol/common/LinkedList.t.sol b/packages/protocol/test-sol/unit/common/LinkedList.t.sol similarity index 100% rename from packages/protocol/test-sol/common/LinkedList.t.sol rename to packages/protocol/test-sol/unit/common/LinkedList.t.sol diff --git a/packages/protocol/test-sol/common/MentoFeeCurrencyAdapter.t.sol b/packages/protocol/test-sol/unit/common/MentoFeeCurrencyAdapter.t.sol similarity index 97% rename from packages/protocol/test-sol/common/MentoFeeCurrencyAdapter.t.sol rename to packages/protocol/test-sol/unit/common/MentoFeeCurrencyAdapter.t.sol index d4bab08d2b0..8c5657cc0c9 100644 --- a/packages/protocol/test-sol/common/MentoFeeCurrencyAdapter.t.sol +++ b/packages/protocol/test-sol/unit/common/MentoFeeCurrencyAdapter.t.sol @@ -2,8 +2,8 @@ pragma solidity >=0.8.7 <0.8.20; import "celo-foundry-8/Test.sol"; -import "../../contracts-0.8/common/MentoFeeCurrencyAdapter.sol"; -import "../../contracts-0.8/common/mocks/MockOracle.sol"; +import "@celo-contracts-8/common/MentoFeeCurrencyAdapter.sol"; +import "@celo-contracts-8/common/mocks/MockOracle.sol"; contract MentoFeeCurrencyAdapterBase is Test { MentoFeeCurrencyAdapter mentoAdapter; diff --git a/packages/protocol/test-sol/common/Multisig.t.sol b/packages/protocol/test-sol/unit/common/Multisig.t.sol similarity index 99% rename from packages/protocol/test-sol/common/Multisig.t.sol rename to packages/protocol/test-sol/unit/common/Multisig.t.sol index 142f09d00d3..67322618c57 100644 --- a/packages/protocol/test-sol/common/Multisig.t.sol +++ b/packages/protocol/test-sol/unit/common/Multisig.t.sol @@ -67,7 +67,8 @@ contract MultiSigTest_fallbackFunction is MultiSigTest { uint256 amount = 100; function uncheckedSendViaCall(address payable _to, uint256 _amount) public payable { - _to.call.value(_amount)(""); + (bool res, ) = _to.call.value(_amount)(""); + require(res, "call failed"); } function test_Emits_DepositEventWithCorrectParameters_whenReceivingCelo() public payable { diff --git a/packages/protocol/test-sol/common/Proxy.t.sol b/packages/protocol/test-sol/unit/common/Proxy.t.sol similarity index 97% rename from packages/protocol/test-sol/common/Proxy.t.sol rename to packages/protocol/test-sol/unit/common/Proxy.t.sol index bfd765bd628..b8e105eaee2 100644 --- a/packages/protocol/test-sol/common/Proxy.t.sol +++ b/packages/protocol/test-sol/unit/common/Proxy.t.sol @@ -2,17 +2,14 @@ pragma solidity ^0.5.13; import "@celo-contracts/common/Proxy.sol"; -import "celo-foundry/Test.sol"; - -import { Constants } from "@test-sol/constants.sol"; -import { Utils } from "@test-sol/utils.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; import "@celo-contracts/common/test/GetSetV0.sol"; import "@celo-contracts/common/test/GetSetV1.sol"; import "@celo-contracts/common/test/HasInitializer.sol"; import "@celo-contracts/common/test/MsgSenderCheck.sol"; -contract ProxyTest is Test, Constants, Utils { +contract ProxyTest is TestWithUtils { Proxy proxy; GetSetV0 getSet; GetSetV0 proxiedGetSet; diff --git a/packages/protocol/test-sol/unit/common/ProxyFactory08.t.sol b/packages/protocol/test-sol/unit/common/ProxyFactory08.t.sol new file mode 100644 index 00000000000..c290e570b67 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/ProxyFactory08.t.sol @@ -0,0 +1,79 @@ +pragma solidity ^0.8.15; + +import "celo-foundry-8/Test.sol"; +import "@celo-contracts-8/common/ProxyFactory08.sol"; +import "@celo-contracts/common/interfaces/IProxy.sol"; + +import { Utils08 } from "@test-sol/utils08.sol"; + +contract ProxyFactoryTest is Test, Utils08 { + ProxyFactory08 proxyFactory08; + bytes proxyInitCode; + address constant owner = address(0xAA963FC97281d9632d96700aB62A4D1340F9a28a); + + function setUp() public { + proxyFactory08 = new ProxyFactory08(); + proxyInitCode = vm.getCode("Proxy.sol"); + } + + function test_deployProxy() public { + address deployedAddress = proxyFactory08.deployArbitraryByteCode(0, owner, 0, proxyInitCode); + + IProxy proxy = IProxy(deployedAddress); + + assertEq(proxy._getOwner(), owner); + } + + function test_Reverts_WhenDeployingWithSameSenderAddressAndBytecode() public { + proxyFactory08.deployArbitraryByteCode(0, owner, 0, proxyInitCode); + vm.expectRevert("Create2: Failed on deploy"); + proxyFactory08.deployArbitraryByteCode(0, owner, 0, proxyInitCode); + } + + function test_deploysWithDifferentSalts() public { + address deployedAddress = proxyFactory08.deployArbitraryByteCode(0, owner, 0, proxyInitCode); + address deployedAddress2 = proxyFactory08.deployArbitraryByteCode(0, owner, 1, proxyInitCode); + + assertFalse(deployedAddress == deployedAddress2); + } + + function test_verifyArtifacts() public { + string memory compiler = "0.5.17+commit.d19bba13"; + + checkbytecode(compiler, proxyInitCode, "./artifacts/Proxy/proxyInitCode"); + address deployedAddress = proxyFactory08.deployArbitraryByteCode(0, owner, 0, proxyInitCode); + checkbytecode(compiler, deployedAddress.code, "./artifacts/Proxy/proxyBytecode"); + } + + function checkbytecode( + string memory compiler, + bytes memory bytecode, + string memory artifactPath + ) public view { + string memory bytecodeBackUp = vm.readFile(string.concat(artifactPath, compiler, ".hex")); + string memory bytecodeString = vm.toString(bytecode); + + // Calculate the length of the bytecode to compare (ignoring the last 43 bytes for Swarm hash) + uint compareLength = bytes(bytecodeBackUp).length - 86; // 43 bytes in hex is 86 characters + + // Slice the strings to exclude the Swarm hash + string memory bytecodeBackUpToCompare = substring(bytecodeBackUp, 0, compareLength); + string memory bytecodeToCompare = substring(bytecodeString, 0, compareLength); + + // Assert that the truncated bytecode matches + assert(compareStrings(bytecodeBackUpToCompare, bytecodeToCompare)); + } + + function substring( + string memory str, + uint startIndex, + uint endIndex + ) internal pure returns (string memory) { + bytes memory strBytes = bytes(str); + bytes memory result = new bytes(endIndex - startIndex); + for (uint i = startIndex; i < endIndex; i++) { + result[i - startIndex] = strBytes[i]; + } + return string(result); + } +} diff --git a/packages/protocol/test-sol/common/Registry.t.sol b/packages/protocol/test-sol/unit/common/Registry.t.sol similarity index 53% rename from packages/protocol/test-sol/common/Registry.t.sol rename to packages/protocol/test-sol/unit/common/Registry.t.sol index 404101316d6..bcfda7012bc 100644 --- a/packages/protocol/test-sol/common/Registry.t.sol +++ b/packages/protocol/test-sol/unit/common/Registry.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.5.13; -import "celo-foundry/Test.sol"; +import "@test-sol/utils/WhenL2.sol"; import "@celo-contracts/common/Registry.sol"; @@ -14,89 +14,111 @@ contract RegistryTest is Test { // hash is harcoded to avoid test and implementation changing at the same time bytes32 constant ID_HASH = 0x05445421d7b4d4c2e571c5a4ccf9317ec68601449f752c75ddbcc61a16061004; - Registry registry; + Registry _registry; address owner; function setUp() public { owner = address(this); vm.prank(owner); - registry = new Registry(true); - registry.initialize(); + _registry = new Registry(true); + _registry.initialize(); } +} + +contract RegistryTest_L2 is WhenL2, RegistryTest { + function setUp() public { + super.setUp(); + registry = IRegistry(address(_registry)); + } +} +contract RegistryTest_initialize is RegistryTest { function test_SetsTheOwner() public { - assertEq(registry.owner(), owner); + assertEq(_registry.owner(), owner); } function test_Reverts_WhenCalledAgain() public { vm.expectRevert("contract already initialized"); - registry.initialize(); + _registry.initialize(); } } contract RegistryTest_setAddressFor is RegistryTest { function test_SetsAddress() public { vm.prank(owner); - registry.setAddressFor(SOME_ID, SOME_ADDRESS); - assertEq(registry.registry(ID_HASH), SOME_ADDRESS); + _registry.setAddressFor(SOME_ID, SOME_ADDRESS); + assertEq(_registry.registry(ID_HASH), SOME_ADDRESS); } function test_Reverts_WhenCalledByNonOwner() public { vm.expectRevert("Ownable: caller is not the owner"); vm.prank(msg.sender); - registry.setAddressFor(SOME_ID, SOME_ADDRESS); + _registry.setAddressFor(SOME_ID, SOME_ADDRESS); } function test_Emits_RegistryUpdated() public { vm.expectEmit(true, true, false, true); emit RegistryUpdated(SOME_ID, ID_HASH, SOME_ADDRESS); - registry.setAddressFor(SOME_ID, SOME_ADDRESS); + _registry.setAddressFor(SOME_ID, SOME_ADDRESS); } } +contract RegistryTest_setAddressFor_L2 is RegistryTest_L2, RegistryTest_setAddressFor {} + contract RegistryTest_getAddressFor is RegistryTest { function test_GetsRightAddress() public { - registry.setAddressFor(SOME_ID, SOME_ADDRESS); - assertEq(registry.getAddressFor(ID_HASH), SOME_ADDRESS); + _registry.setAddressFor(SOME_ID, SOME_ADDRESS); + assertEq(_registry.getAddressFor(ID_HASH), SOME_ADDRESS); } function test_ReturnsZero_WhenNotFound() public { - assertEq(registry.getAddressFor(ID_HASH), address(0)); + assertEq(_registry.getAddressFor(ID_HASH), address(0)); } } +contract RegistryTest_getAddressFor_L2 is RegistryTest_L2, RegistryTest_getAddressFor {} + contract RegistryTest_getAddressForString is RegistryTest { function test_GetsRightAddress() public { - registry.setAddressFor(SOME_ID, SOME_ADDRESS); - assertEq(registry.getAddressForString(SOME_ID), SOME_ADDRESS); + _registry.setAddressFor(SOME_ID, SOME_ADDRESS); + assertEq(_registry.getAddressForString(SOME_ID), SOME_ADDRESS); } function test_DoesNotRevers_WhenGettingAddress() public view { - registry.getAddressForString(SOME_ID); + _registry.getAddressForString(SOME_ID); } } +contract RegistryTest_getAddressForString_L2 is RegistryTest_L2, RegistryTest_getAddressForString {} + contract RegistryTest_getAddressForOrDie is RegistryTest { function test_GetsRightAddress() public { - registry.setAddressFor(SOME_ID, SOME_ADDRESS); - assertEq(registry.getAddressForOrDie(ID_HASH), SOME_ADDRESS); + _registry.setAddressFor(SOME_ID, SOME_ADDRESS); + assertEq(_registry.getAddressForOrDie(ID_HASH), SOME_ADDRESS); } function test_Reverts_WhenAddressNotFound() public { vm.expectRevert("identifier has no registry entry"); - registry.getAddressForOrDie(ID_HASH); + _registry.getAddressForOrDie(ID_HASH); } } +contract RegistryTest_getAddressForOrDie_L2 is RegistryTest_L2, RegistryTest_getAddressForOrDie {} + contract RegistryTest_getAddressForStringOrDie is RegistryTest { function test_GetAddressForStringOrDie_gets_address() public { - registry.setAddressFor(SOME_ID, SOME_ADDRESS); - assertEq(registry.getAddressForStringOrDie(SOME_ID), SOME_ADDRESS); + _registry.setAddressFor(SOME_ID, SOME_ADDRESS); + assertEq(_registry.getAddressForStringOrDie(SOME_ID), SOME_ADDRESS); } function test_Reverts_WhenAddressNotFound() public { vm.expectRevert("identifier has no registry entry"); - registry.getAddressForStringOrDie(SOME_ID); + _registry.getAddressForStringOrDie(SOME_ID); } } + +contract RegistryTest_getAddressForStringOrDie_L2 is + RegistryTest_L2, + RegistryTest_getAddressForStringOrDie +{} diff --git a/packages/protocol/test-sol/unit/common/ScoreManager.t.sol b/packages/protocol/test-sol/unit/common/ScoreManager.t.sol new file mode 100644 index 00000000000..eccba616e3b --- /dev/null +++ b/packages/protocol/test-sol/unit/common/ScoreManager.t.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import { TestConstants } from "@test-sol/constants.sol"; + +import "@celo-contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/common/interfaces/IScoreManagerGovernance.sol"; +import "@celo-contracts/common/interfaces/IScoreManager.sol"; +import { ScoreManager } from "@celo-contracts-8/common/ScoreManager.sol"; + +// merging interfaces here because in 0.5 it can't be done +// TODO remove this from here after moving everything to 0.8 +interface IScoreManagerTemp is IScoreManagerGovernance, IScoreManager {} + +contract ScoreManagerTest is Test, TestConstants { + IRegistry registry; + IScoreManagerTemp public scoreManager; + address owner; + address nonOwner; + address scoreManagerSetter; + + event GroupScoreSet(address indexed group, uint256 score); + event ValidatorScoreSet(address indexed validator, uint256 score); + event ScoreManagerSetterSet(address indexed scoreManagerSetter); + + uint256 constant ZERO_SCORE = 1e24 + 1; + + function setUp() public virtual { + owner = address(this); + nonOwner = actor("nonOwner"); + scoreManagerSetter = actor("scoreManager"); + + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); + + ScoreManager scoreManagerImpl = new ScoreManager(true); + scoreManager = IScoreManagerTemp(address(scoreManagerImpl)); + + registry = IRegistry(REGISTRY_ADDRESS); + + registry.setAddressFor("ScoreManager", address(scoreManager)); + + scoreManagerImpl.initialize(); + } + + function _whenL2() public { + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); + } +} + +contract ScoreManagerTest_setGroupScore is ScoreManagerTest { + function test_setGroupScore() public { + scoreManager.setGroupScore(owner, 42); + assertEq(scoreManager.getGroupScore(owner), 42); + } + + function test_Reverts_WhenNotCalledByOwner() public { + vm.prank(nonOwner); + vm.expectRevert("Sender not authorized to update score"); + scoreManager.setGroupScore(owner, 42); + } + + function test_Reverts_WhenSetToMoreThan1e24Plus1() public { + vm.expectRevert("Score must be less than or equal to 1e24."); + scoreManager.setGroupScore(owner, 1e24 + 1); + } + + function test_Returns1FixidityWhenGroupScoreDoesNotExist() public { + assertEq(scoreManager.getGroupScore(owner), 1e24); + } + + function test_Returns0WhenGroupScoreIsZERO_SCORE() public { + scoreManager.setGroupScore(owner, 0); + assert(scoreManager.getGroupScore(owner) == 0); + } + + function test_EmitsGroupScoreSet() public { + vm.expectEmit(false, false, false, true); + emit GroupScoreSet(owner, 42); + scoreManager.setGroupScore(owner, 42); + } + + function test_WhenCalledByScoreManager() public { + scoreManager.setScoreManagerSetter(scoreManagerSetter); + + vm.prank(scoreManagerSetter); + scoreManager.setGroupScore(owner, 42); + assertEq(scoreManager.getGroupScore(owner), 42); + } +} + +contract ScoreManagerTest_setValidatorScore is ScoreManagerTest { + function test_setValidatorScore() public { + scoreManager.setValidatorScore(owner, 42); + assertEq(scoreManager.getValidatorScore(owner), 42); + } + + function test_Reverts_WhenNotCalledByOwner() public { + vm.prank(nonOwner); + vm.expectRevert("Sender not authorized to update score"); + scoreManager.setValidatorScore(owner, 42); + } + + function test_Reverts_WhenSetToMoreThan1e24() public { + vm.expectRevert("Score must be less than or equal to 1e24."); + scoreManager.setValidatorScore(owner, 1e24 + 1); + } + + function test_Returns0WhenValidatorScoreIsZero() public { + scoreManager.setValidatorScore(owner, 0); + assert(scoreManager.getValidatorScore(owner) == 0); + } + + function test_EmitsValidatorScoreSet() public { + vm.expectEmit(false, false, false, true); + emit ValidatorScoreSet(owner, 42); + scoreManager.setValidatorScore(owner, 42); + } + + function test_Returns1FixidityWhenValidatorScoreDoesNotExist() public { + assertEq(scoreManager.getValidatorScore(owner), 1e24); + } + + function test_setScoreManager_WhenCalledByScoreManager() public { + scoreManager.setScoreManagerSetter(scoreManagerSetter); + + vm.prank(scoreManagerSetter); + scoreManager.setValidatorScore(owner, 42); + assertEq(scoreManager.getValidatorScore(owner), 42); + } +} + +contract ScoreManagerTest_setScoreManagerSetter is ScoreManagerTest { + function test_onlyOwnwerCanSetScoreManager() public { + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + scoreManager.setScoreManagerSetter(owner); + } + + function test_setScoreManager() public { + scoreManager.setScoreManagerSetter(nonOwner); + assertEq(scoreManager.getScoreManagerSetter(), nonOwner, "Score Manager not set"); + } + + function test_emits_ScoreManagerSetterSet() public { + vm.expectEmit(false, false, false, true); + emit ScoreManagerSetterSet(nonOwner); + scoreManager.setScoreManagerSetter(nonOwner); + } +} diff --git a/packages/protocol/test-sol/unit/common/SortedLinkedList.t.sol b/packages/protocol/test-sol/unit/common/SortedLinkedList.t.sol new file mode 100644 index 00000000000..247ac0bc4ad --- /dev/null +++ b/packages/protocol/test-sol/unit/common/SortedLinkedList.t.sol @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; + +import "celo-foundry/Test.sol"; + +import "@celo-contracts/common/test/SortedLinkedListMock.sol"; + +contract SortedLinkedListTest is Test { + SortedLinkedListMock sortedList; + + function setUp() public { + sortedList = new SortedLinkedListMock(); + } +} + +contract SortedLinkedListTest_insert is SortedLinkedListTest { + bytes32 key = keccak256("key"); + uint256 numerator = 2; + + function test_ShouldAddASingleElementToTheList() public { + sortedList.insert(key, numerator, bytes32(0), bytes32(0)); + assertEq(sortedList.contains(key), true, "should contain the key"); + (bytes32[] memory keys, uint256[] memory numerators) = sortedList.getElements(); + assertEq(keys.length, 1, "keys should have a single element"); + assertEq(keys[0], key, "should have the correct key"); + + assertEq(numerators.length, 1, "numerators should have a single element"); + assertEq(numerators[0], numerator, "should have the correct numerator"); + } + + function test_ShouldIncrementNumElements() public { + sortedList.insert(key, numerator, bytes32(0), bytes32(0)); + assertEq(sortedList.getNumElements(), 1, "should have a single element"); + } + + function test_ShouldUpdateTheHead() public { + sortedList.insert(key, numerator, bytes32(0), bytes32(0)); + assertEq(sortedList.head(), key, "should have the correct head"); + } + + function test_ShouldUpdateTheTail() public { + sortedList.insert(key, numerator, bytes32(0), bytes32(0)); + assertEq(sortedList.tail(), key, "should have the correct tail"); + } + + function test_ShouldRevertIfKeyIsZero() public { + vm.expectRevert("invalid key"); + sortedList.insert(bytes32(0), numerator, bytes32(0), bytes32(0)); + } + + function test_ShouldRevertIfLesserIsEqualToKey() public { + vm.expectRevert("invalid key"); + sortedList.insert(key, numerator, key, bytes32(0)); + } + + function test_ShouldRevertIfGreaterIsEqualToKey() public { + vm.expectRevert("invalid key"); + sortedList.insert(key, numerator, bytes32(0), key); + } + + function test_ShouldRevert_WhenInsertingElementAlreadyInTheList() public { + sortedList.insert(key, numerator, bytes32(0), bytes32(0)); + vm.expectRevert("invalid key"); + sortedList.insert(key, numerator, bytes32(0), bytes32(0)); + } +} + +contract SortedLinkedListTest_update is SortedLinkedListTest { + bytes32 key0 = keccak256("key"); + bytes32 key1 = keccak256("key2"); + bytes32 key2 = keccak256("key3"); + bytes32 absentKey = keccak256("absentKey"); + uint256 value0 = 1; + uint256 value1 = 3; + uint256 value2 = 5; + uint256 smallestValue = 0; + uint256 smallerValue = 2; + uint256 largerValue = 4; + uint256 largestValue = 6; + + function setUp() public { + super.setUp(); + sortedList.insert(key0, value0, bytes32(0), bytes32(0)); + sortedList.insert(key1, value1, key0, bytes32(0)); + sortedList.insert(key2, value2, key1, bytes32(0)); + } + + function test_ShouldUpdateValueForAnExistingElement_whenBecomingTheSmallestElement() public { + sortedList.update(key1, smallestValue, bytes32(0), key0); + (bytes32[] memory keys, uint256[] memory values) = sortedList.getElements(); + assertEq(keys.length, 3, "keys should have three elements"); + assertEq(keys[2], key1, "should have the correct key"); + + assertEq(values.length, 3, "values should have three elements"); + assertEq(values[2], smallestValue, "should have the correct value"); + } + + function test_ShouldUpdateValueForAnExistingElement_whenBecomingTheLargestElement() public { + sortedList.update(key1, largestValue, key2, bytes32(0)); + (bytes32[] memory keys, uint256[] memory values) = sortedList.getElements(); + assertEq(keys.length, 3, "keys should have three element"); + assertEq(keys[0], key1, "should have the correct key"); + + assertEq(values.length, 3, "values should have three elements"); + assertEq(values[0], largestValue, "should have the correct value"); + } + + function test_ShouldUpdateValueForAnExistingElement_whenIncreasingValueButStayingInPlace() + public + { + sortedList.update(key1, largerValue, key0, key2); + (bytes32[] memory keys, uint256[] memory values) = sortedList.getElements(); + assertEq(keys.length, 3, "keys should have three element"); + assertEq(keys[1], key1, "should have the correct key"); + + assertEq(values.length, 3, "values should have three elements"); + assertEq(values[1], largerValue, "should have the correct value"); + } + + function test_ShouldUpdateValueForAnExistingElement_whenDecreasingValueButStayingInPlace() + public + { + sortedList.update(key1, smallerValue, key0, key2); + (bytes32[] memory keys, uint256[] memory values) = sortedList.getElements(); + assertEq(keys.length, 3, "keys should have three element"); + assertEq(keys[1], key1, "should have the correct key"); + + assertEq(values.length, 3, "values should have three elements"); + assertEq(values[1], smallerValue, "should have the correct value"); + } + + function test_ShouldUpdateValueForAnExistingElement_whenNotChangingValue() public { + sortedList.update(key1, value1, key0, key2); + (bytes32[] memory keys, uint256[] memory values) = sortedList.getElements(); + assertEq(keys.length, 3, "keys should have three elements"); + assertEq(keys[1], key1, "should have the correct key"); + + assertEq(values.length, 3, "values should have three element"); + assertEq(values[1], value1, "should have the correct value"); + } + + function test_ShouldRevertIfTheKeyIsNotInTheList() public { + vm.expectRevert("key not in list"); + sortedList.update(absentKey, smallestValue, bytes32(0), key0); + } + + function test_ShouldRevertIfLesserIsEqualToKey() public { + vm.expectRevert("invalid key"); + sortedList.update(key1, smallestValue, key1, key0); + } + + function test_ShouldRevertIfGreaterIsEqualToKey() public { + vm.expectRevert("invalid key"); + sortedList.update(key1, largestValue, key2, key1); + } +} + +contract SortedLinkedListTest_remove is SortedLinkedListTest { + bytes32 key = keccak256("key"); + bytes32 key2 = keccak256("key2"); + uint256 numerator = 2; + + function setUp() public { + super.setUp(); + sortedList.insert(key, numerator, bytes32(0), bytes32(0)); + } + + function test_ShouldRemoveTheElementFromTheList() public { + sortedList.remove(key); + assertEq(sortedList.contains(key), false, "should not contain the key"); + } + + function test_ShouldDecrementNumElements() public { + sortedList.remove(key); + assertEq(sortedList.getNumElements(), 0, "should have no elements"); + } + + function test_ShouldUpdateTheHead() public { + sortedList.remove(key); + assertEq(sortedList.head(), bytes32(0), "should have the correct head"); + } + + function test_ShouldUpdateTheTail() public { + sortedList.remove(key); + assertEq(sortedList.tail(), bytes32(0), "should have the correct tail"); + } + + function test_ShouldRevertIfTheKeyIsNotInTheList() public { + vm.expectRevert("key not in list"); + sortedList.remove(key2); + } +} + +contract SortedLinkedListTest_WhenThereAreMultipleActions is SortedLinkedListTest { + uint256 nonce = 0; + + enum SortedListActionType { + Update, + Remove, + Insert + } + + struct SortedElement { + bytes32 key; + uint256 numerator; + } + + struct SortedListAction { + SortedListActionType actionType; + SortedElement element; + } + + function getLesserAndGreater( + uint256 numerator + ) internal view returns (bytes32 lesser, bytes32 greater) { + // Fetch all elements from the list + (bytes32[] memory keys, uint256[] memory numerators) = sortedList.getElements(); + uint256 length = keys.length; + + lesser = bytes32(0); // Initialize with the default values + greater = bytes32(0); + + for (uint256 i = 0; i < length; i++) { + // Find the first key with a numerator greater than the given one + if (numerators[i] > numerator) { + greater = keys[i]; + if (i > 0) { + lesser = keys[i - 1]; + } + break; + } + } + + // If no greater key is found, the last key in the list is considered `lesser` + if (greater == bytes32(0) && length > 0) { + lesser = keys[length - 1]; + } + } + + function random(uint256 maxNumber) public returns (uint256) { + nonce += 1; + return + uint256(keccak256(abi.encodePacked(nonce, msg.sender, blockhash(block.number - 1)))) % + maxNumber; + } + + function getLesserAndGreaterIncorrect() internal returns (bytes32 lesser, bytes32 greater) { + (bytes32[] memory keys, ) = sortedList.getElements(); + + uint256 random1 = random(100); + if (random1 < 50) { + return (bytes32(0), bytes32(0)); + } else { + uint256 random2 = random(keys.length); + uint256 random3 = random(keys.length); + return (keys[random2], keys[random3]); + } + } + + function assertSortedFractionListInvariants() internal view { + // Fetch all elements from the list + (bytes32[] memory keys, uint256[] memory numerators) = sortedList.getElements(); + uint256 numElements = sortedList.getNumElements(); // Assuming getNumElements() returns the total number of elements + + // Assert the number of elements is correct + require(keys.length == numElements, "Incorrect number of elements"); + + // Assert keys are sorted in descending order of numerators + for (uint256 i = 1; i < keys.length; i++) { + require(numerators[i - 1] >= numerators[i], "Elements not sorted"); + } + } + + function test_MultipleInsertsUpdatesRemovals() public { + bytes32[100] memory keys; + uint256[100] memory numerators; + + // Initialize keys and numerators + for (uint256 i = 0; i < 100; i++) { + keys[i] = bytes32(uint256(i + 1)); + numerators[i] = i * 100; // Example numerator values + } + + // Simulating the action sequence + for (uint256 i = 0; i < 100; i++) { + bytes32 key = keys[i]; + uint256 numerator = numerators[i]; + + // Randomly decide on action: Insert, Update, or Remove + uint256 actionType = i % 3; // This is a simplification of random selection + + if (actionType == uint256(SortedListActionType.Insert)) { + (bytes32 greater, bytes32 lesser) = getLesserAndGreater(numerator); + sortedList.insert(key, numerator, greater, lesser); + assertTrue(sortedList.contains(key)); + } else if (actionType == uint256(SortedListActionType.Update)) { + if (sortedList.contains(key)) { + (bytes32 greater, bytes32 lesser) = getLesserAndGreater(numerator); + sortedList.update(key, numerator + 1, greater, lesser); + } + } else if (actionType == uint256(SortedListActionType.Remove)) { + if (sortedList.contains(key)) { + sortedList.remove(key); + assertFalse(sortedList.contains(key)); + } + } + } + + assertSortedFractionListInvariants(); + } + + function test_MultipleInsertsUpdatesRemovalsIncorrectGreaterAndLesser() public { + bytes32[100] memory keys; + uint256[100] memory numerators; + + // Initialize keys and numerators + for (uint256 i = 0; i < 100; i++) { + keys[i] = bytes32(uint256(i + 1)); + numerators[i] = i * 100; // Example numerator values + } + + // Simulating the action sequence + for (uint256 i = 0; i < 100; i++) { + bytes32 key = keys[i]; + uint256 numerator = numerators[i]; + + // Randomly decide on action: Insert, Update, or Remove + uint256 actionType = i % 3; // This is a simplification of random selection + + if (actionType == uint256(SortedListActionType.Insert)) { + (bytes32 greater, bytes32 lesser) = getLesserAndGreaterIncorrect(); + (bool success, ) = address(sortedList).call( + abi.encodeWithSelector(sortedList.insert.selector, key, numerator, greater, lesser) + ); + if (success) { + assertTrue(sortedList.contains(key)); + } + // Handle failure case if needed + } else if (actionType == uint256(SortedListActionType.Update)) { + if (sortedList.contains(key)) { + (bytes32 greater, bytes32 lesser) = getLesserAndGreaterIncorrect(); + (bool success, ) = address(sortedList).call( + abi.encodeWithSelector(sortedList.update.selector, key, numerator + 1, greater, lesser) + ); + if (success) { + assertTrue(sortedList.contains(key)); + } + } + } else if (actionType == uint256(SortedListActionType.Remove)) { + if (sortedList.contains(key)) { + (bool success, ) = address(sortedList).call( + abi.encodeWithSelector(sortedList.remove.selector, key) + ); + if (success) { + assertFalse(sortedList.contains(key)); + } + } + } + } + + assertSortedFractionListInvariants(); + } +} diff --git a/packages/protocol/test-sol/unit/common/UniswapFeeHandlerSeller.t.sol b/packages/protocol/test-sol/unit/common/UniswapFeeHandlerSeller.t.sol new file mode 100644 index 00000000000..acfe44c3069 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/UniswapFeeHandlerSeller.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; +pragma experimental ABIEncoderV2; + +// Helper contracts +import { Test } from "celo-foundry/Test.sol"; +import { TestConstants } from "@test-sol/constants.sol"; + +import { UniswapFeeHandlerSeller } from "@celo-contracts/common/UniswapFeeHandlerSeller.sol"; + +contract UniswapFeeHandlerSellerTest is Test, TestConstants { + // Actors + address ZERO_ADDRESS = address(0); + address UNISWAP_FEE_HANDLER_SELLER_OWNER_ADDRESS; + address NON_OWNER_ADDRESS = actor("Arbitrary Non-Owner"); + address ARBITRARY_TOKEN_ADDRESS = actor("Arbitrary Token Address"); + address ARBITRARY_ROUTER_ADDRESS_A = actor("Arbitrary Router Address A"); + address ARBITRARY_ROUTER_ADDRESS_B = actor("Arbitrary Router Address B"); + address ARBITRARY_ROUTER_ADDRESS_C = actor("Arbitrary Router Address C"); + address ARBITRARY_ROUTER_ADDRESS_D = actor("Arbitrary Router Address D"); + + // Contract instance + UniswapFeeHandlerSeller uniswapFeeHandlerSeller; + + function setUp() public { + uniswapFeeHandlerSeller = new UniswapFeeHandlerSeller(true); + UNISWAP_FEE_HANDLER_SELLER_OWNER_ADDRESS = uniswapFeeHandlerSeller.owner(); + } +} + +contract UniswapFeeHandlerSellerTest_SetRouter is UniswapFeeHandlerSellerTest { + function test_SetRouter_ShouldSucceedWhen_CalledByOwner() public { + vm.prank(UNISWAP_FEE_HANDLER_SELLER_OWNER_ADDRESS); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_A); + + assertEq( + uniswapFeeHandlerSeller.getRoutersForToken(ARBITRARY_TOKEN_ADDRESS)[0], + ARBITRARY_ROUTER_ADDRESS_A + ); + } + + function test_SetRouter_ShouldRevertWhen_CalledByNonOwner() public { + vm.prank(NON_OWNER_ADDRESS); + + vm.expectRevert("Ownable: caller is not the owner"); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_A); + } + + function test_SetRouter_ShouldRevertWhen_SettingRouterToZeroAddress() public { + vm.prank(UNISWAP_FEE_HANDLER_SELLER_OWNER_ADDRESS); + + vm.expectRevert("Router can't be address zero"); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ZERO_ADDRESS); + } + + function test_SetRouter_ShouldRevertWhen_SettingMoreRoutesThanMaxAllowed() public { + vm.startPrank(UNISWAP_FEE_HANDLER_SELLER_OWNER_ADDRESS); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_A); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_B); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_C); // Max number of routers defined in contract + + vm.expectRevert("Max number of routers reached"); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_D); // Attempt to set one more router + vm.stopPrank(); + } +} + +contract UniswapFeeHandlerSellerTest_RemoveRouter is UniswapFeeHandlerSellerTest { + function test_RemoveRouter_ShouldSucceedWhen_CalledByOwner() public { + vm.startPrank(UNISWAP_FEE_HANDLER_SELLER_OWNER_ADDRESS); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_A); + uniswapFeeHandlerSeller.removeRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_A); + vm.stopPrank(); + + assertEq(uniswapFeeHandlerSeller.getRoutersForToken(ARBITRARY_TOKEN_ADDRESS).length, 0); + } + + function test_RemoveRouter_ShouldSucceedWhen_ListIsLarge() public { + address[2] memory EXPECTED_ROUTERS; + + vm.startPrank(UNISWAP_FEE_HANDLER_SELLER_OWNER_ADDRESS); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_A); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_B); + uniswapFeeHandlerSeller.setRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_C); + uniswapFeeHandlerSeller.removeRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_C); + vm.stopPrank(); + + EXPECTED_ROUTERS[0] = ARBITRARY_ROUTER_ADDRESS_A; + EXPECTED_ROUTERS[1] = ARBITRARY_ROUTER_ADDRESS_B; + + assertEq( + uniswapFeeHandlerSeller.getRoutersForToken(ARBITRARY_TOKEN_ADDRESS).length, + EXPECTED_ROUTERS.length + ); + assertEq( + uniswapFeeHandlerSeller.getRoutersForToken(ARBITRARY_TOKEN_ADDRESS)[0], + EXPECTED_ROUTERS[0] + ); // Can't use assertEq for arrays + assertEq( + uniswapFeeHandlerSeller.getRoutersForToken(ARBITRARY_TOKEN_ADDRESS)[1], + EXPECTED_ROUTERS[1] + ); + } + + function test_RemoveRouter_ShouldRevertWhen_CalledByNonOwner() public { + vm.prank(NON_OWNER_ADDRESS); + + vm.expectRevert("Ownable: caller is not the owner"); + uniswapFeeHandlerSeller.removeRouter(ARBITRARY_TOKEN_ADDRESS, ARBITRARY_ROUTER_ADDRESS_A); + } +} diff --git a/packages/protocol/test-sol/unit/common/mocks/MockEpochManager.sol b/packages/protocol/test-sol/unit/common/mocks/MockEpochManager.sol new file mode 100644 index 00000000000..5bc6899bdd0 --- /dev/null +++ b/packages/protocol/test-sol/unit/common/mocks/MockEpochManager.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.5.13 <0.9.0; +// solhint-disable no-unused-vars + +import "../../../../contracts/common/interfaces/IEpochManager.sol"; + +/** + * @title A mock EpochManager for testing with 0.5. + */ + +contract MockEpochManager is IEpochManager { + enum EpochProcessStatus { + NotStarted, + Started + } + + struct Epoch { + uint256 firstBlock; + uint256 lastBlock; + uint256 startTimestamp; + uint256 endTimestamp; + uint256 rewardsBlock; + } + + struct EpochProcessState { + EpochProcessStatus status; + uint256 perValidatorReward; // The per validator epoch reward. + uint256 totalRewardsVoter; // The total rewards to voters. + uint256 totalRewardsCommunity; // The total community reward. + uint256 totalRewardsCarbonFund; // The total carbon offsetting partner reward. + } + + uint256 public epochDuration; + + uint256 public firstKnownEpoch; + uint256 private currentEpochNumber; + address[] public electedAccounts; + address[] public electedSigners; + uint256 numberOfElectedAccounts; + address public epochManagerEnabler; + bool systemInitialized; + + bool private _isTimeForNextEpoch; + bool private isProcessingEpoch; + EpochProcessState public epochProcessing; + mapping(uint256 => Epoch) private epochs; + + event SendValidatorPaymentCalled(address validator); + + function setCurrentEpochNumber(uint256 _newEpochNumber) external { + currentEpochNumber = _newEpochNumber; + } + + function initializeSystem( + uint256 firstEpochNumber, + uint256 firstEpochBlock, + address[] calldata firstElected + ) external { + firstKnownEpoch = firstEpochNumber; + currentEpochNumber = firstEpochNumber; + + Epoch storage _currentEpoch = epochs[currentEpochNumber]; + _currentEpoch.firstBlock = firstEpochBlock; + _currentEpoch.startTimestamp = block.timestamp; + + electedAccounts = firstElected; + electedSigners = firstElected; + + systemInitialized = true; + epochManagerEnabler = address(0); + } + + function startNextEpochProcess() external {} + function finishNextEpochProcess( + address[] calldata, + address[] calldata, + address[] calldata + ) external { + epochs[currentEpochNumber].lastBlock = block.number - 1; + + currentEpochNumber++; + epochs[currentEpochNumber].firstBlock = block.number; + epochs[currentEpochNumber].startTimestamp = block.timestamp; + + EpochProcessState memory _epochProcessingEmpty; + epochProcessing = _epochProcessingEmpty; + } + + function setToProcessGroups() external {} + function processGroup(address group, address lesser, address greater) external {} + + function setIsTimeForNextEpoch(bool _isTime) external { + _isTimeForNextEpoch = _isTime; + } + function setIsOnEpochProcess(bool _isProcessing) external { + isProcessingEpoch = _isProcessing; + } + + function setNumberOfElectedInCurrentSet(uint256 value) external { + numberOfElectedAccounts = value; + } + + function getCurrentEpoch() external view returns (uint256, uint256, uint256, uint256) { + return getEpochByNumber(currentEpochNumber); + } + + function getCurrentEpochNumber() external view returns (uint256) { + return currentEpochNumber; + } + + function numberOfElectedInCurrentSet() external view returns (uint256) { + return numberOfElectedAccounts; + } + + function getElectedAccounts() external view returns (address[] memory) { + return electedAccounts; + } + + function getElectedAccountByIndex(uint256 index) external view returns (address) { + return electedAccounts[index]; + } + + function getFirstBlockAtEpoch(uint256 _epoch) external view returns (uint256) { + Epoch storage targetEpoch = epochs[_epoch]; + + return (targetEpoch.firstBlock); + } + + function getLastBlockAtEpoch(uint256 _epoch) external view returns (uint256) { + Epoch storage targetEpoch = epochs[_epoch]; + + return (targetEpoch.lastBlock); + } + + function getEpochProcessingState() + external + view + returns (uint256, uint256, uint256, uint256, uint256) + { + return (0, 0, 0, 0, 0); + } + + function systemAlreadyInitialized() external view returns (bool) { + return systemInitialized; + } + + function isBlocked() external view returns (bool) { + return isProcessingEpoch; + } + function isTimeForNextEpoch() external view returns (bool) { + return _isTimeForNextEpoch; + } + function isOnEpochProcess() external view returns (bool) { + return isProcessingEpoch; + } + + function getEpochByBlockNumber( + uint256 + ) external view returns (uint256, uint256, uint256, uint256) { + return (0, 0, 0, 0); + } + + function getEpochNumberOfBlock(uint256 _blockNumber) external view returns (uint256) { + (uint256 _epochNumber, , , , ) = _getEpochByBlockNumber(_blockNumber); + return _epochNumber; + } + + function getElectedSigners() external view returns (address[] memory) { + return electedSigners; + } + + function getElectedSignerByIndex(uint256 index) external view returns (address) { + return electedSigners[index]; + } + + function sendValidatorPayment(address validator) public { + emit SendValidatorPaymentCalled(validator); + } + + function getEpochByNumber( + uint256 epochNumber + ) public view returns (uint256, uint256, uint256, uint256) { + Epoch storage _epoch = epochs[epochNumber]; + return (_epoch.firstBlock, _epoch.lastBlock, _epoch.startTimestamp, _epoch.rewardsBlock); + } + + function _getEpochByBlockNumber( + uint256 _blockNumber + ) internal view returns (uint256, uint256, uint256, uint256, uint256) { + require(_blockNumber <= block.number, "Invalid blockNumber. Value too high."); + (uint256 _firstBlockOfFirstEpoch, , , ) = getEpochByNumber(firstKnownEpoch); + require(_blockNumber >= _firstBlockOfFirstEpoch, "Invalid blockNumber. Value too low."); + uint256 _firstBlockOfCurrentEpoch = epochs[currentEpochNumber].firstBlock; + + if (_blockNumber >= _firstBlockOfCurrentEpoch) { + ( + uint256 _firstBlock, + uint256 _lastBlock, + uint256 _startTimestamp, + uint256 _rewardsBlock + ) = getEpochByNumber(currentEpochNumber); + return (currentEpochNumber, _firstBlock, _lastBlock, _startTimestamp, _rewardsBlock); + } + + uint256 left = firstKnownEpoch; + uint256 right = currentEpochNumber - 1; + + while (left <= right) { + uint256 mid = (left + right) / 2; + Epoch memory _epoch = epochs[mid]; + + if (_blockNumber >= _epoch.firstBlock && _blockNumber <= _epoch.lastBlock) { + return ( + mid, + _epoch.firstBlock, + _epoch.lastBlock, + _epoch.startTimestamp, + _epoch.rewardsBlock + ); + } else if (_blockNumber < _epoch.firstBlock) { + right = mid - 1; + } else { + left = mid + 1; + } + } + + revert("No matching epoch found for the given block number."); + } +} diff --git a/packages/protocol/test-sol/governance/mock/MockGovernance.sol b/packages/protocol/test-sol/unit/governance/mock/MockGovernance.sol similarity index 81% rename from packages/protocol/test-sol/governance/mock/MockGovernance.sol rename to packages/protocol/test-sol/unit/governance/mock/MockGovernance.sol index a54f4e4d23e..7718a9d9082 100644 --- a/packages/protocol/test-sol/governance/mock/MockGovernance.sol +++ b/packages/protocol/test-sol/unit/governance/mock/MockGovernance.sol @@ -42,22 +42,16 @@ contract MockGovernance is IGovernance { removeVotesCalledFor[account] = maxAmountAllowed; } - function setConstitution(address destination, bytes4 functionId, uint256 threshold) external { + function setConstitution(address, bytes4, uint256) external { revert("not implemented"); } - function votePartially( - uint256 proposalId, - uint256 index, - uint256 yesVotes, - uint256 noVotes, - uint256 abstainVotes - ) external returns (bool) { + function votePartially(uint256, uint256, uint256, uint256, uint256) external returns (bool) { return true; } function getProposal( - uint256 proposalId + uint256 ) external view returns (address, uint256, uint256, uint256, string memory, uint256, bool) { return (address(0), 0, 0, 0, "", 0, false); } @@ -66,7 +60,7 @@ contract MockGovernance is IGovernance { return totalVotes[account]; } - function getReferendumStageDuration() external view returns (uint256) { + function getReferendumStageDuration() external pure returns (uint256) { return 0; } } diff --git a/packages/protocol/test-sol/governance/network/BlockchainParameters.t.sol b/packages/protocol/test-sol/unit/governance/network/BlockchainParameters.t.sol similarity index 90% rename from packages/protocol/test-sol/governance/network/BlockchainParameters.t.sol rename to packages/protocol/test-sol/unit/governance/network/BlockchainParameters.t.sol index 5cb344dfa73..ebee8644d3d 100644 --- a/packages/protocol/test-sol/governance/network/BlockchainParameters.t.sol +++ b/packages/protocol/test-sol/unit/governance/network/BlockchainParameters.t.sol @@ -2,16 +2,12 @@ pragma solidity ^0.5.13; import "@celo-contracts/governance/BlockchainParameters.sol"; -import "celo-foundry/Test.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; -import { Constants } from "@test-sol/constants.sol"; -import { Utils } from "@test-sol/utils.sol"; - -contract BlockchainParametersTest is Test, Constants, Utils { +contract BlockchainParametersTest is TestWithUtils { uint256 constant gasLimit = 7000000; uint256 constant gasForNonGoldCurrencies = 50000; address nonOwner; - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; BlockchainParameters blockchainParameters; @@ -25,9 +21,6 @@ contract BlockchainParametersTest is Test, Constants, Utils { ph.setEpochSize(DAY / 5); blockchainParameters = new BlockchainParameters(true); } - function _whenL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); - } } contract BlockchainParametersTest_initialize is BlockchainParametersTest { @@ -174,3 +167,21 @@ contract BlockchainParametersTest_setUptimeLookbackWindow is BlockchainParameter blockchainParameters.setUptimeLookbackWindow(100); } } + +contract BlockchainParametersTest_blockGasLimit is BlockchainParametersTest { + function test_Reverts_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + blockchainParameters.blockGasLimit(); + } +} + +contract BlockchainParametersTest_intrinsicGasForAlternativeFeeCurrency is + BlockchainParametersTest +{ + function test_Reverts_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + blockchainParameters.intrinsicGasForAlternativeFeeCurrency(); + } +} diff --git a/packages/protocol/test-sol/governance/network/EpochRewards.t.sol b/packages/protocol/test-sol/unit/governance/network/EpochRewards.t.sol similarity index 82% rename from packages/protocol/test-sol/governance/network/EpochRewards.t.sol rename to packages/protocol/test-sol/unit/governance/network/EpochRewards.t.sol index bac0bc3888a..b8f4624c2ec 100644 --- a/packages/protocol/test-sol/governance/network/EpochRewards.t.sol +++ b/packages/protocol/test-sol/unit/governance/network/EpochRewards.t.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.5.13; -import "celo-foundry/Test.sol"; import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Freezer.sol"; @@ -11,12 +10,12 @@ import { Reserve } from "@lib/mento-core/contracts/Reserve.sol"; import { MockSortedOracles } from "@celo-contracts/stability/test/MockSortedOracles.sol"; import { MockStableToken } from "@celo-contracts/stability/test/MockStableToken.sol"; -import { GoldTokenMock } from "@test-sol/common/GoldTokenMock.sol"; +import { CeloTokenMock } from "@test-sol/unit/common/CeloTokenMock.sol"; -import { Constants } from "@test-sol/constants.sol"; -import { Utils } from "@test-sol/utils.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; +import "@test-sol/utils/WhenL2.sol"; -contract EpochRewardsTest is Test, Constants, Utils { +contract EpochRewardsTest is TestWithUtils { uint256 constant targetVotingYieldParamsInitial = 0.00016e24; // 0.00016 uint256 constant targetVotingYieldParamsMax = 0.0005e24; // 0.0005 uint256 constant targetVotingYieldParamsAdjustmentFactor = 1127990000000000000; // 0.00000112799 @@ -34,7 +33,6 @@ contract EpochRewardsTest is Test, Constants, Utils { uint256 constant sortedOraclesDenominator = FIXED1; uint256 constant SUPPLY_CAP = 1e9 ether; - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; bytes32[] initialAssetAllocationSymbols; uint256[] initialAssetAllocationWeights; @@ -43,12 +41,11 @@ contract EpochRewardsTest is Test, Constants, Utils { MockElection election; MockSortedOracles mockSortedOracles; MockStableToken mockStableToken; - GoldTokenMock mockGoldToken; + CeloTokenMock mockCeloToken; Reserve reserve; Freezer freezer; - Registry registry; - + address celoUnreleasedTreasuryAddress; address caller = address(this); event TargetVotingGoldFractionSet(uint256 fraction); @@ -63,20 +60,28 @@ contract EpochRewardsTest is Test, Constants, Utils { event TargetVotingYieldSet(uint256 target); function setUp() public { + super.setUp(); // Mocked contracts epochRewards = new EpochRewardsMock(); election = new MockElection(); mockSortedOracles = new MockSortedOracles(); mockStableToken = new MockStableToken(); - mockGoldToken = new GoldTokenMock(); + + mockCeloToken = new CeloTokenMock(); + mockCeloToken.setRegistry(REGISTRY_ADDRESS); + mockCeloToken.setTotalSupply(L1_MINTED_CELO_SUPPLY); freezer = new Freezer(true); - registry = new Registry(true); + celoUnreleasedTreasuryAddress = actor("celoUnreleasedTreasury"); + deployCodeTo("CeloUnreleasedTreasury.sol", abi.encode(false), celoUnreleasedTreasuryAddress); + registry.setAddressFor(CeloUnreleasedTreasuryContract, celoUnreleasedTreasuryAddress); + + vm.deal(celoUnreleasedTreasuryAddress, L2_INITIAL_STASH_BALANCE); registry.setAddressFor(ElectionContract, address(election)); registry.setAddressFor(SortedOraclesContract, address(mockSortedOracles)); registry.setAddressFor(StableTokenContract, address(mockStableToken)); - registry.setAddressFor(GoldTokenContract, address(mockGoldToken)); + registry.setAddressFor(CeloTokenContract, address(mockCeloToken)); registry.setAddressFor(FreezerContract, address(freezer)); mockSortedOracles.setMedianRate( @@ -100,9 +105,24 @@ contract EpochRewardsTest is Test, Constants, Utils { ); } - function _whenL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); + function _setNumberOfElectedInCurrentSetBaseOnLayer(uint256 numberValidators) internal { + if (isL2()) { + epochManager.setNumberOfElectedInCurrentSet(numberValidators); + } else { + epochRewards.setNumberValidatorsInCurrentSet(numberValidators); + } + } + + function _updateTargetVotingYieldBasedOnLayer() internal { + if (isL2()) { + vm.prank(address(epochManager)); + epochRewards.updateTargetVotingYield(); + } else { + vm.prank(address(0)); + epochRewards.updateTargetVotingYield(); + } } + function getExpectedTargetTotalSupply(uint256 timeDelta) internal pure returns (uint256) { uint256 genesisSupply = 600000000 ether; uint256 linearRewards = 200000000 ether; @@ -110,6 +130,8 @@ contract EpochRewardsTest is Test, Constants, Utils { } } +contract EpochRewardsTest_L2 is WhenL2, EpochRewardsTest {} + contract EpochRewardsTest_initialize is EpochRewardsTest { function test_ShouldHaveSetOwner() public { assertEq(epochRewards.owner(), caller); @@ -193,14 +215,13 @@ contract EpochRewardsTest_setTargetVotingGoldFraction is EpochRewardsTest { vm.expectRevert("Target voting gold fraction unchanged"); epochRewards.setTargetVotingGoldFraction(targetVotingGoldFraction); } - - function test_Reverts_WhenCalledOnL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - epochRewards.setTargetVotingGoldFraction(targetVotingGoldFraction); - } } +contract EpochRewardsTest_setTargetVotingGoldFraction_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_setTargetVotingGoldFraction +{} + contract EpochRewardsTest_setCommunityRewardFraction is EpochRewardsTest { uint256 newFraction = communityRewardFraction + 1; @@ -238,14 +259,13 @@ contract EpochRewardsTest_setCommunityRewardFraction is EpochRewardsTest { ); epochRewards.setCommunityRewardFraction(communityRewardFraction); } - - function test_Reverts_WhenCalledOnL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - epochRewards.setCommunityRewardFraction(communityRewardFraction); - } } +contract EpochRewardsTest_setCommunityRewardFraction_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_setCommunityRewardFraction +{} + contract EpochRewardsTest_setTargetValidatorEpochPayment is EpochRewardsTest { uint256 newPayment = targetValidatorEpochPayment + 1; @@ -274,14 +294,13 @@ contract EpochRewardsTest_setTargetValidatorEpochPayment is EpochRewardsTest { vm.expectRevert("Target validator epoch payment unchanged"); epochRewards.setTargetValidatorEpochPayment(targetValidatorEpochPayment); } - - function test_Reverts_WhenCalledOnL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - epochRewards.setTargetValidatorEpochPayment(targetValidatorEpochPayment); - } } +contract EpochRewardsTest_setTargetValidatorEpochPayment_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_setTargetValidatorEpochPayment +{} + contract EpochRewardsTest_setRewardsMultiplierParameters is EpochRewardsTest { uint256 newRewardsMultiplierAdjustmentsUnderspend = rewardsMultiplierAdjustmentsUnderspend + 1; @@ -332,18 +351,13 @@ contract EpochRewardsTest_setRewardsMultiplierParameters is EpochRewardsTest { rewardsMultiplierAdjustmentsOverspend ); } - - function test_Reverts_WhenCalledOnL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - epochRewards.setRewardsMultiplierParameters( - rewardsMultiplierMax, - rewardsMultiplierAdjustmentsUnderspend, - rewardsMultiplierAdjustmentsOverspend - ); - } } +contract EpochRewardsTest_setRewardsMultiplierParameters_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_setRewardsMultiplierParameters +{} + contract EpochRewardsTest_setTargetVotingYieldParameters is EpochRewardsTest { uint256 newTargetVotingYieldParamsMax = targetVotingYieldParamsMax + 1; uint256 newTargetVotingYieldParamsAdjustmentFactor = targetVotingYieldParamsAdjustmentFactor + 1; @@ -388,17 +402,13 @@ contract EpochRewardsTest_setTargetVotingYieldParameters is EpochRewardsTest { newTargetVotingYieldParamsAdjustmentFactor ); } - - function test_Reverts_WhenCalledOnL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - epochRewards.setTargetVotingYieldParameters( - newTargetVotingYieldParamsMax, - newTargetVotingYieldParamsAdjustmentFactor - ); - } } +contract EpochRewardsTest_setTargetVotingYieldParameters_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_setTargetVotingYieldParameters +{} + contract EpochRewardsTest_setTargetVotingYield is EpochRewardsTest { uint256 constant newTargetVotingYieldParamsInitial = targetVotingYieldParamsInitial + 1; @@ -420,14 +430,13 @@ contract EpochRewardsTest_setTargetVotingYield is EpochRewardsTest { vm.expectRevert("Ownable: caller is not the owner"); epochRewards.setTargetVotingYield(newTargetVotingYieldParamsInitial); } - - function test_Reverts_WhenCalledOnL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - epochRewards.setTargetVotingYield(newTargetVotingYieldParamsInitial); - } } +contract EpochRewardsTest_setTargetVotingYield_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_setTargetVotingYield +{} + contract EpochRewardsTest_getTargetGoldTotalSupply is EpochRewardsTest { function test_ShouldReturn1B_WhenLessThan15YearsSinceGenesis() public { uint256 timeDelta = YEAR * 10; @@ -436,6 +445,11 @@ contract EpochRewardsTest_getTargetGoldTotalSupply is EpochRewardsTest { } } +contract EpochRewardsTest_getTargetGoldTotalSupply_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_getTargetGoldTotalSupply +{} + contract EpochRewardsTest_getTargetVoterRewards is EpochRewardsTest { function test_ShouldReturnAPercentageOfActiveVotes_WhenThereAreActiveVotes() public { uint256 activeVotes = 1000000; @@ -446,16 +460,26 @@ contract EpochRewardsTest_getTargetVoterRewards is EpochRewardsTest { } } +contract EpochRewardsTest_getTargetVoterRewards_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_getTargetVoterRewards +{} + contract EpochRewardsTest_getTargetTotalEpochPaymentsInGold is EpochRewardsTest { function test_ShouldgetTargetTotalEpochPaymentsInGold_WhenExchangeRateIsSet() public { uint256 numberValidators = 100; - epochRewards.setNumberValidatorsInCurrentSet(numberValidators); + _setNumberOfElectedInCurrentSetBaseOnLayer(numberValidators); uint256 expected = uint256((targetValidatorEpochPayment * numberValidators) / exchangeRate); assertEq(epochRewards.getTargetTotalEpochPaymentsInGold(), expected); } } +contract EpochRewardsTest_getTargetTotalEpochPaymentsInGold_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_getTargetTotalEpochPaymentsInGold +{} + contract EpochRewardsTest_getRewardsMultiplier is EpochRewardsTest { uint256 constant timeDelta = YEAR * 10; uint256 expectedTargetTotalSupply; @@ -464,6 +488,7 @@ contract EpochRewardsTest_getRewardsMultiplier is EpochRewardsTest { function setUp() public { super.setUp(); + expectedTargetTotalSupply = getExpectedTargetTotalSupply(timeDelta); expectedTargetRemainingSupply = SUPPLY_CAP - expectedTargetTotalSupply; targetEpochReward = @@ -473,7 +498,13 @@ contract EpochRewardsTest_getRewardsMultiplier is EpochRewardsTest { } function test_ShouldReturnOne_WhenTheTargetSupplyIsEqualToTheActualSupplyAfterRewards() public { - mockGoldToken.setTotalSupply(expectedTargetTotalSupply - targetEpochReward); + if (isL2()) { + uint256 celoUnreleasedTreasuryBalance = SUPPLY_CAP - expectedTargetTotalSupply; + vm.deal(celoUnreleasedTreasuryAddress, celoUnreleasedTreasuryBalance - targetEpochReward); + } else { + mockCeloToken.setTotalSupply(expectedTargetTotalSupply - targetEpochReward); + } + assertEq(epochRewards.getRewardsMultiplier(), FIXED1); } @@ -481,8 +512,13 @@ contract EpochRewardsTest_getRewardsMultiplier is EpochRewardsTest { public { uint256 actualRemainingSupply = uint256((expectedTargetRemainingSupply * 11) / 10); - uint256 totalSupply = SUPPLY_CAP - actualRemainingSupply - targetEpochReward; - mockGoldToken.setTotalSupply(totalSupply); + + if (isL2()) { + vm.deal(celoUnreleasedTreasuryAddress, actualRemainingSupply - targetEpochReward); + } else { + uint256 totalSupply = SUPPLY_CAP - actualRemainingSupply - targetEpochReward; + mockCeloToken.setTotalSupply(totalSupply); + } uint256 actual = epochRewards.getRewardsMultiplier(); uint256 expected = uint256((FIXED1 + (rewardsMultiplierAdjustmentsUnderspend / 10))); @@ -493,8 +529,13 @@ contract EpochRewardsTest_getRewardsMultiplier is EpochRewardsTest { public { uint256 actualRemainingSupply = uint256((expectedTargetRemainingSupply * 9) / 10); - uint256 totalSupply = SUPPLY_CAP - actualRemainingSupply - targetEpochReward; - mockGoldToken.setTotalSupply(totalSupply); + + if (isL2()) { + vm.deal(celoUnreleasedTreasuryAddress, actualRemainingSupply - targetEpochReward); + } else { + uint256 totalSupply = SUPPLY_CAP - actualRemainingSupply - targetEpochReward; + mockCeloToken.setTotalSupply(totalSupply); + } uint256 actual = epochRewards.getRewardsMultiplier(); uint256 expected = uint256((FIXED1 - (rewardsMultiplierAdjustmentsOverspend / 10))); @@ -502,10 +543,16 @@ contract EpochRewardsTest_getRewardsMultiplier is EpochRewardsTest { } } +contract EpochRewardsTest_getRewardsMultiplier_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_getRewardsMultiplier +{} + contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { - uint256 constant totalSupply = 6000000 ether; + uint256 constant totalSupplyL1 = 6000000 ether; + uint256 constant celoUnreleasedTreasuryBalance = SUPPLY_CAP - totalSupplyL1; uint256 constant reserveBalance = 1000000 ether; - uint256 constant floatingSupply = totalSupply - reserveBalance; + uint256 constant floatingSupply = totalSupplyL1 - reserveBalance; function setUp() public { super.setUp(); @@ -531,7 +578,11 @@ contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { 2 * FIXED1 ); - mockGoldToken.setTotalSupply(totalSupply); + if (isL2()) { + vm.deal(celoUnreleasedTreasuryAddress, celoUnreleasedTreasuryBalance); + } else { + mockCeloToken.setTotalSupply(totalSupplyL1); + } vm.deal(address(reserve), reserveBalance); } @@ -629,8 +680,7 @@ contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { for (uint256 i = 0; i < 600; i++) { // naive time travel: mining takes too long, just repeatedly update target voting yield. One call is one epoch travelled // time travel alone is not enough, updateTargetVotingYield needs to be called - vm.prank(address(0)); - epochRewards.updateTargetVotingYield(); + _updateTargetVotingYieldBasedOnLayer(); } (uint256 result, , ) = epochRewards.getTargetVotingYieldParameters(); @@ -645,8 +695,7 @@ contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { election.setTotalVotes(totalVotes); // naive time travel: mining takes too long, just repeatedly update target voting yield. One call is one epoch travelled for (uint256 i = 0; i < 800; i++) { - vm.prank(address(0)); - epochRewards.updateTargetVotingYield(); + _updateTargetVotingYieldBasedOnLayer(); } (uint256 result, , ) = epochRewards.getTargetVotingYieldParameters(); @@ -660,8 +709,7 @@ contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { election.setTotalVotes(totalVotes); // naive time travel: mining takes too long, just repeatedly update target voting yield. One call is one epoch travelled for (uint256 i = 0; i < 5; i++) { - vm.prank(address(0)); - epochRewards.updateTargetVotingYield(); + _updateTargetVotingYieldBasedOnLayer(); } uint256 expected = targetVotingYieldParamsInitial + @@ -680,8 +728,7 @@ contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { election.setTotalVotes(totalVotes); // naive time travel: mining takes too long, just repeatedly update target voting yield. One call is one epoch travelled for (uint256 i = 0; i < 5; i++) { - vm.prank(address(0)); - epochRewards.updateTargetVotingYield(); + _updateTargetVotingYieldBasedOnLayer(); } uint256 expected = targetVotingYieldParamsInitial + @@ -727,8 +774,7 @@ contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { uint256 totalVotes = (floatingSupply * (targetVotingGoldFraction - 0.1e24)) / FIXED1; election.setTotalVotes(totalVotes); for (uint256 i = 0; i < 356; i++) { - vm.prank(address(0)); - epochRewards.updateTargetVotingYield(); + _updateTargetVotingYieldBasedOnLayer(); } uint256 expected = targetVotingYieldParamsInitial + @@ -743,8 +789,7 @@ contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { uint256 totalVotes = (floatingSupply * (targetVotingGoldFraction + 0.1e24)) / FIXED1; election.setTotalVotes(totalVotes); for (uint256 i = 0; i < 356; i++) { - vm.prank(address(0)); - epochRewards.updateTargetVotingYield(); + _updateTargetVotingYieldBasedOnLayer(); } uint256 expected = targetVotingYieldParamsInitial - @@ -753,20 +798,17 @@ contract EpochRewardsTest_updateTargetVotingYield is EpochRewardsTest { assertApproxEqRel(result, expected, 1e16); // TODO I suspect it has a 1% error due rounding errors, but need to double check } - function test_Reverts_WhenCalledOnL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - vm.prank(address(0)); - epochRewards.updateTargetVotingYield(); - } - function mockVotes(uint256 votes) internal { election.setTotalVotes(votes); - vm.prank(address(0)); - epochRewards.updateTargetVotingYield(); + _updateTargetVotingYieldBasedOnLayer(); } } +contract EpochRewardsTest_updateTargetVotingYield_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_updateTargetVotingYield +{} + contract EpochRewardsTest_WhenThereAreActiveVotesAStableTokenExchangeRateIsSetAndTheActualRemainingSupplyIs10pMoreThanTheTargetRemainingSupplyAfterRewards_calculateTargetEpochRewards is EpochRewardsTest { @@ -780,7 +822,7 @@ contract EpochRewardsTest_WhenThereAreActiveVotesAStableTokenExchangeRateIsSetAn function setUp() public { super.setUp(); - epochRewards.setNumberValidatorsInCurrentSet(numberValidators); + _setNumberOfElectedInCurrentSetBaseOnLayer(numberValidators); election.setActiveVotes(activeVotes); uint256 expectedTargetTotalEpochPaymentsInGold = (targetValidatorEpochPayment * numberValidators) / exchangeRate; @@ -793,8 +835,16 @@ contract EpochRewardsTest_WhenThereAreActiveVotesAStableTokenExchangeRateIsSetAn uint256 expectedTargetTotalSupply = getExpectedTargetTotalSupply(timeDelta); uint256 expectedTargetRemainingSupply = SUPPLY_CAP - expectedTargetTotalSupply; uint256 actualRemainingSupply = (expectedTargetRemainingSupply * 11) / 10; - uint256 totalSupply = SUPPLY_CAP - actualRemainingSupply - expectedTargetGoldSupplyIncrease; - mockGoldToken.setTotalSupply(totalSupply); + + if (isL2()) { + vm.deal( + celoUnreleasedTreasuryAddress, + actualRemainingSupply + expectedTargetGoldSupplyIncrease + ); + } else { + uint256 totalSupply = SUPPLY_CAP - actualRemainingSupply - expectedTargetGoldSupplyIncrease; + mockCeloToken.setTotalSupply(totalSupply); + } expectedMultiplier = (FIXED1 + rewardsMultiplierAdjustmentsUnderspend / 10); validatorReward = (targetValidatorEpochPayment * numberValidators) / exchangeRate; @@ -804,7 +854,7 @@ contract EpochRewardsTest_WhenThereAreActiveVotesAStableTokenExchangeRateIsSetAn } function test_ShouldFetchTheExpectedRewardsMultiplier() public { - assertApproxEqRel(epochRewards.getRewardsMultiplier(), expectedMultiplier, 2e13); + assertApproxEqRel(epochRewards.getRewardsMultiplier(), expectedMultiplier, 6e13); } function test_ShouldReturnTheTargetValidatorEpochPaymentTimesTheRewardsMultiplier() public { @@ -843,6 +893,11 @@ contract EpochRewardsTest_WhenThereAreActiveVotesAStableTokenExchangeRateIsSetAn } } +contract EpochRewardsTest_WhenThereAreActiveVotesAStableTokenExchangeRateIsSetAndTheActualRemainingSupplyIs10pMoreThanTheTargetRemainingSupplyAfterRewards_calculateTargetEpochRewards_L2 is + EpochRewardsTest_L2, + EpochRewardsTest_WhenThereAreActiveVotesAStableTokenExchangeRateIsSetAndTheActualRemainingSupplyIs10pMoreThanTheTargetRemainingSupplyAfterRewards_calculateTargetEpochRewards +{} + contract EpochRewardsTest_isReserveLow is EpochRewardsTest { uint256 constant stableBalance = 2397846127684712867321; @@ -873,22 +928,22 @@ contract EpochRewardsTest_isReserveLow is EpochRewardsTest { 2 * FIXED1 ); reserve.addToken(address(mockStableToken)); - mockGoldToken.setTotalSupply(totalSupply); + mockCeloToken.setTotalSupply(totalSupply); mockStableToken.setTotalSupply(stableBalance); } // reserve ratio of 0.5' function test_ShouldBeLowAtStart_WhenReserveRatioIs05() public { - uint256 goldBalance = ((stableBalance / exchangeRate) / 2) / 2; - vm.deal(address(reserve), goldBalance); + uint256 celoBalance = ((stableBalance / exchangeRate) / 2) / 2; + vm.deal(address(reserve), celoBalance); // no time travel assertEq(epochRewards.isReserveLow(), true); } // reserve ratio of 1.5 function test_ShouldBeLowAt15Years_WhenReserveRatioIs05() public { - uint256 goldBalance = ((stableBalance / exchangeRate) / 2) / 2; - vm.deal(address(reserve), goldBalance); + uint256 celoBalance = ((stableBalance / exchangeRate) / 2) / 2; + vm.deal(address(reserve), celoBalance); uint256 timeDelta = YEAR * 15; timeTravel(timeDelta); @@ -896,8 +951,8 @@ contract EpochRewardsTest_isReserveLow is EpochRewardsTest { } function test_ShouldBeLowAt25Years_WhenReserveRatioIs05() public { - uint256 goldBalance = ((stableBalance / exchangeRate) / 2) / 2; - vm.deal(address(reserve), goldBalance); + uint256 celoBalance = ((stableBalance / exchangeRate) / 2) / 2; + vm.deal(address(reserve), celoBalance); uint256 timeDelta = YEAR * 25; timeTravel(timeDelta); @@ -905,54 +960,54 @@ contract EpochRewardsTest_isReserveLow is EpochRewardsTest { } function test_ShouldBeLowAtStar_WhenReserveRatioIs1point5() public { - uint256 goldBalance = ((3 * stableBalance) / exchangeRate) / 4; - vm.deal(address(reserve), goldBalance); + uint256 celoBalance = ((3 * stableBalance) / exchangeRate) / 4; + vm.deal(address(reserve), celoBalance); // no time travel assertEq(epochRewards.isReserveLow(), true); } function test_ShouldBeLowAt12Years_WhenReserveRatioIs1point5() public { - uint256 goldBalance = ((3 * stableBalance) / exchangeRate) / 4; - vm.deal(address(reserve), goldBalance); + uint256 celoBalance = ((3 * stableBalance) / exchangeRate) / 4; + vm.deal(address(reserve), celoBalance); uint256 timeDelta = YEAR * 12; timeTravel(timeDelta); assertEq(epochRewards.isReserveLow(), true); } function test_ShouldNotBeLowAt15Years_WhenReserveRatioIs1point5() public { - uint256 goldBalance = ((3 * stableBalance) / exchangeRate) / 4; - vm.deal(address(reserve), goldBalance); + uint256 celoBalance = ((3 * stableBalance) / exchangeRate) / 4; + vm.deal(address(reserve), celoBalance); uint256 timeDelta = YEAR * 15; timeTravel(timeDelta); assertEq(epochRewards.isReserveLow(), false); } function test_ShouldNotBeLowAt25Years_WhenReserveRatioIs1point5() public { - uint256 goldBalance = ((3 * stableBalance) / exchangeRate) / 4; - vm.deal(address(reserve), goldBalance); + uint256 celoBalance = ((3 * stableBalance) / exchangeRate) / 4; + vm.deal(address(reserve), celoBalance); uint256 timeDelta = YEAR * 25; timeTravel(timeDelta); assertEq(epochRewards.isReserveLow(), false); } function test_ShouldBeLowAtStar_WhenReserveRatioIs2point5() public { - uint256 goldBalance = ((5 * stableBalance) / exchangeRate) / 4; - vm.deal(address(reserve), goldBalance); + uint256 celoBalance = ((5 * stableBalance) / exchangeRate) / 4; + vm.deal(address(reserve), celoBalance); // no time travel assertEq(epochRewards.isReserveLow(), false); } function test_ShouldNotBeLowAt15Years_WhenReserveRatioIs2point5() public { - uint256 goldBalance = ((5 * stableBalance) / exchangeRate) / 4; - vm.deal(address(reserve), goldBalance); + uint256 celoBalance = ((5 * stableBalance) / exchangeRate) / 4; + vm.deal(address(reserve), celoBalance); uint256 timeDelta = YEAR * 15; timeTravel(timeDelta); assertEq(epochRewards.isReserveLow(), false); } function test_ShouldNotBeLowAt25Years_WhenReserveRatioIs2point5() public { - uint256 goldBalance = ((5 * stableBalance) / exchangeRate) / 4; - vm.deal(address(reserve), goldBalance); + uint256 celoBalance = ((5 * stableBalance) / exchangeRate) / 4; + vm.deal(address(reserve), celoBalance); uint256 timeDelta = YEAR * 25; timeTravel(timeDelta); assertEq(epochRewards.isReserveLow(), false); @@ -961,8 +1016,21 @@ contract EpochRewardsTest_isReserveLow is EpochRewardsTest { // when the contract is frozen function test_ShouldMakeUpdateTargetVotingyieldRevert_WhenTheContractIsFrozen() public { freezer.freeze(address(epochRewards)); - vm.prank(address(0)); - vm.expectRevert("can't call when contract is frozen"); - epochRewards.updateTargetVotingYield(); + if (isL2()) { + vm.prank(address(epochManager)); + vm.expectRevert("can't call when contract is frozen"); + epochRewards.updateTargetVotingYield(); + } else { + vm.prank(address(0)); + vm.expectRevert("can't call when contract is frozen"); + epochRewards.updateTargetVotingYield(); + } + } +} + +contract EpochRewardsTest_isReserveLow_L2 is EpochRewardsTest_L2 { + function test_ShouldRevert() public { + vm.expectRevert("This method is no longer supported in L2."); + epochRewards.isReserveLow(); } } diff --git a/packages/protocol/test-sol/governance/network/Governance.t.sol b/packages/protocol/test-sol/unit/governance/network/Governance.t.sol similarity index 82% rename from packages/protocol/test-sol/governance/network/Governance.t.sol rename to packages/protocol/test-sol/unit/governance/network/Governance.t.sol index 37b503026af..ee4a8284cc3 100644 --- a/packages/protocol/test-sol/governance/network/Governance.t.sol +++ b/packages/protocol/test-sol/unit/governance/network/Governance.t.sol @@ -1,6 +1,8 @@ pragma solidity ^0.5.13; -import "celo-foundry/Test.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; +import "@test-sol/utils/WhenL2.sol"; + import "solidity-bytes-utils/contracts/BytesLib.sol"; import "openzeppelin-solidity/contracts/cryptography/ECDSA.sol"; @@ -11,38 +13,16 @@ import "@celo-contracts/governance/test/MockValidators.sol"; import "@celo-contracts/governance/test/TestTransactions.sol"; import "@celo-contracts/common/Accounts.sol"; import "@celo-contracts/common/Signatures.sol"; -import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/FixidityLib.sol"; contract GovernanceMock is Governance(true) { address[] validatorSet; - // Minimally override core functions from UsingPrecompiles - function numberValidatorsInCurrentSet() public view returns (uint256) { - return validatorSet.length; - } - - function numberValidatorsInSet(uint256) public view returns (uint256) { - return validatorSet.length; - } - - function validatorSignerAddressFromCurrentSet(uint256 index) public view returns (address) { - return validatorSet[index]; - } - // Expose test utilities function addValidator(address validator) external { validatorSet.push(validator); } - // exposes removeVotesWhenRevokingDelegatedVotes for tests - function removeVotesWhenRevokingDelegatedVotesTest( - address account, - uint256 maxAmountAllowed - ) public { - _removeVotesWhenRevokingDelegatedVotes(account, maxAmountAllowed); - } - function setDeprecatedWeight( address voterAddress, uint256 proposalIndex, @@ -54,9 +34,30 @@ contract GovernanceMock is Governance(true) { voteRecord.deprecated_weight = weight; voteRecord.proposalId = proposalId; } + + // exposes removeVotesWhenRevokingDelegatedVotes for tests + function removeVotesWhenRevokingDelegatedVotesTest( + address account, + uint256 maxAmountAllowed + ) public { + _removeVotesWhenRevokingDelegatedVotes(account, maxAmountAllowed); + } + + // Minimally override core functions from UsingPrecompiles + function numberValidatorsInCurrentSet() public view returns (uint256) { + return validatorSet.length; + } + + function numberValidatorsInSet(uint256) public view returns (uint256) { + return validatorSet.length; + } + + function validatorSignerAddressFromCurrentSet(uint256 index) public view returns (address) { + return validatorSet[index]; + } } -contract GovernanceTest is Test { +contract GovernanceTest is TestWithUtils { using FixidityLib for FixidityLib.Fraction; using BytesLib for bytes; @@ -71,6 +72,7 @@ contract GovernanceTest is Test { address accVoter; address accOwner; address accApprover; + address accCouncil; uint256 constant DEPOSIT = 5; uint256 constant VOTER_GOLD = 100; uint256 constant REFERENDUM_STAGE_DURATION = 5 * 60; @@ -95,12 +97,17 @@ contract GovernanceTest is Test { FixidityLib.Fraction participationBaseline; FixidityLib.Fraction participationFloor; FixidityLib.Fraction baselineQuorumFactor; + uint256 NEW_VALUE = 45; + uint256 proposalId; + address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; function setUp() public { + super.setUp(); // Define Accounts accVoter = actor("voter"); accOwner = actor("owner"); accApprover = actor("approver"); + accCouncil = actor("council"); baselineUpdateFactor = FixidityLib.newFixedFraction(1, 5); participationBaseline = FixidityLib.newFixedFraction(5, 10); @@ -120,10 +127,67 @@ contract GovernanceTest is Test { vm.warp(100 * 60); setUpContracts(); + setUpVoterAccount(); + setUpProposalStubs(); } + function assertNotEq(uint256 a, uint256 b) internal { + if (a == b) { + emit log("Error: a != b not satisfied [uint]"); + emit log_named_uint(" Left", a); + emit log_named_uint(" Right", b); + fail(); + } + } + + function makeValidProposal() internal returns (uint256) { + return + governance.propose.value(DEPOSIT)( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + okProp.description + ); + } + + function makeEmptyProposal() internal returns (uint256) { + Proposal memory emptyProposal; + return + governance.propose.value(DEPOSIT)( + emptyProposal.values, + emptyProposal.destinations, + emptyProposal.data, + emptyProposal.dataLengths, + "empty proposal" + ); + } + + function makeAndApproveProposal(uint256 index) internal returns (uint256 id) { + id = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(id, index); + } + + function authorizeValidatorSigner(uint256 signerPk, address account) internal { + bytes32 messageHash = keccak256(abi.encodePacked(account)); + bytes32 prefixedHash = ECDSA.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, prefixedHash); + vm.prank(account); + accounts.authorizeValidatorSigner(vm.addr(signerPk), v, r, s); + } + + function authorizeVoteSigner(uint256 signerPk, address account) internal { + bytes32 messageHash = keccak256(abi.encodePacked(account)); + bytes32 prefixedHash = ECDSA.toEthSignedMessageHash(messageHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, prefixedHash); + vm.prank(account); + accounts.authorizeVoteSigner(vm.addr(signerPk), v, r, s); + } function setUpVoterAccount() private { vm.prank(accVoter); accounts.createAccount(); @@ -135,18 +199,13 @@ contract GovernanceTest is Test { function setUpContracts() private { vm.startPrank(accOwner); - Registry registry = new Registry(true); - mockValidators = new MockValidators(); - registry.setAddressFor("Validators", address(mockValidators)); mockLockedGold = new MockLockedGold(); mockLockedGold.setTotalLockedGold(VOTER_GOLD); - registry.setAddressFor("LockedGold", address(mockLockedGold)); accounts = new Accounts(true); accounts.initialize(address(registry)); - registry.setAddressFor("Accounts", address(accounts)); governance = new GovernanceMock(); governance.initialize( @@ -164,6 +223,10 @@ contract GovernanceTest is Test { baselineQuorumFactor.unwrap() ); vm.stopPrank(); + + registry.setAddressFor("Validators", address(mockValidators)); + registry.setAddressFor("LockedGold", address(mockLockedGold)); + registry.setAddressFor("Accounts", address(accounts)); } function setUpProposalStubs() private { @@ -198,64 +261,10 @@ contract GovernanceTest is Test { failingProp.destinations.push(address(testTransactions)); failingProp.description = "failing proposal"; } - - function assertNotEq(uint256 a, uint256 b) internal { - if (a == b) { - emit log("Error: a != b not satisfied [uint]"); - emit log_named_uint(" Left", a); - emit log_named_uint(" Right", b); - fail(); - } - } - - function makeValidProposal() internal returns (uint256 proposalId) { - return - governance.propose.value(DEPOSIT)( - okProp.values, - okProp.destinations, - okProp.data, - okProp.dataLengths, - okProp.description - ); - } - - function makeEmptyProposal() internal returns (uint256 proposalId) { - Proposal memory emptyProposal; - return - governance.propose.value(DEPOSIT)( - emptyProposal.values, - emptyProposal.destinations, - emptyProposal.data, - emptyProposal.dataLengths, - "empty proposal" - ); - } - - function makeAndApproveProposal(uint256 index) internal returns (uint256 id) { - id = makeValidProposal(); - vm.warp(block.timestamp + governance.dequeueFrequency()); - - vm.prank(accApprover); - governance.approve(id, index); - } - - function authorizeValidatorSigner(uint256 signerPk, address account) internal { - bytes32 messageHash = keccak256(abi.encodePacked(account)); - bytes32 prefixedHash = ECDSA.toEthSignedMessageHash(messageHash); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, prefixedHash); - vm.prank(account); - accounts.authorizeValidatorSigner(vm.addr(signerPk), v, r, s); - } - - function authorizeVoteSigner(uint256 signerPk, address account) internal { - bytes32 messageHash = keccak256(abi.encodePacked(account)); - bytes32 prefixedHash = ECDSA.toEthSignedMessageHash(messageHash); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, prefixedHash); - vm.prank(account); - accounts.authorizeVoteSigner(vm.addr(signerPk), v, r, s); - } } +contract GovernanceTest_L2 is GovernanceTest, WhenL2 {} + contract GovernanceTest_initialize is GovernanceTest { function test_SetsTheOwner() public { assertEq(governance.owner(), accOwner); @@ -316,10 +325,10 @@ contract GovernanceTest_initialize is GovernanceTest { } contract GovernanceTest_setApprover is GovernanceTest { - event ApproverSet(address indexed approver); - address NEW_APPROVER = address(7777); + event ApproverSet(address indexed approver); + function test_SetsValue() public { vm.prank(accOwner); governance.setApprover(NEW_APPROVER); @@ -333,29 +342,39 @@ contract GovernanceTest_setApprover is GovernanceTest { governance.setApprover(NEW_APPROVER); } - function test_RevertIf_NullAddress() public { + function test_Reverts_IfNullAddress() public { vm.expectRevert("Approver cannot be 0"); vm.prank(accOwner); governance.setApprover(address(0)); } - function test_RevertIf_Unchanged() public { + function test_Reverts_IfUnchanged() public { vm.expectRevert("Approver unchanged"); vm.prank(accOwner); governance.setApprover(accApprover); } - function test_RevertWhen_CalledByNotOwner() public { + function test_Reverts_WhenSetToSecurityCouncilAddress() public { + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + + vm.expectRevert("Approver cannot be council"); + vm.prank(accOwner); + governance.setApprover(accCouncil); + } + + function test_Reverts_WhenCalledByNotOwner() public { vm.expectRevert("Ownable: caller is not the owner"); vm.prank(address(9999)); governance.setApprover(NEW_APPROVER); } } -contract GovernanceTest_setMinDeposit is GovernanceTest { - event MinDepositSet(uint256 minDeposit); +contract GovernanceTest_setApprover_L2 is GovernanceTest_L2, GovernanceTest_setApprover {} +contract GovernanceTest_setMinDeposit is GovernanceTest { uint256 NEW_MINDEPOSIT = 45; + event MinDepositSet(uint256 minDeposit); function test_SetsValue() public { vm.prank(accOwner); @@ -383,10 +402,11 @@ contract GovernanceTest_setMinDeposit is GovernanceTest { } } -contract GovernanceTest_setConcurrentProposals is GovernanceTest { - event ConcurrentProposalsSet(uint256 concurrentProposals); +contract GovernanceTest_setMinDeposit_L2 is GovernanceTest_L2, GovernanceTest_setMinDeposit {} +contract GovernanceTest_setConcurrentProposals is GovernanceTest { uint256 NEW_CONCURRENT_PROPOSALS = 45; + event ConcurrentProposalsSet(uint256 concurrentProposals); function test_SetsValue() public { vm.prank(accOwner); @@ -420,11 +440,14 @@ contract GovernanceTest_setConcurrentProposals is GovernanceTest { } } +contract GovernanceTest_setConcurrentProposals_L2 is + GovernanceTest_L2, + GovernanceTest_setConcurrentProposals +{} + contract GovernanceTest_setQueueExpiry is GovernanceTest { event QueueExpirySet(uint256 queueExpiry); - uint256 NEW_VALUE = 45; - function test_SetsValue() public { vm.prank(accOwner); governance.setQueueExpiry(NEW_VALUE); @@ -457,11 +480,11 @@ contract GovernanceTest_setQueueExpiry is GovernanceTest { } } +contract GovernanceTest_setQueueExpiry_L2 is GovernanceTest_L2, GovernanceTest_setQueueExpiry {} + contract GovernanceTest_setDequeueFrequency is GovernanceTest { event DequeueFrequencySet(uint256 dequeueFrequency); - uint256 NEW_VALUE = 45; - function test_SetsValue() public { vm.prank(accOwner); governance.setDequeueFrequency(NEW_VALUE); @@ -494,11 +517,14 @@ contract GovernanceTest_setDequeueFrequency is GovernanceTest { } } +contract GovernanceTest_setDequeueFrequency_L2 is + GovernanceTest_L2, + GovernanceTest_setDequeueFrequency +{} + contract GovernanceTest_setReferendumStageDuration is GovernanceTest { event ReferendumStageDurationSet(uint256 value); - uint256 NEW_VALUE = 45; - function test_SetsValue() public { vm.prank(accOwner); governance.setReferendumStageDuration(NEW_VALUE); @@ -531,11 +557,14 @@ contract GovernanceTest_setReferendumStageDuration is GovernanceTest { } } +contract GovernanceTest_setReferendumStageDuration_L2 is + GovernanceTest_L2, + GovernanceTest_setReferendumStageDuration +{} + contract GovernanceTest_setExecutionStageDuration is GovernanceTest { event ExecutionStageDurationSet(uint256 dequeueFrequency); - uint256 NEW_VALUE = 45; - function test_SetsValue() public { vm.prank(accOwner); governance.setExecutionStageDuration(NEW_VALUE); @@ -568,20 +597,18 @@ contract GovernanceTest_setExecutionStageDuration is GovernanceTest { } } +contract GovernanceTest_setExecutionStageDuration_L2 is + GovernanceTest_L2, + GovernanceTest_setExecutionStageDuration +{} + contract GovernanceTest_setParticipationFloor is GovernanceTest { event ParticipationFloorSet(uint256 value); - uint256 NEW_VALUE = 45; - function test_SetsValue() public { vm.prank(accOwner); governance.setParticipationFloor(NEW_VALUE); - ( - uint256 baseline, - uint256 baselineFloor, - uint256 _baselineUpdateFactor, - uint256 _baselineQuorumFactor - ) = governance.getParticipationParameters(); + (, uint256 baselineFloor, , ) = governance.getParticipationParameters(); assertEq(baselineFloor, NEW_VALUE); } @@ -605,20 +632,18 @@ contract GovernanceTest_setParticipationFloor is GovernanceTest { } } +contract GovernanceTest_setParticipationFloor_L2 is + GovernanceTest_L2, + GovernanceTest_setParticipationFloor +{} + contract GovernanceTest_setBaselineUpdateFactor is GovernanceTest { event ParticipationBaselineUpdateFactorSet(uint256 value); - uint256 NEW_VALUE = 45; - function test_SetsValue() public { vm.prank(accOwner); governance.setBaselineUpdateFactor(NEW_VALUE); - ( - uint256 baseline, - uint256 baselineFloor, - uint256 _baselineUpdateFactor, - uint256 _baselineQuorumFactor - ) = governance.getParticipationParameters(); + (, , uint256 _baselineUpdateFactor, ) = governance.getParticipationParameters(); assertEq(_baselineUpdateFactor, NEW_VALUE); } @@ -642,20 +667,18 @@ contract GovernanceTest_setBaselineUpdateFactor is GovernanceTest { } } +contract GovernanceTest_setBaselineUpdateFactor_L2 is + GovernanceTest_L2, + GovernanceTest_setBaselineUpdateFactor +{} + contract GovernanceTest_setBaselineQuorumFactor is GovernanceTest { event ParticipationBaselineQuorumFactorSet(uint256 value); - uint256 NEW_VALUE = 45; - function test_SetsValue() public { vm.prank(accOwner); governance.setBaselineQuorumFactor(NEW_VALUE); - ( - uint256 baseline, - uint256 baselineFloor, - uint256 _baselineUpdateFactor, - uint256 _baselineQuorumFactor - ) = governance.getParticipationParameters(); + (, , , uint256 _baselineQuorumFactor) = governance.getParticipationParameters(); assertEq(_baselineQuorumFactor, NEW_VALUE); } @@ -679,6 +702,11 @@ contract GovernanceTest_setBaselineQuorumFactor is GovernanceTest { } } +contract GovernanceTest_setBaselineQuorumFactor_L2 is + GovernanceTest_L2, + GovernanceTest_setBaselineQuorumFactor +{} + contract GovernanceTest_setConstitution is GovernanceTest { event ConstitutionSet(address indexed destination, bytes4 indexed functionId, uint256 threshold); @@ -755,6 +783,93 @@ contract GovernanceTest_setConstitution is GovernanceTest { } } +contract GovernanceTest_setConstitution_L2 is GovernanceTest_L2, GovernanceTest_setConstitution {} + +contract GovernanceTest_setSecurityCouncil is GovernanceTest { + event SecurityCouncilSet(address indexed council); + + function test_ShouldSetSecurityCouncil() public { + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + + assertEq(governance.securityCouncil(), accCouncil); + } + + function test_Emits_SecurityCouncilSetEvent() public { + vm.expectEmit(true, true, true, true); + emit SecurityCouncilSet(accCouncil); + + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + } + + function test_Reverts_WhenCalledByNonOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + governance.setSecurityCouncil(accCouncil); + } + + function test_Reverts_WhenSetToAddressZero() public { + vm.expectRevert("Council cannot be address zero"); + vm.prank(accOwner); + governance.setSecurityCouncil(address(0)); + } + + function test_Reverts_WhenSetToSameAddress() public { + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + + vm.expectRevert("Council unchanged"); + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + } + + function test_Reverts_WhenSetToApproverAddress() public { + vm.expectRevert("Council cannot be approver"); + vm.prank(accOwner); + governance.setSecurityCouncil(accApprover); + } +} + +contract GovernanceTest_setSecurityCouncil_L2 is + GovernanceTest_L2, + GovernanceTest_setSecurityCouncil +{} + +contract GovernanceTest_setHotfixExecutionTimeWindow is GovernanceTest { + event HotfixExecutionTimeWindowSet(uint256 timeDelta); + + function test_ShouldSetHotfixExecutionTimeWindow() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + + assertEq(governance.hotfixExecutionTimeWindow(), DAY); + } + + function test_Emits_HotfixExecutionTimeWindowSetEvent() public { + vm.expectEmit(true, true, true, true); + emit HotfixExecutionTimeWindowSet(DAY); + + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + } + + function test_Reverts_WhenCalledByNonOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + governance.setHotfixExecutionTimeWindow(DAY); + } + + function test_Reverts_WhenSetToZero() public { + vm.expectRevert("Execution time window cannot be zero"); + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(0); + } +} + +contract GovernanceTest_setHotfixExecutionTimeWindow_L2 is + GovernanceTest_L2, + GovernanceTest_setHotfixExecutionTimeWindow +{} + contract GovernanceTest_propose is GovernanceTest { event ProposalQueued( uint256 indexed proposalId, @@ -786,69 +901,6 @@ contract GovernanceTest_propose is GovernanceTest { assertEq(upVotes[0], 0); } - function check_registerProposal(Proposal memory proposal) private { - uint256 id = governance.propose.value(DEPOSIT)( - proposal.values, - proposal.destinations, - proposal.data, - proposal.dataLengths, - proposal.description - ); - - ( - address proposer, - uint256 deposit, - uint256 timestamp, - uint256 txCount, - string memory description, - uint256 networkWeight, - bool approved - ) = governance.getProposal(id); - - assertEq(proposer, address(this)); - assertEq(deposit, DEPOSIT); - assertEq(timestamp, block.timestamp); - assertEq(txCount, proposal.values.length); - assertEq(description, proposal.description); - assertEq(networkWeight, 0); - assertEq(approved, false); - } - - function check_registerProposalTransactions(Proposal memory proposal) private { - uint256 id = governance.propose.value(DEPOSIT)( - proposal.values, - proposal.destinations, - proposal.data, - proposal.dataLengths, - proposal.description - ); - - uint256 dataPosition = 0; - for (uint256 i = 0; i < proposal.values.length; i++) { - (uint256 value, address destination, bytes memory data) = governance.getProposalTransaction( - id, - i - ); - assertEq(proposal.values[i], value); - assertEq(proposal.destinations[i], destination); - bytes memory expectedData = proposal.data.slice(dataPosition, proposal.dataLengths[i]); - assertEq(data, expectedData); - dataPosition = dataPosition + proposal.dataLengths[i]; - } - } - - function check_emitsProposalQueuedEvents(Proposal memory proposal) private { - vm.expectEmit(true, true, true, true); - emit ProposalQueued(1, address(this), proposal.values.length, DEPOSIT, block.timestamp); - governance.propose.value(DEPOSIT)( - proposal.values, - proposal.destinations, - proposal.data, - proposal.dataLengths, - proposal.description - ); - } - function test_registerTheProposal_whenProposalHasZeroTransactions() public { Proposal memory zeroProp; zeroProp.description = "zero tx proposal"; @@ -927,14 +979,77 @@ contract GovernanceTest_propose is GovernanceTest { assertEq(governance.getQueueLength(), 1); assertEq(governance.lastDequeue(), originalLastDequeue); } + + function check_registerProposal(Proposal memory proposal) private { + uint256 id = governance.propose.value(DEPOSIT)( + proposal.values, + proposal.destinations, + proposal.data, + proposal.dataLengths, + proposal.description + ); + + ( + address proposer, + uint256 deposit, + uint256 timestamp, + uint256 txCount, + string memory description, + uint256 networkWeight, + bool approved + ) = governance.getProposal(id); + + assertEq(proposer, address(this)); + assertEq(deposit, DEPOSIT); + assertEq(timestamp, block.timestamp); + assertEq(txCount, proposal.values.length); + assertEq(description, proposal.description); + assertEq(networkWeight, 0); + assertEq(approved, false); + } + + function check_registerProposalTransactions(Proposal memory proposal) private { + uint256 id = governance.propose.value(DEPOSIT)( + proposal.values, + proposal.destinations, + proposal.data, + proposal.dataLengths, + proposal.description + ); + + uint256 dataPosition = 0; + for (uint256 i = 0; i < proposal.values.length; i++) { + (uint256 value, address destination, bytes memory data) = governance.getProposalTransaction( + id, + i + ); + assertEq(proposal.values[i], value); + assertEq(proposal.destinations[i], destination); + bytes memory expectedData = proposal.data.slice(dataPosition, proposal.dataLengths[i]); + assertEq(data, expectedData); + dataPosition = dataPosition + proposal.dataLengths[i]; + } + } + + function check_emitsProposalQueuedEvents(Proposal memory proposal) private { + vm.expectEmit(true, true, true, true); + emit ProposalQueued(1, address(this), proposal.values.length, DEPOSIT, block.timestamp); + governance.propose.value(DEPOSIT)( + proposal.values, + proposal.destinations, + proposal.data, + proposal.dataLengths, + proposal.description + ); + } } +contract GovernanceTest_propose_L2 is GovernanceTest_L2, GovernanceTest_propose {} + contract GovernanceTest_upvote is GovernanceTest { event ProposalUpvoted(uint256 indexed proposalId, address indexed account, uint256 upvotes); event ProposalExpired(uint256 indexed proposalId); - uint256 proposalId; - function setUp() public { super.setUp(); proposalId = makeValidProposal(); @@ -987,27 +1102,6 @@ contract GovernanceTest_upvote is GovernanceTest { assertEq(upvotes[0], VOTER_GOLD); } - function setUp_whenUpvotedProposalIsExpired() private { - uint256 queueExpiry = governance.queueExpiry(); - - // Prevent dequeues for the sake of this test. - vm.prank(accOwner); - governance.setDequeueFrequency(queueExpiry * 2); - - // make another proposal (id=2) - uint256 newProposalId = makeValidProposal(); - - address accOtherVoter = actor("otherVoter"); - vm.startPrank(accOtherVoter); - accounts.createAccount(); - mockLockedGold.setAccountTotalLockedGold(accOtherVoter, VOTER_GOLD); - mockLockedGold.setAccountTotalGovernancePower(accOtherVoter, VOTER_GOLD); - governance.upvote(newProposalId, proposalId, 0); - vm.stopPrank(); - - vm.warp(block.timestamp + queueExpiry); - } - function test_returnsFalse_whenUpvotedProposalIsExpired() public { setUp_whenUpvotedProposalIsExpired(); vm.prank(accVoter); @@ -1064,21 +1158,6 @@ contract GovernanceTest_upvote is GovernanceTest { governance.upvote(proposalId, 0, 0); } - function setUp_whenPreviousUpvotedProposalIsInQueueAndExpired() - private - returns (uint256 newProposalId) - { - uint256 queueExpiry = 60; - vm.prank(accOwner); - governance.setQueueExpiry(queueExpiry); - - vm.prank(accVoter); - governance.upvote(proposalId, 0, 0); - - vm.warp(block.timestamp + queueExpiry); - return makeValidProposal(); - } - function test_increaseNumberOfUpvotesForTheProposal_whenPreviousUpvotedProposalIsInQueueAndExpired() public { @@ -1128,8 +1207,46 @@ contract GovernanceTest_upvote is GovernanceTest { vm.prank(accVoter); governance.upvote(newProposalId, 0, 0); } + + function setUp_whenUpvotedProposalIsExpired() private { + uint256 queueExpiry = governance.queueExpiry(); + + // Prevent dequeues for the sake of this test. + vm.prank(accOwner); + governance.setDequeueFrequency(queueExpiry * 2); + + // make another proposal (id=2) + uint256 newProposalId = makeValidProposal(); + + address accOtherVoter = actor("otherVoter"); + vm.startPrank(accOtherVoter); + accounts.createAccount(); + mockLockedGold.setAccountTotalLockedGold(accOtherVoter, VOTER_GOLD); + mockLockedGold.setAccountTotalGovernancePower(accOtherVoter, VOTER_GOLD); + governance.upvote(newProposalId, proposalId, 0); + vm.stopPrank(); + + vm.warp(block.timestamp + queueExpiry); + } + + function setUp_whenPreviousUpvotedProposalIsInQueueAndExpired() + private + returns (uint256 newProposalId) + { + uint256 queueExpiry = 60; + vm.prank(accOwner); + governance.setQueueExpiry(queueExpiry); + + vm.prank(accVoter); + governance.upvote(proposalId, 0, 0); + + vm.warp(block.timestamp + queueExpiry); + return makeValidProposal(); + } } +contract GovernanceTest_upvote_L2 is GovernanceTest_L2, GovernanceTest_upvote {} + contract GovernanceTest_revokeUpvote is GovernanceTest { event ProposalExpired(uint256 indexed proposalId); event ProposalUpvoteRevoked( @@ -1138,8 +1255,6 @@ contract GovernanceTest_revokeUpvote is GovernanceTest { uint256 revokedUpvotes ); - uint256 proposalId; - function setUp() public { super.setUp(); proposalId = makeValidProposal(); @@ -1213,7 +1328,6 @@ contract GovernanceTest_revokeUpvote is GovernanceTest { function test_markAccountAsNotHavingUpvoted_whenMoreThanDequeueFrequencySinceLastDequeue() public { - uint256 originalLastDequeue = governance.lastDequeue(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.revokeUpvote(0, 0); (uint256 recordId, uint256 recordWeight) = governance.getUpvoteRecord(accVoter); @@ -1222,9 +1336,9 @@ contract GovernanceTest_revokeUpvote is GovernanceTest { } } -contract GovernanceTest_withdraw is GovernanceTest { - uint256 proposalId; +contract GovernanceTest_revokeUpvote_L2 is GovernanceTest_L2, GovernanceTest_revokeUpvote {} +contract GovernanceTest_withdraw is GovernanceTest { address accProposer; function setUp() public { @@ -1261,13 +1375,14 @@ contract GovernanceTest_withdraw is GovernanceTest { } } +contract GovernanceTest_withdraw_L2 is GovernanceTest_L2, GovernanceTest_withdraw {} + contract GovernanceTest_approve is GovernanceTest { + uint256 INDEX = 0; // first proposal index + event ProposalDequeued(uint256 indexed proposalId, uint256 timestamp); event ProposalApproved(uint256 indexed proposalId); - uint256 INDEX = 0; // first proposal index - uint256 proposalId; - function setUp() public { super.setUp(); proposalId = makeValidProposal(); @@ -1436,7 +1551,11 @@ contract GovernanceTest_approve is GovernanceTest { } } +contract GovernanceTest_approve_L2 is GovernanceTest_L2, GovernanceTest_approve {} + contract GovernanceTest_revokeVotes is GovernanceTest { + uint256 numVoted; + event ProposalVoteRevokedV2( uint256 indexed proposalId, address indexed account, @@ -1445,28 +1564,6 @@ contract GovernanceTest_revokeVotes is GovernanceTest { uint256 abstainVotes ); - uint256 numVoted; - - function setUp() public { - super.setUp(); - vm.prank(accOwner); - governance.setConcurrentProposals(3); - - makeValidProposal(); - makeValidProposal(); - makeValidProposal(); - - vm.warp(block.timestamp + governance.dequeueFrequency()); - - vm.startPrank(accApprover); - governance.approve(1, 0); - governance.approve(2, 1); - governance.approve(3, 2); - vm.stopPrank(); - - vm.startPrank(accVoter); - } - modifier voteForEachNumVoted() { for (uint256 _numVoted = 0; _numVoted < 3; _numVoted++) { uint256 snapshot = vm.snapshot(); @@ -1495,20 +1592,40 @@ contract GovernanceTest_revokeVotes is GovernanceTest { } } - function test_unsetMostRecentReferendumProposalVotedOn_whenAccountHasVotedOnXProposal() - public - voteForEachNumVoted - { - governance.revokeVotes(); - assertEq(governance.getMostRecentReferendumProposal(accVoter), 0); - } - - function test_isVotingReturnsFalse_whenAccountHasVotedOnXProposal() public voteForEachNumVoted { - governance.revokeVotes(); - assertFalse(governance.isVoting(accVoter)); - } + function setUp() public { + super.setUp(); + vm.prank(accOwner); + governance.setConcurrentProposals(3); - function test_Emits_ProposalVoteRevokedV2EventXtimes_whenAccountHasVotedOnXProposal() + makeValidProposal(); + makeValidProposal(); + makeValidProposal(); + + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.startPrank(accApprover); + governance.approve(1, 0); + governance.approve(2, 1); + governance.approve(3, 2); + vm.stopPrank(); + + vm.startPrank(accVoter); + } + + function test_unsetMostRecentReferendumProposalVotedOn_whenAccountHasVotedOnXProposal() + public + voteForEachNumVoted + { + governance.revokeVotes(); + assertEq(governance.getMostRecentReferendumProposal(accVoter), 0); + } + + function test_isVotingReturnsFalse_whenAccountHasVotedOnXProposal() public voteForEachNumVoted { + governance.revokeVotes(); + assertFalse(governance.isVoting(accVoter)); + } + + function test_Emits_ProposalVoteRevokedV2EventXtimes_whenAccountHasVotedOnXProposal() public voteForEachNumVoted { @@ -1564,6 +1681,8 @@ contract GovernanceTest_revokeVotes is GovernanceTest { } } +contract GovernanceTest_revokeVotes_L2 is GovernanceTest_L2, GovernanceTest_revokeVotes {} + contract GovernanceTest_vote_WhenProposalIsApproved is GovernanceTest { event ProposalVotedV2( uint256 indexed proposalId, @@ -1572,11 +1691,8 @@ contract GovernanceTest_vote_WhenProposalIsApproved is GovernanceTest { uint256 noVotes, uint256 abstainVotes ); - event ParticipationBaselineUpdated(uint256 participationBaseline); - uint256 proposalId; - function setUp() public { super.setUp(); proposalId = makeValidProposal(); @@ -1689,7 +1805,7 @@ contract GovernanceTest_vote_WhenProposalIsApproved is GovernanceTest { vm.startPrank(accVoter); governance.vote(proposalId, 0, Proposals.VoteValue.Yes); governance.vote(proposalId, 0, Proposals.VoteValue.No); - (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + (uint256 yes, uint256 no, ) = governance.getVoteTotals(proposalId); assertEq(yes, 0); assertEq(no, VOTER_GOLD); } @@ -1698,8 +1814,7 @@ contract GovernanceTest_vote_WhenProposalIsApproved is GovernanceTest { vm.startPrank(accVoter); governance.vote(proposalId, 0, Proposals.VoteValue.Yes); governance.vote(proposalId, 0, Proposals.VoteValue.No); - (uint256 id, uint256 _1, uint256 _2, uint256 yes, uint256 no, uint256 abstain) = governance - .getVoteRecord(accVoter, 0); + (uint256 id, , , uint256 yes, uint256 no, ) = governance.getVoteRecord(accVoter, 0); assertEq(id, proposalId); assertEq(yes, 0); assertEq(no, VOTER_GOLD); @@ -1709,7 +1824,7 @@ contract GovernanceTest_vote_WhenProposalIsApproved is GovernanceTest { vm.startPrank(accVoter); governance.vote(proposalId, 0, Proposals.VoteValue.No); governance.vote(proposalId, 0, Proposals.VoteValue.Abstain); - (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + (, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); assertEq(no, 0); assertEq(abstain, VOTER_GOLD); } @@ -1718,8 +1833,7 @@ contract GovernanceTest_vote_WhenProposalIsApproved is GovernanceTest { vm.startPrank(accVoter); governance.vote(proposalId, 0, Proposals.VoteValue.No); governance.vote(proposalId, 0, Proposals.VoteValue.Abstain); - (uint256 id, uint256 _1, uint256 _2, uint256 yes, uint256 no, uint256 abstain) = governance - .getVoteRecord(accVoter, 0); + (uint256 id, , , , uint256 no, uint256 abstain) = governance.getVoteRecord(accVoter, 0); assertEq(id, proposalId); assertEq(no, 0); assertEq(abstain, VOTER_GOLD); @@ -1729,7 +1843,7 @@ contract GovernanceTest_vote_WhenProposalIsApproved is GovernanceTest { vm.startPrank(accVoter); governance.vote(proposalId, 0, Proposals.VoteValue.Abstain); governance.vote(proposalId, 0, Proposals.VoteValue.Yes); - (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + (uint256 yes, , uint256 abstain) = governance.getVoteTotals(proposalId); assertEq(abstain, 0); assertEq(yes, VOTER_GOLD); } @@ -1738,8 +1852,7 @@ contract GovernanceTest_vote_WhenProposalIsApproved is GovernanceTest { vm.startPrank(accVoter); governance.vote(proposalId, 0, Proposals.VoteValue.Abstain); governance.vote(proposalId, 0, Proposals.VoteValue.Yes); - (uint256 id, uint256 _1, uint256 _2, uint256 yes, uint256 no, uint256 abstain) = governance - .getVoteRecord(accVoter, 0); + (uint256 id, , , uint256 yes, , uint256 abstain) = governance.getVoteRecord(accVoter, 0); assertEq(id, proposalId); assertEq(abstain, 0); assertEq(yes, VOTER_GOLD); @@ -1808,7 +1921,14 @@ contract GovernanceTest_vote_WhenProposalIsApproved is GovernanceTest { } } +contract GovernanceTest_vote_WhenProposalIsApproved_L2 is + GovernanceTest_L2, + GovernanceTest_vote_WhenProposalIsApproved +{} + contract GovernanceTest_vote_WhenProposalIsApprovedAndHaveSigner is GovernanceTest { + address accSigner; + event ProposalVotedV2( uint256 indexed proposalId, address indexed account, @@ -1817,9 +1937,6 @@ contract GovernanceTest_vote_WhenProposalIsApprovedAndHaveSigner is GovernanceTe uint256 abstainVotes ); - address accSigner; - uint256 proposalId; - function setUp() public { super.setUp(); bytes32 voteSignerRole = keccak256(abi.encodePacked("celo.org/core/vote")); @@ -1888,6 +2005,11 @@ contract GovernanceTest_vote_WhenProposalIsApprovedAndHaveSigner is GovernanceTe } } +contract GovernanceTest_vote_WhenProposalIsApprovedAndHaveSigner_L2 is + GovernanceTest_L2, + GovernanceTest_vote_WhenProposalIsApprovedAndHaveSigner +{} + contract GovernanceTest_vote_WhenProposalIsNotApproved is GovernanceTest { event ProposalVotedV2( uint256 indexed proposalId, @@ -1896,7 +2018,6 @@ contract GovernanceTest_vote_WhenProposalIsNotApproved is GovernanceTest { uint256 noVotes, uint256 abstainVotes ); - uint256 proposalId; function setUp() public { super.setUp(); @@ -1969,6 +2090,11 @@ contract GovernanceTest_vote_WhenProposalIsNotApproved is GovernanceTest { } } +contract GovernanceTest_vote_WhenProposalIsNotApproved_L2 is + GovernanceTest_L2, + GovernanceTest_vote_WhenProposalIsNotApproved +{} + contract GovernanceTest_vote_WhenVotingOnDifferentProposalWithSameIndex is GovernanceTest { function test_IgnoreVotesFromPreviousProposal() public { uint256 proposalId1 = makeValidProposal(); @@ -2019,6 +2145,11 @@ contract GovernanceTest_vote_WhenVotingOnDifferentProposalWithSameIndex is Gover } } +contract GovernanceTest_vote_WhenVotingOnDifferentProposalWithSameIndex_L2 is + GovernanceTest_L2, + GovernanceTest_vote_WhenVotingOnDifferentProposalWithSameIndex +{} + contract GovernanceTest_vote_PartiallyWhenProposalIsApproved is GovernanceTest { event ProposalVotedV2( uint256 indexed proposalId, @@ -2030,8 +2161,6 @@ contract GovernanceTest_vote_PartiallyWhenProposalIsApproved is GovernanceTest { event ParticipationBaselineUpdated(uint256 participationBaseline); - uint256 proposalId; - function setUp() public { super.setUp(); proposalId = makeValidProposal(); @@ -2176,8 +2305,10 @@ contract GovernanceTest_vote_PartiallyWhenProposalIsApproved is GovernanceTest { vm.startPrank(accVoter); governance.votePartially(proposalId, 0, 10, 50, 30); governance.votePartially(proposalId, 0, 30, 20, 40); - (uint256 id, uint256 _1, uint256 _2, uint256 yes, uint256 no, uint256 abstain) = governance - .getVoteRecord(accVoter, 0); + (uint256 id, , , uint256 yes, uint256 no, uint256 abstain) = governance.getVoteRecord( + accVoter, + 0 + ); assertEq(id, proposalId); assertEq(yes, 30); assertEq(no, 20); @@ -2247,7 +2378,14 @@ contract GovernanceTest_vote_PartiallyWhenProposalIsApproved is GovernanceTest { } } +contract GovernanceTest_vote_PartiallyWhenProposalIsApproved_L2 is + GovernanceTest_L2, + GovernanceTest_vote_PartiallyWhenProposalIsApproved +{} + contract GovernanceTest_votePartially_WhenProposalIsApprovedAndHaveSigner is GovernanceTest { + address accSigner; + event ProposalVotedV2( uint256 indexed proposalId, address indexed account, @@ -2256,9 +2394,6 @@ contract GovernanceTest_votePartially_WhenProposalIsApprovedAndHaveSigner is Gov uint256 abstainVotes ); - address accSigner; - uint256 proposalId; - function setUp() public { super.setUp(); bytes32 voteSignerRole = keccak256(abi.encodePacked("celo.org/core/vote")); @@ -2362,6 +2497,11 @@ contract GovernanceTest_votePartially_WhenProposalIsApprovedAndHaveSigner is Gov } } +contract GovernanceTest_votePartially_WhenProposalIsApprovedAndHaveSigner_L2 is + GovernanceTest_L2, + GovernanceTest_votePartially_WhenProposalIsApprovedAndHaveSigner +{} + contract GovernanceTest_votePartially_WhenProposalIsNotApproved is GovernanceTest { event ProposalVotedV2( uint256 indexed proposalId, @@ -2370,7 +2510,6 @@ contract GovernanceTest_votePartially_WhenProposalIsNotApproved is GovernanceTes uint256 noVotes, uint256 abstainVotes ); - uint256 proposalId; function setUp() public { super.setUp(); @@ -2443,6 +2582,11 @@ contract GovernanceTest_votePartially_WhenProposalIsNotApproved is GovernanceTes } } +contract GovernanceTest_votePartially_WhenProposalIsNotApproved_L2 is + GovernanceTest_L2, + GovernanceTest_votePartially_WhenProposalIsNotApproved +{} + contract GovernanceTest_votePartially_WhenVotingOnDifferentProposalWithSameIndex is GovernanceTest { function test_IgnoreVotesFromPreviousProposal() public { uint256 proposalId1 = makeValidProposal(); @@ -2493,19 +2637,15 @@ contract GovernanceTest_votePartially_WhenVotingOnDifferentProposalWithSameIndex } } +contract GovernanceTest_votePartially_WhenVotingOnDifferentProposalWithSameIndex_L2 is + GovernanceTest_L2, + GovernanceTest_votePartially_WhenVotingOnDifferentProposalWithSameIndex +{} + contract GovernanceTest_execute is GovernanceTest { event ParticipationBaselineUpdated(uint256 participationBaseline); event ProposalExecuted(uint256 indexed proposalId); - uint256 proposalId; - - function setupProposalCanExecute() private { - proposalId = makeAndApproveProposal(0); - vm.prank(accVoter); - governance.vote(proposalId, 0, Proposals.VoteValue.Yes); - vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); - } - function test_returnTrue_WhenProposalCanExecute() public { setupProposalCanExecute(); assertTrue(governance.execute(proposalId, 0)); @@ -2550,16 +2690,6 @@ contract GovernanceTest_execute is GovernanceTest { governance.execute(proposalId, 1); } - function setupWhenProposalApprovedInExecutionStage() private { - proposalId = makeValidProposal(); - vm.warp(block.timestamp + governance.dequeueFrequency()); - vm.prank(accVoter); - governance.vote(proposalId, 0, Proposals.VoteValue.Yes); - vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); - vm.prank(accApprover); - governance.approve(proposalId, 0); - } - function test_returnTrue_WhenProposalApprovedInExecutionStage() public { setupWhenProposalApprovedInExecutionStage(); assertTrue(governance.execute(proposalId, 0)); @@ -2659,23 +2789,6 @@ contract GovernanceTest_execute is GovernanceTest { governance.execute(proposalId, 0); } - function setup2TxProposal() private { - proposalId = governance.propose.value(DEPOSIT)( - twoTxProp.values, - twoTxProp.destinations, - twoTxProp.data, - twoTxProp.dataLengths, - twoTxProp.description - ); - vm.warp(block.timestamp + governance.dequeueFrequency()); - - vm.prank(accApprover); - governance.approve(proposalId, 0); - vm.prank(accVoter); - governance.vote(proposalId, 0, Proposals.VoteValue.Yes); - vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); - } - function test_returnTrue_When2TxProposal() public { setup2TxProposal(); assertTrue(governance.execute(proposalId, 0)); @@ -2771,14 +2884,6 @@ contract GovernanceTest_execute is GovernanceTest { governance.execute(proposalId, 0); } - function setupProposalPastExecutionStage() private { - proposalId = makeAndApproveProposal(0); - vm.prank(accVoter); - governance.vote(proposalId, 0, Proposals.VoteValue.Yes); - vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); - vm.warp(block.timestamp + governance.getExecutionStageDuration()); - } - function test_returnFalse_WhenProposalIsPastExecutionStage() public { setupProposalPastExecutionStage(); assertFalse(governance.execute(proposalId, 0)); @@ -2924,39 +3029,166 @@ contract GovernanceTest_execute is GovernanceTest { emit ParticipationBaselineUpdated(expectedParticipationBaseline); governance.execute(proposalId, 0); } + + function setupProposalPastExecutionStage() private { + proposalId = makeAndApproveProposal(0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.warp(block.timestamp + governance.getExecutionStageDuration()); + } + + function setup2TxProposal() private { + proposalId = governance.propose.value(DEPOSIT)( + twoTxProp.values, + twoTxProp.destinations, + twoTxProp.data, + twoTxProp.dataLengths, + twoTxProp.description + ); + vm.warp(block.timestamp + governance.dequeueFrequency()); + + vm.prank(accApprover); + governance.approve(proposalId, 0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + } + + function setupWhenProposalApprovedInExecutionStage() private { + proposalId = makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + vm.prank(accApprover); + governance.approve(proposalId, 0); + } + + function setupProposalCanExecute() private { + proposalId = makeAndApproveProposal(0); + vm.prank(accVoter); + governance.vote(proposalId, 0, Proposals.VoteValue.Yes); + vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); + } } +contract GovernanceTest_execute_L2 is GovernanceTest_L2, GovernanceTest_execute {} + contract GovernanceTest_approveHotfix is GovernanceTest { - event HotfixApproved(bytes32 indexed hash); + bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + event HotfixApproved(bytes32 indexed hash, address approver); + + function test_markHotfixRecordApprovedWhenCalledByApprover() public { + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + (bool approved, , ) = governance.getL1HotfixRecord(HOTFIX_HASH); + assertTrue(approved); + } + + function test_Emits_HotfixApprovedEvent() public { + vm.expectEmit(true, true, true, true); + emit HotfixApproved(HOTFIX_HASH, accApprover); + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + } + + function test_Reverts_WhenCalledByNonApproverOrCouncil() public { + vm.expectRevert("msg.sender not approver or Security Council"); + governance.approveHotfix(HOTFIX_HASH); + } + + function test_Reverts_WhenCalledBySecurityCouncilOnL1() public { + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + + vm.prank(accCouncil); + vm.expectRevert("Hotfix approval by security council is not available on L1."); + governance.approveHotfix(HOTFIX_HASH); + } + + function test_Reverts_WhenCalledByZeroAddressOnL1() public { + vm.prank(address(0)); + vm.expectRevert("msg.sender cannot be address zero"); + governance.approveHotfix(HOTFIX_HASH); + } +} +contract GovernanceTest_approveHotfix_L2 is GovernanceTest { bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + event HotfixApproved(bytes32 indexed hash, address approver); + function setUp() public { + super.setUp(); + + _whenL2(); + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + } function test_markHotfixRecordApprovedWhenCalledByApprover() public { vm.prank(accApprover); governance.approveHotfix(HOTFIX_HASH); - (bool approved, , ) = governance.getHotfixRecord(HOTFIX_HASH); + + (bool approved, , , ) = governance.getL2HotfixRecord(HOTFIX_HASH); + assertTrue(approved); + } + function test_markHotfixRecordApprovedWhenCalledBySecurityCouncil() public { + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); + + (, bool approved, , ) = governance.getL2HotfixRecord(HOTFIX_HASH); assertTrue(approved); } - function test_emitHotfixApprovedEvent() public { + function test_Emits_HotfixApprovedEvent() public { + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + vm.expectEmit(true, true, true, true); - emit HotfixApproved(HOTFIX_HASH); + emit HotfixApproved(HOTFIX_HASH, accApprover); vm.prank(accApprover); governance.approveHotfix(HOTFIX_HASH); + + vm.expectEmit(true, true, true, true); + emit HotfixApproved(HOTFIX_HASH, accCouncil); + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); } - function test_RevertIf_CalledByNonApprover() public { - vm.expectRevert("msg.sender not approver"); + function test_Reverts_WhenCalledByNonApproverOrCouncil() public { + vm.expectRevert("msg.sender not approver or Security Council"); + governance.approveHotfix(HOTFIX_HASH); + } + + function test_Reverts_WhenSecurityCouncilIsNotSet() public { + vm.prank(accCouncil); + vm.expectRevert("msg.sender not approver or Security Council"); + governance.approveHotfix(HOTFIX_HASH); + + vm.expectRevert("msg.sender cannot be address zero"); + vm.prank(address(0)); governance.approveHotfix(HOTFIX_HASH); } } -contract GovernanceTest_whitelistHotfix is GovernanceTest { +contract GovernanceTest_whitelistHotfix_setup is GovernanceTest { + bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); event HotfixWhitelisted(bytes32 indexed hash, address whitelister); +} - bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); +contract GovernanceTest_whitelistHotfix is GovernanceTest_whitelistHotfix_setup { + function test_ShouldWhitelistHotfixByValidator() public { + address validator = actor("validator1"); + governance.addValidator(validator); + vm.prank(validator); + governance.whitelistHotfix(HOTFIX_HASH); - function test_EmitHotfixWhitelistEvent() public { + assertTrue(governance.isHotfixWhitelistedBy(HOTFIX_HASH, validator)); + } + function test_Emits_HotfixWhitelistEvent() public { address validator = actor("validator1"); governance.addValidator(validator); governance.addValidator(actor("validator2")); @@ -2968,7 +3200,20 @@ contract GovernanceTest_whitelistHotfix is GovernanceTest { } } -contract GovernanceTest_hotfixWhitelistValidatorTally is GovernanceTest { +contract GovernanceTest_whitelistHotfix_L2 is + GovernanceTest_L2, + GovernanceTest_whitelistHotfix_setup +{ + function test_Reverts_WhenCalled() public { + address validator = actor("validator1"); + governance.addValidator(validator); + vm.expectRevert("This method is no longer supported in L2."); + vm.prank(validator); + governance.whitelistHotfix(HOTFIX_HASH); + } +} + +contract GovernanceTest_hotfixWhitelistValidatorTally_setup is GovernanceTest { bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); address[] validators; @@ -2991,7 +3236,11 @@ contract GovernanceTest_hotfixWhitelistValidatorTally is GovernanceTest { signers.push(signer); } } +} +contract GovernanceTest_hotfixWhitelistValidatorTally is + GovernanceTest_hotfixWhitelistValidatorTally_setup +{ function test_countValidatorAccountsThatHaveWhitelisted() public { for (uint256 i = 0; i < 3; i++) { vm.prank(validators[i]); @@ -3035,72 +3284,93 @@ contract GovernanceTest_hotfixWhitelistValidatorTally is GovernanceTest { } } -contract GovernanceTest_isHotfixPassing is GovernanceTest { +contract GovernanceTest_hotfixWhitelistValidatorTally_L2 is + GovernanceTest_L2, + GovernanceTest_hotfixWhitelistValidatorTally_setup +{ + function test_Reverts_WhenCalled() public { + address validator = actor("validator1"); + governance.addValidator(validator); + vm.expectRevert("This method is no longer supported in L2."); + governance.hotfixWhitelistValidatorTally(HOTFIX_HASH); + } +} + +contract GovernanceTest_isHotfixPassing_setup is GovernanceTest { bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + address validator1; + address validator2; function setUp() public { super.setUp(); - address val1 = actor("validator1"); - governance.addValidator(val1); - vm.prank(val1); + validator1 = actor("validator1"); + governance.addValidator(validator1); + vm.prank(validator1); accounts.createAccount(); - address val2 = actor("validator2"); - governance.addValidator(val2); - vm.prank(val2); + validator2 = actor("validator2"); + governance.addValidator(validator2); + vm.prank(validator2); accounts.createAccount(); } +} +contract GovernanceTest_isHotfixPassing is GovernanceTest_isHotfixPassing_setup { function test_returnFalseWhenHotfixHasNotBeenWhitelisted() public { assertFalse(governance.isHotfixPassing(HOTFIX_HASH)); } function test_returnFalseWhenHotfixHasBeenWhitelistedButNotByQuorum() public { - vm.prank(actor("validator1")); + vm.prank(validator1); governance.whitelistHotfix(HOTFIX_HASH); assertFalse(governance.isHotfixPassing(HOTFIX_HASH)); } function test_returnTrueWhenHotfixIsWhitelistedByQuorum() public { - vm.prank(actor("validator1")); + vm.prank(validator1); governance.whitelistHotfix(HOTFIX_HASH); - vm.prank(actor("validator2")); + vm.prank(validator2); governance.whitelistHotfix(HOTFIX_HASH); assertTrue(governance.isHotfixPassing(HOTFIX_HASH)); } } -contract GovernanceTest_prepareHotfix is GovernanceTest { - event HotfixPrepared(bytes32 indexed hash, uint256 indexed epoch); +contract GovernanceTest_isHotfixPassing_L2 is + GovernanceTest_L2, + GovernanceTest_isHotfixPassing_setup +{ + function test_Reverts_WhenCalled() public { + vm.expectRevert("This method is no longer supported in L2."); + governance.isHotfixPassing(HOTFIX_HASH); + } +} +contract GovernanceTest_prepareHotfix is GovernanceTest { bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + address validator1; + event HotfixPrepared(bytes32 indexed hash, uint256 indexed epoch); function setUp() public { super.setUp(); - address val1 = actor("validator1"); - governance.addValidator(val1); - vm.prank(val1); + validator1 = actor("validator1"); + governance.addValidator(validator1); + vm.prank(validator1); accounts.createAccount(); } - function test_RevertIf_HotfixIsNotPassing() public { - vm.expectRevert("hotfix not whitelisted by 2f+1 validators"); - governance.prepareHotfix(HOTFIX_HASH); - } - function test_markHotfixRecordPreparedEpoch_whenHotfixIsPassing() public { vm.roll(block.number + governance.getEpochSize()); - vm.prank(actor("validator1")); + vm.prank(validator1); governance.whitelistHotfix(HOTFIX_HASH); governance.prepareHotfix(HOTFIX_HASH); - (, , uint256 preparedEpoch) = governance.getHotfixRecord(HOTFIX_HASH); + (, , uint256 preparedEpoch) = governance.getL1HotfixRecord(HOTFIX_HASH); assertEq(preparedEpoch, governance.getEpochNumber()); } function test_emitHotfixPreparedEvent_whenHotfixIsPassing() public { vm.roll(block.number + governance.getEpochSize()); - vm.prank(actor("validator1")); + vm.prank(validator1); governance.whitelistHotfix(HOTFIX_HASH); uint256 epoch = governance.getEpochNumber(); @@ -3109,51 +3379,251 @@ contract GovernanceTest_prepareHotfix is GovernanceTest { governance.prepareHotfix(HOTFIX_HASH); } - function test_RevertIf_EpochEqualsPreparedEpoch_whenHotfixIsPassing() public { + function test_succeedForEpochDifferentPreparedEpoch_whenHotfixIsPassing() public { vm.roll(block.number + governance.getEpochSize()); - vm.prank(actor("validator1")); + vm.prank(validator1); governance.whitelistHotfix(HOTFIX_HASH); governance.prepareHotfix(HOTFIX_HASH); - vm.expectRevert("hotfix already prepared for this epoch"); + vm.roll(block.number + governance.getEpochSize()); governance.prepareHotfix(HOTFIX_HASH); } - function test_succeedForEpochDifferentPreparedEpoch_whenHotfixIsPassing() public { + function test_Reverts_IfHotfixIsNotPassing() public { + vm.expectRevert("hotfix not whitelisted by 2f+1 validators"); + governance.prepareHotfix(HOTFIX_HASH); + } + + function test_Reverts_IfEpochEqualsPreparedEpoch_whenHotfixIsPassing() public { vm.roll(block.number + governance.getEpochSize()); - vm.prank(actor("validator1")); + vm.prank(validator1); governance.whitelistHotfix(HOTFIX_HASH); governance.prepareHotfix(HOTFIX_HASH); - vm.roll(block.number + governance.getEpochSize()); + vm.expectRevert("hotfix already prepared for this epoch"); governance.prepareHotfix(HOTFIX_HASH); } } -contract GovernanceTest_executeHotfix is GovernanceTest { - event HotfixExecuted(bytes32 indexed hash); - - bytes32 SALT = 0x657ed9d64e84fa3d1af43b3a307db22aba2d90a158015df1c588c02e24ca08f0; - bytes32 hotfixHash; - - address validator; +contract GovernanceTest_prepareHotfix_L2 is GovernanceTest { + bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + event HotfixPrepared(bytes32 indexed hash, uint256 indexed epoch); function setUp() public { super.setUp(); - validator = actor("validator"); - vm.prank(validator); - accounts.createAccount(); - governance.addValidator(validator); + _whenL2(); + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + } - // call governance test method to generate proper hotfix (needs calldata arguments) - hotfixHash = governance.getHotfixHash( - okProp.values, - okProp.destinations, + function test_shouldMarkHotfixRecordExecutionTimeLimit_whenHotfixApproved() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + + governance.prepareHotfix(HOTFIX_HASH); + (, , , uint256 preparedTimeLimit) = governance.getL2HotfixRecord(HOTFIX_HASH); + + assertEq(preparedTimeLimit, block.timestamp + DAY); + } + + function test_ShouldUpdateExecutionTimeLimitAfterApproval_WhenHotfixRecordWasReset() public { + uint256 _preparedTimeLimit; + bool _approved; + bool _councilApproved; + + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + + governance.prepareHotfix(HOTFIX_HASH); + timeTravel(DAY + 3600); + governance.resetHotFixRecord(HOTFIX_HASH); + + (_approved, _councilApproved, , _preparedTimeLimit) = governance.getL2HotfixRecord(HOTFIX_HASH); + + assertFalse(_approved); + assertFalse(_councilApproved); + assertEq(_preparedTimeLimit, 0); + + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + + governance.prepareHotfix(HOTFIX_HASH); + (_approved, _councilApproved, , _preparedTimeLimit) = governance.getL2HotfixRecord(HOTFIX_HASH); + + assertTrue(_approved); + assertTrue(_councilApproved); + assertEq(_preparedTimeLimit, block.timestamp + DAY); + } + + function test_Emits_HotfixPreparedEvent_whenHotfixApproved() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + + vm.expectEmit(true, true, true, true); + emit HotfixPrepared(HOTFIX_HASH, block.timestamp + DAY); + governance.prepareHotfix(HOTFIX_HASH); + } + + function test_Reverts_IfHotfixExecutionTimeWindowNotSet() public { + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + + vm.expectRevert("Hotfix execution time window not set"); + governance.prepareHotfix(HOTFIX_HASH); + } + function test_Reverts_IfHotfixIsNotApproved() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + + vm.expectRevert("Hotfix not approved by approvers."); + governance.prepareHotfix(HOTFIX_HASH); + + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + vm.expectRevert("Hotfix not approved by security council."); + governance.prepareHotfix(HOTFIX_HASH); + } + + function test_Reverts_IfPreparedTwiceWithinExecutionTimeLimit() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + + governance.prepareHotfix(HOTFIX_HASH); + vm.expectRevert("Hotfix already prepared for this timeframe."); + governance.prepareHotfix(HOTFIX_HASH); + } +} + +contract GovernanceTest_resetHotfix_setup is GovernanceTest { + bytes32 constant HOTFIX_HASH = bytes32(uint256(0x123456789)); + bytes32 constant SALT = 0x657ed9d64e84fa3d1af43b3a307db22aba2d90a158015df1c588c02e24ca08f0; + bytes32 hotfixHash; + address validator1; + event HotfixRecordReset(bytes32 indexed hash); + + function setUp() public { + super.setUp(); + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + + hotfixHash = governance.getHotfixHash( + okProp.values, + okProp.destinations, okProp.data, okProp.dataLengths, SALT ); } +} + +contract GovernanceTest_resetHotfix is GovernanceTest_resetHotfix_setup { + function setUp() public { + super.setUp(); + + validator1 = actor("validator1"); + governance.addValidator(validator1); + vm.prank(validator1); + accounts.createAccount(); + } + + function test_Reverts_whenCalledOnL1() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + + (bool approved, , ) = governance.getHotfixRecord(HOTFIX_HASH); + + assertTrue(approved); + + vm.roll(block.number + governance.getEpochSize()); + vm.prank(validator1); + governance.whitelistHotfix(HOTFIX_HASH); + + uint256 epoch = governance.getEpochNumber(); + + governance.prepareHotfix(HOTFIX_HASH); + + timeTravel(DAY + 1); + + vm.expectRevert("hotfix not prepared"); + governance.resetHotFixRecord(HOTFIX_HASH); + } +} + +contract GovernanceTest_resetHotfix_L2 is GovernanceTest_L2, GovernanceTest_resetHotfix_setup { + function test_ShouldResetHotfixRecordWhenExecutionTimeLimitHasPassed() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); + + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + + (bool approved, bool councilApproved, , uint256 _preparedTimeLimit) = governance + .getL2HotfixRecord(HOTFIX_HASH); + + assertTrue(approved); + assertTrue(councilApproved); + + governance.prepareHotfix(HOTFIX_HASH); + timeTravel(DAY + 1); + governance.resetHotFixRecord(HOTFIX_HASH); + + (approved, councilApproved, , _preparedTimeLimit) = governance.getL2HotfixRecord(HOTFIX_HASH); + assertFalse(approved); + assertFalse(councilApproved); + } + function test_Emits_HotfixRecordResetWhenExecutionTimeLimitHasPassed() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + + governance.prepareHotfix(HOTFIX_HASH); + timeTravel(DAY + 1); + vm.expectEmit(true, true, true, true); + emit HotfixRecordReset(HOTFIX_HASH); + governance.resetHotFixRecord(HOTFIX_HASH); + } + + function test_Reverts_WhenHotfixAlreadyExecuted() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + + vm.prank(accCouncil); + governance.approveHotfix(hotfixHash); + vm.prank(accApprover); + governance.approveHotfix(hotfixHash); + + governance.prepareHotfix(hotfixHash); - function executeHotfixTx() private { governance.executeHotfix( okProp.values, okProp.destinations, @@ -3161,23 +3631,67 @@ contract GovernanceTest_executeHotfix is GovernanceTest { okProp.dataLengths, SALT ); + vm.expectRevert("hotfix already executed"); + governance.resetHotFixRecord(hotfixHash); } + function test_Reverts_WhenHotfixNotPrepared() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); - function approveAndPrepareHotfix() private { + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); vm.prank(accApprover); - governance.approveHotfix(hotfixHash); - vm.roll(block.number + governance.getEpochSize()); + governance.approveHotfix(HOTFIX_HASH); + + vm.expectRevert("hotfix not prepared"); + governance.resetHotFixRecord(HOTFIX_HASH); + } + function test_Reverts_WhenExecutionTimeLimitNotReached() public { + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + + vm.prank(accCouncil); + governance.approveHotfix(HOTFIX_HASH); + vm.prank(accApprover); + governance.approveHotfix(HOTFIX_HASH); + + governance.prepareHotfix(HOTFIX_HASH); + vm.expectRevert("hotfix execution time limit not reached"); + governance.resetHotFixRecord(HOTFIX_HASH); + } +} + +contract GovernanceTest_executeHotfix is GovernanceTest { + bytes32 SALT = 0x657ed9d64e84fa3d1af43b3a307db22aba2d90a158015df1c588c02e24ca08f0; + bytes32 hotfixHash; + + address validator; + + event HotfixExecuted(bytes32 indexed hash); + + function setUp() public { + super.setUp(); + validator = actor("validator"); vm.prank(validator); - governance.whitelistHotfix(hotfixHash); - governance.prepareHotfix(hotfixHash); + accounts.createAccount(); + governance.addValidator(validator); + + // call governance test method to generate proper hotfix (needs calldata arguments) + hotfixHash = governance.getHotfixHash( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + SALT + ); } - function test_RevertIf_hotfixNotApproved() public { + function test_Reverts_IfHotfixNotApproved() public { vm.expectRevert("hotfix not approved"); executeHotfixTx(); } - function test_RevertIf_hotfixNotPreparedForCurrentEpoch() public { + function test_Reverts_IfHotfixNotPreparedForCurrentEpoch() public { vm.roll(block.number + governance.getEpochSize()); vm.prank(accApprover); governance.approveHotfix(hotfixHash); @@ -3186,7 +3700,7 @@ contract GovernanceTest_executeHotfix is GovernanceTest { executeHotfixTx(); } - function test_RevertIf_hotfixPreparedButNotForCurrentEpoch() public { + function test_Reverts_IfHotfixPreparedButNotForCurrentEpoch() public { vm.prank(accApprover); governance.approveHotfix(hotfixHash); vm.prank(validator); @@ -3206,7 +3720,7 @@ contract GovernanceTest_executeHotfix is GovernanceTest { function test_markHotfixAsExecuted_WhenApprovedAndPreparedForCurrentEpoch() public { approveAndPrepareHotfix(); executeHotfixTx(); - (, bool executed, ) = governance.getHotfixRecord(hotfixHash); + (, bool executed, ) = governance.getL1HotfixRecord(hotfixHash); assertTrue(executed); } @@ -3223,11 +3737,132 @@ contract GovernanceTest_executeHotfix is GovernanceTest { vm.expectRevert("hotfix already executed"); executeHotfixTx(); } + + function executeHotfixTx() private { + governance.executeHotfix( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + SALT + ); + } + + function approveAndPrepareHotfix() private { + vm.prank(accApprover); + governance.approveHotfix(hotfixHash); + vm.roll(block.number + governance.getEpochSize()); + vm.prank(validator); + governance.whitelistHotfix(hotfixHash); + governance.prepareHotfix(hotfixHash); + } } -contract GovernanceTest_isVoting is GovernanceTest { - uint256 proposalId; +contract GovernanceTest_executeHotfix_L2 is GovernanceTest { + bytes32 SALT = 0x657ed9d64e84fa3d1af43b3a307db22aba2d90a158015df1c588c02e24ca08f0; + bytes32 hotfixHash; + + address validator; + + event HotfixExecuted(bytes32 indexed hash); + function setUp() public { + super.setUp(); + + _whenL2(); + vm.prank(accOwner); + governance.setSecurityCouncil(accCouncil); + vm.prank(accOwner); + governance.setHotfixExecutionTimeWindow(DAY); + + hotfixHash = governance.getHotfixHash( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + SALT + ); + } + + function test_ShouldExecuteHotfix_WhenApprovedByApproverAndSecurityCouncil() public { + approveAndPrepareHotfix(); + + executeHotfixTx(); + assertEq(testTransactions.getValue(1), 1); + } + + function test_ShouldMarkHotfixAsExecuted_WhenApprovedByApproverAndSecurityCouncil() public { + approveAndPrepareHotfix(); + + executeHotfixTx(); + (, , bool executed, ) = governance.getL2HotfixRecord(hotfixHash); + assertTrue(executed); + } + + function test_Emits_HotfixExecutedEventWhenApprovedByApproverAndSecurityCouncil() public { + approveAndPrepareHotfix(); + + vm.expectEmit(true, true, true, true); + emit HotfixExecuted(hotfixHash); + executeHotfixTx(); + } + + function test_Reverts_WhenExecutingSameHotfixTwice() public { + approveAndPrepareHotfix(); + + executeHotfixTx(); + vm.expectRevert("hotfix already executed"); + executeHotfixTx(); + } + + function test_Reverts_IfHotfixNotApprovedByApprover() public { + vm.expectRevert("hotfix not approved"); + executeHotfixTx(); + } + + function test_Reverts_IfHotfixNotApprovedBySecurityCouncil() public { + vm.prank(accApprover); + governance.approveHotfix(hotfixHash); + vm.expectRevert("hotfix not approved by security council"); + executeHotfixTx(); + } + + function test_Reverts_WhenHotfixNotPrepared() public { + vm.prank(accApprover); + governance.approveHotfix(hotfixHash); + vm.prank(accCouncil); + governance.approveHotfix(hotfixHash); + vm.expectRevert("Execution time limit has already been reached."); + executeHotfixTx(); + } + function test_Reverts_WhenExecutedBeyondTheExecutionTimeLimit() public { + approveAndPrepareHotfix(); + + timeTravel(2 * DAY); + vm.expectRevert("Execution time limit has already been reached."); + executeHotfixTx(); + } + + function executeHotfixTx() private { + governance.executeHotfix( + okProp.values, + okProp.destinations, + okProp.data, + okProp.dataLengths, + SALT + ); + } + + function approveAndPrepareHotfix() private { + vm.prank(accApprover); + governance.approveHotfix(hotfixHash); + vm.prank(accCouncil); + governance.approveHotfix(hotfixHash); + governance.prepareHotfix(hotfixHash); + } +} + +contract GovernanceTest_isVoting is GovernanceTest { function setUp() public { super.setUp(); proposalId = makeValidProposal(); @@ -3281,8 +3916,9 @@ contract GovernanceTest_isVoting is GovernanceTest { } } +contract GovernanceTest_isVoting_L2 is GovernanceTest_L2, GovernanceTest_isVoting {} + contract GovernanceTest_isProposalPassing is GovernanceTest { - uint256 proposalId; address accSndVoter; function setUp() public { @@ -3324,6 +3960,11 @@ contract GovernanceTest_isProposalPassing is GovernanceTest { } } +contract GovernanceTest_isProposalPassing_L2 is + GovernanceTest_L2, + GovernanceTest_isProposalPassing +{} + contract GovernanceTest_dequeueProposalsIfReady is GovernanceTest { function test_notUpdateLastDequeueWhenThereAreNoQueuedProposals() public { uint256 originalLastDequeue = governance.lastDequeue(); @@ -3357,6 +3998,11 @@ contract GovernanceTest_dequeueProposalsIfReady is GovernanceTest { } } +contract GovernanceTest_dequeueProposalsIfReady_L2 is + GovernanceTest_L2, + GovernanceTest_dequeueProposalsIfReady +{} + contract GovernanceTest_getProposalStage is GovernanceTest { function test_returnNoneStageWhenProposalDoesNotExists() public { assertEq(uint256(governance.getProposalStage(0)), uint256(Proposals.Stage.None)); @@ -3364,25 +4010,25 @@ contract GovernanceTest_getProposalStage is GovernanceTest { } function test_returnQueuedWhenNotExpired() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Queued)); } function test_returnExpirationWhenExpired() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.queueExpiry()); assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Expiration)); } function test_returnReferendumWhenNotVotedAndNotExpired() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.dequeueProposalsIfReady(); assertEq(uint256(governance.getProposalStage(proposalId)), uint256(Proposals.Stage.Referendum)); } function test_returnExpirationWhenExpiredButDequeued() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.dequeueProposalsIfReady(); vm.warp(block.timestamp + REFERENDUM_STAGE_DURATION); @@ -3390,7 +4036,7 @@ contract GovernanceTest_getProposalStage is GovernanceTest { } function test_returnReferendumWhenNotExpiredButApproved() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.dequeueProposalsIfReady(); vm.prank(accApprover); @@ -3400,7 +4046,7 @@ contract GovernanceTest_getProposalStage is GovernanceTest { } function test_returnExpirationWhenExpiredButApproved() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.dequeueProposalsIfReady(); vm.prank(accApprover); @@ -3410,7 +4056,7 @@ contract GovernanceTest_getProposalStage is GovernanceTest { } function test_returnExecutionWhenInExecutionStageAndNotExpired() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.dequeueProposalsIfReady(); vm.prank(accApprover); @@ -3422,7 +4068,7 @@ contract GovernanceTest_getProposalStage is GovernanceTest { } function test_returnExpirationWhenExpiredAfterExecutionState() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.dequeueProposalsIfReady(); vm.prank(accApprover); @@ -3437,7 +4083,7 @@ contract GovernanceTest_getProposalStage is GovernanceTest { } function test_returnExpirationPastTheExecutionStageWhenNotApproved_WithEmptyProposal() public { - uint256 proposalId = makeEmptyProposal(); + proposalId = makeEmptyProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.dequeueProposalsIfReady(); vm.prank(accVoter); @@ -3450,7 +4096,7 @@ contract GovernanceTest_getProposalStage is GovernanceTest { } function test_returnExpirationPastTheExecutionStageWhenNotPassing_WithEmptyProposal() public { - uint256 proposalId = makeEmptyProposal(); + proposalId = makeEmptyProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.dequeueProposalsIfReady(); vm.prank(accApprover); @@ -3463,7 +4109,7 @@ contract GovernanceTest_getProposalStage is GovernanceTest { } function test_returnExecutionWhenInExecutionStageAndNotExpired_WithEmptyProposal() public { - uint256 proposalId = makeEmptyProposal(); + proposalId = makeEmptyProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.dequeueProposalsIfReady(); vm.prank(accApprover); @@ -3475,7 +4121,7 @@ contract GovernanceTest_getProposalStage is GovernanceTest { } function test_returnExecutionPastTheExecutionStageIfPassedAndApproved_WithEmptyProposal() public { - uint256 proposalId = makeEmptyProposal(); + proposalId = makeEmptyProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); governance.dequeueProposalsIfReady(); vm.prank(accApprover); @@ -3489,21 +4135,9 @@ contract GovernanceTest_getProposalStage is GovernanceTest { } } -contract GovernanceTest_getAmountOfGoldUsedForVoting is GovernanceTest { - function makeAndApprove3ConcurrentProposals() private { - vm.prank(accOwner); - governance.setConcurrentProposals(3); - makeValidProposal(); - makeValidProposal(); - makeValidProposal(); - vm.warp(block.timestamp + governance.dequeueFrequency()); - vm.startPrank(accApprover); - governance.approve(1, 0); - governance.approve(2, 1); - governance.approve(3, 2); - vm.stopPrank(); - } +contract GovernanceTest_getProposalStage_L2 is GovernanceTest_L2, GovernanceTest_getProposalStage {} +contract GovernanceTest_getAmountOfGoldUsedForVoting is GovernanceTest { function test_showCorrectNumberOfVotes_whenVotingOn1ConcurrentProposal() public { makeAndApprove3ConcurrentProposals(); @@ -3543,7 +4177,7 @@ contract GovernanceTest_getAmountOfGoldUsedForVoting is GovernanceTest { } function test_returnNumberOfVotes_WhenDequeuedAndVotingHappenInV8() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); vm.prank(accApprover); governance.approve(proposalId, 0); @@ -3552,7 +4186,7 @@ contract GovernanceTest_getAmountOfGoldUsedForVoting is GovernanceTest { } function test_returnNumberOfVotes_whenDequeuedAndVotedPartially() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); vm.prank(accApprover); governance.approve(proposalId, 0); @@ -3564,7 +4198,7 @@ contract GovernanceTest_getAmountOfGoldUsedForVoting is GovernanceTest { } function test_return0Votes_whenDequeuedAndVotedPartiallyButExpired() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); vm.prank(accApprover); governance.approve(proposalId, 0); @@ -3577,7 +4211,7 @@ contract GovernanceTest_getAmountOfGoldUsedForVoting is GovernanceTest { } function test_return0Votes_WhenIndexOfProposalGetsReused() public { - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.warp(block.timestamp + governance.dequeueFrequency()); vm.prank(accApprover); governance.approve(proposalId, 0); @@ -3600,7 +4234,7 @@ contract GovernanceTest_getAmountOfGoldUsedForVoting is GovernanceTest { function test_returnFullWeightWhenUpvoting_WhenProposalInQueue() public { vm.prank(accOwner); governance.setConcurrentProposals(3); - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.prank(accVoter); governance.upvote(proposalId, 0, 0); assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), VOTER_GOLD); @@ -3609,14 +4243,33 @@ contract GovernanceTest_getAmountOfGoldUsedForVoting is GovernanceTest { function test_return0IfProposalExpired_WhenProposalInQueue() public { vm.prank(accOwner); governance.setConcurrentProposals(3); - uint256 proposalId = makeValidProposal(); + proposalId = makeValidProposal(); vm.prank(accVoter); governance.upvote(proposalId, 0, 0); vm.warp(block.timestamp + governance.queueExpiry()); assertEq(governance.getAmountOfGoldUsedForVoting(accVoter), 0); } + + function makeAndApprove3ConcurrentProposals() private { + vm.prank(accOwner); + governance.setConcurrentProposals(3); + makeValidProposal(); + makeValidProposal(); + makeValidProposal(); + vm.warp(block.timestamp + governance.dequeueFrequency()); + vm.startPrank(accApprover); + governance.approve(1, 0); + governance.approve(2, 1); + governance.approve(3, 2); + vm.stopPrank(); + } } +contract GovernanceTest_getAmountOfGoldUsedForVoting_L2 is + GovernanceTest_L2, + GovernanceTest_getAmountOfGoldUsedForVoting +{} + contract GovernanceTest_removeVotesWhenRevokingDelegatedVotes is GovernanceTest { uint256[] proposalIds; @@ -3629,13 +4282,82 @@ contract GovernanceTest_removeVotesWhenRevokingDelegatedVotes is GovernanceTest governance.removeVotesWhenRevokingDelegatedVotesTest(address(0), 0); } + function test_adjustVotesCorrectlyTo0_WhenVotingOnlyforYes() public { + makeAndApprove3Proposals(); + setUpVotingOnlyforYes(); + governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, 0); + + assertVoteRecord(0, proposalIds[0], 0, 0, 0); + assertVoteRecord(1, proposalIds[1], 0, 0, 0); + + assertVotesTotal(proposalIds[0], 0, 0, 0); + assertVotesTotal(proposalIds[1], 0, 0, 0); + } + + function test_adjust_votes_correctly_to_30_WhenVotingOnlyForYes() public { + makeAndApprove3Proposals(); + setUpVotingOnlyforYes(); + governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, 30); + + assertVoteRecord(0, proposalIds[0], 30, 0, 0); + assertVoteRecord(1, proposalIds[1], 0, 30, 0); + + assertVotesTotal(proposalIds[0], 30, 0, 0); + assertVotesTotal(proposalIds[1], 0, 30, 0); + } + + function test_adjustVotesCorrectlyTo0_WhenVotingForAllChoices() public { + makeAndApprove3Proposals(); + setupVotingForAllChoices(); + + governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, 0); + + assertVoteRecord(0, proposalIds[0], 0, 0, 0); + assertVoteRecord(1, proposalIds[1], 0, 0, 0); + } + + function test_adjustVotesCorrectlyTo50_WhenVotingForAllChoices() public { + makeAndApprove3Proposals(); + setupVotingForAllChoices(); + + uint256 maxAmount = 50; // means that votes will be halved + governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, maxAmount); + + (uint256 yes0, uint256 no0, uint256 abstain0) = governance.getVoteTotals(proposalIds[0]); + (uint256 yes1, uint256 no1, uint256 abstain1) = governance.getVoteTotals(proposalIds[1]); + (uint256 yes2, uint256 no2, uint256 abstain2) = governance.getVoteTotals(proposalIds[2]); + + assertEq(yes0 + no0 + abstain0, maxAmount); + assertEq(yes1 + no1 + abstain1, maxAmount); + assertEq(yes2 + no2 + abstain2, maxAmount); + + assertEq(yes0, 50 / 2); + assertEq(no0, 20 / 2); + assertEq(abstain0, 30 / 2); + + assertEq(yes1, 0); + assertEq(no1, 40 / 2); + assertEq(abstain1, 60 / 2); + + assertVoteRecord(0, proposalIds[0], 50 / 2, 20 / 2, 30 / 2); + assertVoteRecord(1, proposalIds[1], 0, 40 / 2, 60 / 2); + } + + function test_notAdjustVotes_WhenVotingForAllChoicesAndProposalsExpired() public { + makeAndApprove3Proposals(); + setupVotingForAllChoices(); + vm.warp(block.timestamp + governance.queueExpiry()); + assertVoteRecord(0, proposalIds[0], 50, 20, 30); + assertVoteRecord(1, proposalIds[1], 0, 40, 60); + } + function assertVotesTotal( - uint256 proposalId, + uint256 _proposalId, uint256 expectedYes, uint256 expectedNo, uint256 expectedAbstain ) private { - (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(proposalId); + (uint256 yes, uint256 no, uint256 abstain) = governance.getVoteTotals(_proposalId); assertEq(yes, expectedYes); assertEq(no, expectedNo); @@ -3649,11 +4371,11 @@ contract GovernanceTest_removeVotesWhenRevokingDelegatedVotes is GovernanceTest uint256 expectedNo, uint256 expectedAbstain ) private { - (uint256 proposalId, , , uint256 yes, uint256 no, uint256 abstain) = governance.getVoteRecord( + (uint256 _proposalId, , , uint256 yes, uint256 no, uint256 abstain) = governance.getVoteRecord( accVoter, index ); - assertEq(proposalId, expectedProposalId); + assertEq(_proposalId, expectedProposalId); assertEq(yes, expectedYes); assertEq(no, expectedNo); assertEq(abstain, expectedAbstain); @@ -3695,30 +4417,6 @@ contract GovernanceTest_removeVotesWhenRevokingDelegatedVotes is GovernanceTest assertVotesTotal(proposalIds[1], 0, 100, 0); } - function test_adjustVotesCorrectlyTo0_WhenVotingOnlyforYes() public { - makeAndApprove3Proposals(); - setUpVotingOnlyforYes(); - governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, 0); - - assertVoteRecord(0, proposalIds[0], 0, 0, 0); - assertVoteRecord(1, proposalIds[1], 0, 0, 0); - - assertVotesTotal(proposalIds[0], 0, 0, 0); - assertVotesTotal(proposalIds[1], 0, 0, 0); - } - - function test_adjust_votes_correctly_to_30_WhenVotingOnlyForYes() public { - makeAndApprove3Proposals(); - setUpVotingOnlyforYes(); - governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, 30); - - assertVoteRecord(0, proposalIds[0], 30, 0, 0); - assertVoteRecord(1, proposalIds[1], 0, 30, 0); - - assertVotesTotal(proposalIds[0], 30, 0, 0); - assertVotesTotal(proposalIds[1], 0, 30, 0); - } - function setupVotingForAllChoices() private { vm.prank(accVoter); governance.votePartially(proposalIds[0], 0, 50, 20, 30); @@ -3731,49 +4429,9 @@ contract GovernanceTest_removeVotesWhenRevokingDelegatedVotes is GovernanceTest assertVoteRecord(1, proposalIds[1], 0, 40, 60); assertVoteRecord(2, proposalIds[2], 0, 0, 51); } - - function test_adjustVotesCorrectlyTo0_WhenVotingForAllChoices() public { - makeAndApprove3Proposals(); - setupVotingForAllChoices(); - - governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, 0); - - assertVoteRecord(0, proposalIds[0], 0, 0, 0); - assertVoteRecord(1, proposalIds[1], 0, 0, 0); - } - - function test_adjustVotesCorrectlyTo50_WhenVotingForAllChoices() public { - makeAndApprove3Proposals(); - setupVotingForAllChoices(); - - uint256 maxAmount = 50; // means that votes will be halved - governance.removeVotesWhenRevokingDelegatedVotesTest(accVoter, maxAmount); - - (uint256 yes0, uint256 no0, uint256 abstain0) = governance.getVoteTotals(proposalIds[0]); - (uint256 yes1, uint256 no1, uint256 abstain1) = governance.getVoteTotals(proposalIds[1]); - (uint256 yes2, uint256 no2, uint256 abstain2) = governance.getVoteTotals(proposalIds[2]); - - assertEq(yes0 + no0 + abstain0, maxAmount); - assertEq(yes1 + no1 + abstain1, maxAmount); - assertEq(yes2 + no2 + abstain2, maxAmount); - - assertEq(yes0, 50 / 2); - assertEq(no0, 20 / 2); - assertEq(abstain0, 30 / 2); - - assertEq(yes1, 0); - assertEq(no1, 40 / 2); - assertEq(abstain1, 60 / 2); - - assertVoteRecord(0, proposalIds[0], 50 / 2, 20 / 2, 30 / 2); - assertVoteRecord(1, proposalIds[1], 0, 40 / 2, 60 / 2); - } - - function test_notAdjustVotes_WhenVotingForAllChoicesAndProposalsExpired() public { - makeAndApprove3Proposals(); - setupVotingForAllChoices(); - vm.warp(block.timestamp + governance.queueExpiry()); - assertVoteRecord(0, proposalIds[0], 50, 20, 30); - assertVoteRecord(1, proposalIds[1], 0, 40, 60); - } } + +contract GovernanceTest_removeVotesWhenRevokingDelegatedVotes_L2 is + GovernanceTest_L2, + GovernanceTest_removeVotesWhenRevokingDelegatedVotes +{} diff --git a/packages/protocol/test-sol/unit/governance/network/GovernanceSlasher.t.sol b/packages/protocol/test-sol/unit/governance/network/GovernanceSlasher.t.sol new file mode 100644 index 00000000000..eb8e8599c95 --- /dev/null +++ b/packages/protocol/test-sol/unit/governance/network/GovernanceSlasher.t.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; + +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; +import "@test-sol/utils/WhenL2.sol"; + +import "@celo-contracts/common/Accounts.sol"; +import "@celo-contracts/common/FixidityLib.sol"; +import "@celo-contracts/governance/Proposals.sol"; +import "@celo-contracts/governance/test/MockLockedGold.sol"; +import "@celo-contracts/governance/test/MockValidators.sol"; +import "@celo-contracts/governance/GovernanceSlasher.sol"; + +contract GovernanceSlasherTest is TestWithUtils { + event SlashingApproved(address indexed account, uint256 amount); + event GovernanceSlashPerformed(address indexed account, uint256 amount); + event GovernanceSlashL2Performed(address indexed account, address indexed group, uint256 amount); + event HavelSlashingMultiplierHalved(address validator); + event ValidatorDeaffiliatedCalled(address validator); + + Accounts accounts; + MockLockedGold mockLockedGold; + + GovernanceSlasher public governanceSlasher; + address owner; + address nonOwner; + address validator; + address slashedAddress; + + address[] lessers = new address[](0); + address[] greaters = new address[](0); + uint256[] indices = new uint256[](0); + address internal slasherExecuter; + + function setUp() public { + super.setUp(); + owner = address(this); + nonOwner = actor("nonOwner"); + validator = actor("validator"); + slashedAddress = actor("slashedAddress"); + slasherExecuter = actor("slasherExecuter"); + + accounts = new Accounts(true); + mockLockedGold = new MockLockedGold(); + governanceSlasher = new GovernanceSlasher(true); + + registry.setAddressFor("Accounts", address(accounts)); + registry.setAddressFor("LockedGold", address(mockLockedGold)); + + governanceSlasher.initialize(REGISTRY_ADDRESS); + mockLockedGold.setAccountTotalLockedGold(validator, 5000); + } +} + +contract GovernanceSlasherTest_L2 is GovernanceSlasherTest, WhenL2 {} + +contract GovernanceSlasherTest_initialize is GovernanceSlasherTest { + function test_shouldHaveSetOwner() public { + assertEq(governanceSlasher.owner(), owner); + } + + function test_CanOnlyBeCalledOnce() public { + vm.expectRevert("contract already initialized"); + governanceSlasher.initialize(REGISTRY_ADDRESS); + } +} + +contract GovernanceSlasherTest_approveSlashing is GovernanceSlasherTest { + function test_ShouldSetSlashableAmount() public { + governanceSlasher.approveSlashing(slashedAddress, 1000); + assertEq(governanceSlasher.getApprovedSlashing(slashedAddress), 1000); + } + + function test_ShouldIncrementSlashableAmountWhenApprovedTwice() public { + governanceSlasher.approveSlashing(slashedAddress, 1000); + governanceSlasher.approveSlashing(slashedAddress, 1000); + assertEq(governanceSlasher.getApprovedSlashing(slashedAddress), 2000); + } + + function test_CanOnlyBeCalledByOnwer() public { + vm.expectRevert("Sender not authorized to slash"); + vm.prank(nonOwner); + governanceSlasher.approveSlashing(slashedAddress, 1000); + } + + function test_EmitsSlashingApprovedEvent() public { + vm.expectEmit(true, true, true, true); + emit SlashingApproved(slashedAddress, 1000); + governanceSlasher.approveSlashing(slashedAddress, 1000); + } + + function test_CanBeCalledBySlasherExecuter() public { + governanceSlasher.setSlasherExecuter(slasherExecuter); + governanceSlasher.approveSlashing(slashedAddress, 1000); + assertEq(governanceSlasher.getApprovedSlashing(slashedAddress), 1000); + } +} + +contract GovernanceSlasherTest_approveSlashing_L2 is + GovernanceSlasherTest_L2, + GovernanceSlasherTest_approveSlashing +{} + +contract GovernanceSlasherTest_slash is GovernanceSlasherTest { + function test_ShouldFailIfThereIsNothingToSlash() public { + vm.expectRevert("No penalty given by governance"); + governanceSlasher.slash(validator, lessers, greaters, indices); + } + + function test_ShouldDecrementCelo() public { + governanceSlasher.approveSlashing(validator, 1000); + governanceSlasher.slash(validator, lessers, greaters, indices); + assertEq(mockLockedGold.accountTotalLockedGold(validator), 4000); + } + + function test_ShouldHaveSetTheApprovedSlashingToZero() public { + governanceSlasher.approveSlashing(validator, 1000); + governanceSlasher.slash(validator, lessers, greaters, indices); + assertEq(governanceSlasher.getApprovedSlashing(validator), 0); + } + + function test_EmitsGovernanceSlashPerformedEvent() public { + governanceSlasher.approveSlashing(validator, 1000); + vm.expectEmit(true, true, true, true); + emit GovernanceSlashPerformed(validator, 1000); + governanceSlasher.slash(validator, lessers, greaters, indices); + } +} + +contract GovernanceSlasherTest_slash_L2 is GovernanceSlasherTest_L2 { + function test_Reverts_WhenL2() public { + governanceSlasher.approveSlashing(validator, 1000); + vm.expectRevert("This method is no longer supported in L2."); + governanceSlasher.slash(validator, lessers, greaters, indices); + } +} + +contract GovernanceSlasherTest_slashL2_WhenL1 is GovernanceSlasherTest { + function test_Reverts() public { + governanceSlasher.approveSlashing(validator, 1000); + vm.expectRevert("This method is not supported in L1."); + governanceSlasher.slashL2( + validator, + validator, + new address[](0), + new address[](0), + new uint256[](0) + ); + } +} + +// should work just like the deprecated version +contract GovernanceSlasherTest_slashL2_WhenNotGroup_L2 is GovernanceSlasherTest_L2 { + address group = address(0); + + // only onwer or multisig can call + + function test_ShouldDecrementCelo() public { + governanceSlasher.approveSlashing(validator, 1000); + governanceSlasher.slashL2(validator, group, lessers, greaters, indices); + assertEq(mockLockedGold.accountTotalLockedGold(validator), 4000); + } + + function test_ShouldHaveSetTheApprovedSlashingToZero() public { + governanceSlasher.approveSlashing(validator, 1000); + governanceSlasher.slashL2(validator, group, lessers, greaters, indices); + assertEq(governanceSlasher.getApprovedSlashing(validator), 0); + } + + function test_EmitsGovernanceSlashPerformedEvent() public { + governanceSlasher.approveSlashing(validator, 1000); + vm.expectEmit(true, true, true, true); + emit GovernanceSlashL2Performed(validator, group, 1000); + governanceSlasher.slashL2(validator, group, lessers, greaters, indices); + } +} + +// should work just like the deprecated version +contract GovernanceSlasherTest_slashL2_WhenGroup_L2 is GovernanceSlasherTest_L2 { + address group; + MockValidators validators; + + function setUp() public { + super.setUp(); + + validators = new MockValidators(); + registry.setAddressFor("Validators", address(validators)); + (group, ) = actorWithPK("group"); + + accounts.initialize(REGISTRY_ADDRESS); + + vm.prank(group); + accounts.createAccount(); + vm.prank(validator); + accounts.createAccount(); + vm.prank(validator); + validators.affiliate(group); + + mockLockedGold.setAccountTotalLockedGold(group, 5000); + } + + // functions should be decreased as usual + function test_ShouldDecrementCelo() public { + governanceSlasher.approveSlashing(validator, 1000); + governanceSlasher.slashL2(validator, group, lessers, greaters, indices); + assertEq(mockLockedGold.accountTotalLockedGold(validator), 4000); + } + + function test_ShouldHaveSetTheApprovedSlashingToZero() public { + governanceSlasher.approveSlashing(validator, 1000); + governanceSlasher.slashL2(validator, group, lessers, greaters, indices); + assertEq(governanceSlasher.getApprovedSlashing(validator), 0); + } + + function test_EmitsGovernanceSlashPerformedEvent() public { + governanceSlasher.approveSlashing(validator, 1000); + vm.expectEmit(true, true, true, true); + emit GovernanceSlashL2Performed(validator, group, 1000); + governanceSlasher.slashL2(validator, group, lessers, greaters, indices); + } + + function test_validatorDeAffiliatedAndScoreReduced() public { + governanceSlasher.approveSlashing(validator, 100); + + // functions to affect validator called + vm.expectEmit(true, true, true, true); + emit ValidatorDeaffiliatedCalled(validator); + vm.expectEmit(true, true, true, true); + emit HavelSlashingMultiplierHalved(group); + + governanceSlasher.slashL2(validator, group, lessers, greaters, indices); + // assets removed, slashing called + assertEq(mockLockedGold.accountTotalLockedGold(validator), 4900); + assertEq(mockLockedGold.accountTotalLockedGold(group), 4900); + } + + function test_CanBeCalledBySlasherExecuter() public { + governanceSlasher.approveSlashing(validator, 1000); + governanceSlasher.setSlasherExecuter(slasherExecuter); + governanceSlasher.approveSlashing(slashedAddress, 1000); + vm.prank(slasherExecuter); + governanceSlasher.slashL2(validator, group, lessers, greaters, indices); + } +} + +contract GovernanceSlasherTest_setSlasherExecuter is GovernanceSlasherTest { + function test_onlyOwnwerCanSetSlasherExecuter() public { + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + governanceSlasher.setSlasherExecuter(owner); + } + + function test_setSlasherExecuter() public { + governanceSlasher.setSlasherExecuter(nonOwner); + assertEq(governanceSlasher.getSlasherExecuter(), nonOwner, "Score Manager not set"); + } +} + +contract GovernanceSlasherTest_setSlasherExecuter_L2 is + GovernanceSlasherTest_L2, + GovernanceSlasherTest_setSlasherExecuter +{} diff --git a/packages/protocol/test-sol/governance/network/Proposal.t.sol b/packages/protocol/test-sol/unit/governance/network/Proposal.t.sol similarity index 82% rename from packages/protocol/test-sol/governance/network/Proposal.t.sol rename to packages/protocol/test-sol/unit/governance/network/Proposal.t.sol index 972ff7ebd3b..a065a005767 100644 --- a/packages/protocol/test-sol/governance/network/Proposal.t.sol +++ b/packages/protocol/test-sol/unit/governance/network/Proposal.t.sol @@ -2,20 +2,25 @@ pragma solidity ^0.5.13; import "celo-foundry/Test.sol"; +import "@test-sol/utils/WhenL2.sol"; import "@celo-contracts/governance/Proposals.sol"; import "@celo-contracts/common/FixidityLib.sol"; -contract ProposalTest_getSupportWithQuorumPadding is Test { +contract ProposalTest is Test { using Proposals for Proposals.Proposal; using FixidityLib for FixidityLib.Fraction; - Proposals.Proposal private proposal; + Proposals.Proposal internal proposal; function setUp() public { proposal.networkWeight = 100; } +} + +contract ProposalTest_L2 is WhenL2, ProposalTest {} +contract ProposalTest_getSupportWithQuorumPadding is ProposalTest { function test_ShouldReturnSupportRatioWhenParticipationAboveCriticalBaseline() public { proposal.votes.yes = 15; proposal.votes.no = 10; @@ -47,3 +52,8 @@ contract ProposalTest_getSupportWithQuorumPadding is Test { assertEq(proposal.getSupportWithQuorumPadding(quorum).unwrap(), 0); } } + +contract ProposalTest_getSupportWithQuorumPadding_L2 is + ProposalTest_L2, + ProposalTest_getSupportWithQuorumPadding +{} diff --git a/packages/protocol/test-sol/governance/validators/DoubleSigningSlasher.t.sol b/packages/protocol/test-sol/unit/governance/validators/DoubleSigningSlasher.t.sol similarity index 95% rename from packages/protocol/test-sol/governance/validators/DoubleSigningSlasher.t.sol rename to packages/protocol/test-sol/unit/governance/validators/DoubleSigningSlasher.t.sol index 99423ebce51..7edde24c64b 100644 --- a/packages/protocol/test-sol/governance/validators/DoubleSigningSlasher.t.sol +++ b/packages/protocol/test-sol/unit/governance/validators/DoubleSigningSlasher.t.sol @@ -3,6 +3,8 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; import "celo-foundry/Test.sol"; +import { TestConstants } from "@test-sol/constants.sol"; + import "@celo-contracts/common/FixidityLib.sol"; import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Accounts.sol"; @@ -49,7 +51,7 @@ contract DoubleSigningSlasherTest is DoubleSigningSlasher(true), MockUsingPrecom } } -contract DoubleSigningSlasherBaseTest is Test { +contract DoubleSigningSlasherBaseTest is Test, TestConstants { using FixidityLib for FixidityLib.Fraction; SlashingIncentives public expectedSlashingIncentives; @@ -80,8 +82,6 @@ contract DoubleSigningSlasherBaseTest is Test { address caller2; uint256 caller2PK; - address public registryAddress = 0x000000000000000000000000000000000000ce10; - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; struct SlashingIncentives { // Value of LockedGold to slash from the account. @@ -104,14 +104,14 @@ contract DoubleSigningSlasherBaseTest is Test { (otherGroup, groupPK) = actorWithPK("otherGroup"); (caller2, caller2PK) = actorWithPK("caller2"); - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); accounts = new Accounts(true); validators = new MockValidators(); lockedGold = new MockLockedGold(); slasher = new DoubleSigningSlasherTest(); - registry = Registry(registryAddress); + registry = Registry(REGISTRY_ADDRESS); accounts.createAccount(); @@ -130,7 +130,7 @@ contract DoubleSigningSlasherBaseTest is Test { vm.prank(otherGroup); accounts.createAccount(); - accounts.initialize(registryAddress); + accounts.initialize(REGISTRY_ADDRESS); registry.setAddressFor("LockedGold", address(lockedGold)); registry.setAddressFor("Validators", address(validators)); @@ -145,7 +145,7 @@ contract DoubleSigningSlasherBaseTest is Test { expectedSlashingIncentives.penalty = slashingPenalty; expectedSlashingIncentives.reward = slashingReward; - slasher.initialize(registryAddress, slashingPenalty, slashingReward); + slasher.initialize(REGISTRY_ADDRESS, slashingPenalty, slashingReward); lockedGold.setAccountTotalLockedGold(address(this), 50000); lockedGold.setAccountTotalLockedGold(nonOwner, 50000); @@ -156,7 +156,7 @@ contract DoubleSigningSlasherBaseTest is Test { } function _whenL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); + deployCodeTo("Registry.sol", abi.encode(false), PROXY_ADMIN_ADDRESS); } } @@ -173,7 +173,7 @@ contract DoubleSigningSlasherInitialize is DoubleSigningSlasherBaseTest { function test_RevertWhen_CalledTwice() public { vm.expectRevert("contract already initialized"); - slasher.initialize(registryAddress, slashingPenalty, slashingReward); + slasher.initialize(REGISTRY_ADDRESS, slashingPenalty, slashingReward); } } diff --git a/packages/protocol/test-sol/governance/validators/DowntimeSlasher.t.sol b/packages/protocol/test-sol/unit/governance/validators/DowntimeSlasher.t.sol similarity index 98% rename from packages/protocol/test-sol/governance/validators/DowntimeSlasher.t.sol rename to packages/protocol/test-sol/unit/governance/validators/DowntimeSlasher.t.sol index a8c0ff7e7fb..35416ebe177 100644 --- a/packages/protocol/test-sol/governance/validators/DowntimeSlasher.t.sol +++ b/packages/protocol/test-sol/unit/governance/validators/DowntimeSlasher.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; import "celo-foundry/Test.sol"; + import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "@celo-contracts/common/FixidityLib.sol"; import "@celo-contracts/common/Registry.sol"; @@ -11,7 +12,7 @@ import "@celo-contracts/governance/test/MockValidators.sol"; import "@celo-contracts/governance/test/MockLockedGold.sol"; import "@celo-contracts/governance/DowntimeSlasher.sol"; import "@celo-contracts/governance/test/MockUsingPrecompiles.sol"; -import { Utils } from "@test-sol/utils.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; contract DowntimeSlasherMock is DowntimeSlasher(true), MockUsingPrecompiles, Test { struct SlashParams { @@ -75,7 +76,7 @@ contract DowntimeSlasherMock is DowntimeSlasher(true), MockUsingPrecompiles, Tes } } -contract DowntimeSlasherTest is Test, Utils { +contract DowntimeSlasherTest is TestWithUtils { using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; @@ -116,8 +117,6 @@ contract DowntimeSlasherTest is Test, Utils { address caller2; uint256 caller2PK; - address public registryAddress = 0x000000000000000000000000000000000000ce10; - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; uint256 epochSize; uint256 epoch; @@ -172,14 +171,14 @@ contract DowntimeSlasherTest is Test, Utils { (otherGroup, groupPK) = actorWithPK("otherGroup"); (caller2, caller2PK) = actorWithPK("caller2"); - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); accounts = new Accounts(true); validators = new MockValidators(); lockedGold = new MockLockedGold(); slasher = new DowntimeSlasherMock(); - registry = Registry(registryAddress); + registry = Registry(REGISTRY_ADDRESS); accounts.createAccount(); @@ -200,7 +199,7 @@ contract DowntimeSlasherTest is Test, Utils { vm.prank(otherGroup); accounts.createAccount(); - accounts.initialize(registryAddress); + accounts.initialize(REGISTRY_ADDRESS); registry.setAddressFor("LockedGold", address(lockedGold)); registry.setAddressFor("Validators", address(validators)); @@ -218,7 +217,7 @@ contract DowntimeSlasherTest is Test, Utils { expectedSlashingIncentives.penalty = slashingPenalty; expectedSlashingIncentives.reward = slashingReward; - slasher.initialize(registryAddress, slashingPenalty, slashingReward, slashableDowntime); + slasher.initialize(REGISTRY_ADDRESS, slashingPenalty, slashingReward, slashableDowntime); lockedGold.setAccountTotalLockedGold(address(this), 50000); lockedGold.setAccountTotalLockedGold(nonOwner, 50000); @@ -321,10 +320,6 @@ contract DowntimeSlasherTest is Test, Utils { } return (_epochNumber.sub(1)).mul(epochSize).add(1); } - - function _whenL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); - } } contract DowntimeSlasherTestInitialize is DowntimeSlasherTest { @@ -347,7 +342,7 @@ contract DowntimeSlasherTestInitialize is DowntimeSlasherTest { function test_Reverts_WhenCalledTwice() public { vm.expectRevert("contract already initialized"); - slasher.initialize(registryAddress, slashingPenalty, slashingReward, slashableDowntime); + slasher.initialize(REGISTRY_ADDRESS, slashingPenalty, slashingReward, slashableDowntime); } } diff --git a/packages/protocol/test-sol/governance/validators/IntegerSortedLinkedListMock-8.sol b/packages/protocol/test-sol/unit/governance/validators/IntegerSortedLinkedListMock-8.sol similarity index 100% rename from packages/protocol/test-sol/governance/validators/IntegerSortedLinkedListMock-8.sol rename to packages/protocol/test-sol/unit/governance/validators/IntegerSortedLinkedListMock-8.sol diff --git a/packages/protocol/test-sol/governance/validators/IntergerSortedLinkedList-8.t.sol b/packages/protocol/test-sol/unit/governance/validators/IntergerSortedLinkedList-8.t.sol similarity index 99% rename from packages/protocol/test-sol/governance/validators/IntergerSortedLinkedList-8.t.sol rename to packages/protocol/test-sol/unit/governance/validators/IntergerSortedLinkedList-8.t.sol index f4280ce73af..e5b482663c9 100644 --- a/packages/protocol/test-sol/governance/validators/IntergerSortedLinkedList-8.t.sol +++ b/packages/protocol/test-sol/unit/governance/validators/IntergerSortedLinkedList-8.t.sol @@ -7,7 +7,7 @@ import { CommonBase } from "forge-std-8/Base.sol"; import { StdCheats } from "forge-std-8/StdCheats.sol"; import { StdUtils } from "forge-std-8/StdUtils.sol"; -import "@test-sol/governance/validators/IntegerSortedLinkedListMock-8.sol"; +import "@test-sol/unit/governance/validators/IntegerSortedLinkedListMock-8.sol"; contract IntegerSortedLinkedListTest8 is Test { IntegerSortedLinkedListMock public integerSortedLinkedListMock; diff --git a/packages/protocol/test-sol/governance/validators/IntergerSortedLinkedList.t.sol b/packages/protocol/test-sol/unit/governance/validators/IntergerSortedLinkedList.t.sol similarity index 100% rename from packages/protocol/test-sol/governance/validators/IntergerSortedLinkedList.t.sol rename to packages/protocol/test-sol/unit/governance/validators/IntergerSortedLinkedList.t.sol diff --git a/packages/protocol/test-sol/governance/validators/Validators.t.sol b/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol similarity index 80% rename from packages/protocol/test-sol/governance/validators/Validators.t.sol rename to packages/protocol/test-sol/unit/governance/validators/Validators.t.sol index 5b8ff4d2524..9183fbd6692 100644 --- a/packages/protocol/test-sol/governance/validators/Validators.t.sol +++ b/packages/protocol/test-sol/unit/governance/validators/Validators.t.sol @@ -2,39 +2,51 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; -import "celo-foundry/Test.sol"; +// This test file is in 0.5 although the contract is in 0.8 + +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; import "@celo-contracts/common/FixidityLib.sol"; -import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Accounts.sol"; +import "@celo-contracts-8/common/interfaces/IPrecompiles.sol"; -import "@celo-contracts/governance/Election.sol"; -import "@celo-contracts/governance/LockedGold.sol"; +import "@celo-contracts/governance/interfaces/IValidators.sol"; import "@celo-contracts/stability/test/MockStableToken.sol"; import "@celo-contracts/governance/test/MockElection.sol"; import "@celo-contracts/governance/test/MockLockedGold.sol"; -import "./mocks/ValidatorsMockTunnel.sol"; +import "@test-sol/unit/governance/validators/mocks/ValidatorsMockTunnel.sol"; -import "@celo-contracts/governance/test/ValidatorsMock.sol"; -import "@test-sol/constants.sol"; import "@test-sol/utils/ECDSAHelper.sol"; -import { Utils } from "@test-sol/utils.sol"; -import { Test as ForgeTest } from "forge-std/Test.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; + +import "@test-sol/utils/WhenL2.sol"; -contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { +contract ValidatorsTest is TestWithUtils, ECDSAHelper { using FixidityLib for FixidityLib.Fraction; using SafeMath for uint256; - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; + struct ValidatorLockedGoldRequirements { + uint256 value; + uint256 duration; + } + + struct GroupLockedGoldRequirements { + uint256 value; + uint256 duration; + } + + struct ValidatorScoreParameters { + uint256 exponent; + FixidityLib.Fraction adjustmentSpeed; + } - Registry registry; Accounts accounts; MockStableToken stableToken; MockElection election; ValidatorsMockTunnel public validatorsMockTunnel; - ValidatorsMock public validators; + IValidators public validators; MockLockedGold lockedGold; address owner; @@ -68,28 +80,6 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { FixidityLib.Fraction public commission = FixidityLib.newFixedFraction(1, 100); - event AccountSlashed( - address indexed slashed, - uint256 penalty, - address indexed reporter, - uint256 reward - ); - - struct ValidatorLockedGoldRequirements { - uint256 value; - uint256 duration; - } - - struct GroupLockedGoldRequirements { - uint256 value; - uint256 duration; - } - - struct ValidatorScoreParameters { - uint256 exponent; - FixidityLib.Fraction adjustmentSpeed; - } - ValidatorLockedGoldRequirements public originalValidatorLockedGoldRequirements; GroupLockedGoldRequirements public originalGroupLockedGoldRequirements; ValidatorScoreParameters public originalValidatorScoreParameters; @@ -103,6 +93,12 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { ValidatorsMockTunnel.InitParams public initParams; ValidatorsMockTunnel.InitParams2 public initParams2; + event AccountSlashed( + address indexed slashed, + uint256 penalty, + address indexed reporter, + uint256 reward + ); event MaxGroupSizeSet(uint256 size); event CommissionUpdateDelaySet(uint256 delay); event ValidatorScoreParametersSet(uint256 exponent, uint256 adjustmentSpeed); @@ -134,7 +130,10 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { uint256 groupPayment ); + event SendValidatorPaymentCalled(address validator); + function setUp() public { + super.setUp(); owner = address(this); group = actor("group"); nonValidator = actor("nonValidator"); @@ -160,16 +159,15 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { adjustmentSpeed: FixidityLib.newFixedFraction(5, 20) }); - address registryAddress = 0x000000000000000000000000000000000000ce10; - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); - registry = Registry(registryAddress); - accounts = new Accounts(true); - accounts.initialize(registryAddress); + accounts.initialize(REGISTRY_ADDRESS); lockedGold = new MockLockedGold(); election = new MockElection(); - validators = new ValidatorsMock(); + address validatorsAddress = actor("Validators"); + + deployCodeTo("ValidatorsMock.sol", validatorsAddress); + validators = IValidators(validatorsAddress); validatorsMockTunnel = new ValidatorsMockTunnel(address(validators)); stableToken = new MockStableToken(); @@ -181,7 +179,7 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { registry.setAddressFor(StableTokenContract, address(stableToken)); initParams = ValidatorsMockTunnel.InitParams({ - registryAddress: registryAddress, + registryAddress: REGISTRY_ADDRESS, groupRequirementValue: originalGroupLockedGoldRequirements.value, groupRequirementDuration: originalGroupLockedGoldRequirements.duration, validatorRequirementValue: originalValidatorLockedGoldRequirements.value, @@ -212,6 +210,67 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { accounts.createAccount(); } + function _registerValidatorGroupWithMembers(address _group, uint256 _numMembers) public { + _registerValidatorGroupHelper(_group, _numMembers); + + for (uint256 i = 0; i < _numMembers; i++) { + if (i == 0) { + _registerValidatorHelper(validator, validatorPk); + + vm.prank(validator); + validators.affiliate(group); + + vm.prank(group); + validators.addFirstMember(validator, address(0), address(0)); + } else { + uint256 _validator1Pk = i; + address _validator1 = vm.addr(_validator1Pk); + + vm.prank(_validator1); + accounts.createAccount(); + _registerValidatorHelper(_validator1, _validator1Pk); + vm.prank(_validator1); + validators.affiliate(group); + + vm.prank(group); + validators.addMember(_validator1); + } + } + } + + function _registerValidatorGroupWithMembersHavingSigners( + address _group, + uint256 _numMembers + ) public { + _registerValidatorGroupHelper(_group, _numMembers); + + for (uint256 i = 0; i < _numMembers; i++) { + if (i == 0) { + _registerValidatorWithSignerHelper(validator, signer, signerPk); + + vm.prank(validator); + validators.affiliate(_group); + + vm.prank(_group); + validators.addFirstMember(validator, address(0), address(0)); + } else { + uint256 _validator1Pk = i; + address _validator1 = vm.addr(_validator1Pk); + uint256 _signer1Pk = i + _numMembers; + address _signer1 = vm.addr(_signer1Pk); + + vm.prank(_validator1); + accounts.createAccount(); + _registerValidatorWithSignerHelper(_validator1, _signer1, _signer1Pk); + vm.prank(_validator1); + validators.affiliate(_group); + + vm.prank(_group); + validators.addMember(_validator1); + } + } + } + function getParsedSignatureOfAddress( address _address, uint256 privateKey @@ -232,7 +291,36 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { ecdsaPubKey = addressToPublicKey(addressHash, v, r, s); } - function _registerValidatorWithSignerHelper() internal returns (bytes memory) { + function _registerValidatorWithSignerHelper( + address _validator, + address _signer, + uint256 _signerPk + ) internal returns (bytes memory) { + lockedGold.setAccountTotalLockedGold(_validator, originalValidatorLockedGoldRequirements.value); + + (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( + _validator, + _signerPk + ); + + vm.prank(_validator); + accounts.authorizeValidatorSigner(_signer, v, r, s); + + if (isL2()) { + vm.prank(_validator); + validators.registerValidatorNoBls(_ecdsaPubKey); + } else { + ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(_validator, blsPublicKey, blsPop)); + + vm.prank(_validator); + validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); + } + + validatorRegistrationEpochNumber = getEpochNumber(); + return _ecdsaPubKey; + } + + function _registerValidatorWithSignerHelper_noBls() internal returns (bytes memory) { lockedGold.setAccountTotalLockedGold(validator, originalValidatorLockedGoldRequirements.value); (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( @@ -240,14 +328,12 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { signerPk ); - ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(validator, blsPublicKey, blsPop)); - vm.prank(validator); accounts.authorizeValidatorSigner(signer, v, r, s); vm.prank(validator); - validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); - validatorRegistrationEpochNumber = validators.getEpochNumber(); + validators.registerValidatorNoBls(_ecdsaPubKey); + validatorRegistrationEpochNumber = epochManager.getCurrentEpochNumber(); return _ecdsaPubKey; } @@ -273,11 +359,17 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { lockedGold.setAccountTotalLockedGold(_validator, originalValidatorLockedGoldRequirements.value); bytes memory _ecdsaPubKey = _generateEcdsaPubKey(_validator, _validatorPk); - ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(_validator, blsPublicKey, blsPop)); + if (isL2()) { + vm.prank(_validator); + validators.registerValidatorNoBls(_ecdsaPubKey); + } else { + ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(_validator, blsPublicKey, blsPop)); - vm.prank(_validator); - validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); - validatorRegistrationEpochNumber = validators.getEpochNumber(); + vm.prank(_validator); + validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); + } + + validatorRegistrationEpochNumber = getEpochNumber(); return _ecdsaPubKey; } @@ -296,34 +388,6 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { validators.registerValidatorGroup(commission.unwrap()); } - function _registerValidatorGroupWithMembers(address _group, uint256 _numMembers) public { - _registerValidatorGroupHelper(_group, _numMembers); - - for (uint256 i = 0; i < _numMembers; i++) { - if (i == 0) { - _registerValidatorHelper(validator, validatorPk); - - vm.prank(validator); - validators.affiliate(group); - - vm.prank(group); - validators.addFirstMember(validator, address(0), address(0)); - } else { - uint256 _validator1Pk = i; - address _validator1 = vm.addr(_validator1Pk); - - vm.prank(_validator1); - accounts.createAccount(); - _registerValidatorHelper(_validator1, _validator1Pk); - vm.prank(_validator1); - validators.affiliate(group); - - vm.prank(group); - validators.addMember(_validator1); - } - } - } - function _removeMemberAndTimeTravel( address _group, address _validator, @@ -334,6 +398,14 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { timeTravel(_duration); } + function _calculateScore(uint256 _uptime, uint256 _gracePeriod) internal view returns (uint256) { + return + _safeExponent( + _max1(_uptime.add(_gracePeriod)), + FixidityLib.wrap(originalValidatorScoreParameters.exponent) + ); + } + function _max1(uint256 num) internal pure returns (FixidityLib.Fraction memory) { return num > FixidityLib.fixed1().unwrap() ? FixidityLib.fixed1() : FixidityLib.wrap(num); } @@ -352,29 +424,20 @@ contract ValidatorsTest is Test, Constants, Utils, ECDSAHelper { result = FixidityLib.multiply(result, base); } - return result.unwrap(); - } - - function _calculateScore(uint256 _uptime, uint256 _gracePeriod) internal view returns (uint256) { - return - _safeExponent( - _max1(_uptime.add(_gracePeriod)), - FixidityLib.wrap(originalValidatorScoreParameters.exponent) - ); - } - function _whenL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); + return result.unwrap(); } } +contract ValidatorsTest_L2 is ValidatorsTest, WhenL2 {} + contract ValidatorsTest_Initialize is ValidatorsTest { function test_ShouldhaveSetTheOwner() public { - assertEq(validators.owner(), owner, "Incorrect Owner."); + assertEq(Ownable(address(validators)).owner(), owner, "Incorrect Owner."); } function test_Reverts_WhenCalledMoreThanOnce() public { - vm.expectRevert(); + vm.expectRevert("contract already initialized"); validatorsMockTunnel.MockInitialize(owner, initParams, initParams2); } @@ -421,7 +484,7 @@ contract ValidatorsTest_Initialize is ValidatorsTest { } function test_shouldHaveSetMembershipHistory() public { - uint256 actual = validators.membershipHistoryLength(); + uint256 actual = validators.getMembershipHistoryLength(); assertEq(actual, membershipHistoryLength, "Wrong membershipHistoryLength."); } @@ -435,21 +498,38 @@ contract ValidatorsTest_Initialize is ValidatorsTest { assertEq(actual, commissionUpdateDelay, "Wrong commissionUpdateDelay."); } - function test_Reverts_setCommissionUpdateDelay_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - validators.setCommissionUpdateDelay(commissionUpdateDelay); - } - function test_shouldHaveSetDowntimeGracePeriod() public { uint256 actual = validators.downtimeGracePeriod(); assertEq(actual, downtimeGracePeriod, "Wrong downtimeGracePeriod."); } +} + +contract ValidatorsTest_setCommissionUpdateDelay is ValidatorsTest { + function test_shouldSetCommissionUpdateDelay() public { + validators.setCommissionUpdateDelay(5); + + uint256 actual = validators.getCommissionUpdateDelay(); + assertEq(actual, 5, "Wrong commissionUpdateDelay."); + } +} + +contract ValidatorsTest_setCommissionUpdateDelay_L2 is + ValidatorsTest_L2, + ValidatorsTest_setCommissionUpdateDelay +{} + +contract ValidatorsTest_setDowntimeGracePeriod is ValidatorsTest { + function test_shouldSetDowntimeGracePeriod() public { + validators.setDowntimeGracePeriod(downtimeGracePeriod + 1); + uint256 actual = validators.downtimeGracePeriod(); + assertEq(actual, downtimeGracePeriod + 1, "Wrong downtime grace period."); + } +} - function test_Reverts_SetDowntimeGracePeriod_WhenL2() public { - _whenL2(); +contract ValidatorsTest_setDowntimeGracePeriod_L2 is ValidatorsTest_L2 { + function test_shouldRevert() public { vm.expectRevert("This method is no longer supported in L2."); - validators.setDowntimeGracePeriod(downtimeGracePeriod); + validators.setDowntimeGracePeriod(downtimeGracePeriod + 1); } } @@ -463,13 +543,7 @@ contract ValidatorsTest_SetMembershipHistoryLength is ValidatorsTest { function test_shouldSetTheMembershipHistoryLength() public { validators.setMembershipHistoryLength(newLength); - assertEq(validators.membershipHistoryLength(), newLength); - } - - function test_Reverts_SetTheMembershipHistoryLength_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - validators.setMembershipHistoryLength(newLength); + assertEq(validators.getMembershipHistoryLength(), newLength); } function test_Emits_MembershipHistoryLengthSet() public { @@ -485,17 +559,16 @@ contract ValidatorsTest_SetMembershipHistoryLength is ValidatorsTest { } } +contract ValidatorsTest_SetMembershipHistoryLength_L2 is + ValidatorsTest_L2, + ValidatorsTest_SetMembershipHistoryLength +{} + contract ValidatorsTest_SetMaxGroupSize is ValidatorsTest { uint256 newSize = maxGroupSize + 1; event MaxGroupSizeSet(uint256 size); - function test_Reverts_SetMaxGroupSize_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - validators.setMaxGroupSize(newSize); - } - function test_Emits_MaxGroupSizeSet() public { vm.expectEmit(true, true, true, true); emit MaxGroupSizeSet(newSize); @@ -514,6 +587,8 @@ contract ValidatorsTest_SetMaxGroupSize is ValidatorsTest { } } +contract ValidatorsTest_SetMaxGroupSize_L2 is ValidatorsTest_L2, ValidatorsTest_SetMaxGroupSize {} + contract ValidatorsTest_SetGroupLockedGoldRequirements is ValidatorsTest { GroupLockedGoldRequirements private newRequirements = GroupLockedGoldRequirements({ @@ -549,6 +624,11 @@ contract ValidatorsTest_SetGroupLockedGoldRequirements is ValidatorsTest { } } +contract ValidatorsTest_SetGroupLockedGoldRequirements_L2 is + ValidatorsTest_L2, + ValidatorsTest_SetGroupLockedGoldRequirements +{} + contract ValidatorsTest_SetValidatorLockedGoldRequirements is ValidatorsTest { ValidatorLockedGoldRequirements private newRequirements = ValidatorLockedGoldRequirements({ @@ -584,50 +664,270 @@ contract ValidatorsTest_SetValidatorLockedGoldRequirements is ValidatorsTest { } } -contract ValidatorsTest_SetValidatorScoreParameters is ValidatorsTest { - ValidatorScoreParameters newParams = - ValidatorScoreParameters({ - exponent: originalValidatorScoreParameters.exponent + 1, - adjustmentSpeed: FixidityLib.newFixedFraction(6, 20) - }); +contract ValidatorsTest_SetValidatorLockedGoldRequirements_L2 is + ValidatorsTest_L2, + ValidatorsTest_SetValidatorLockedGoldRequirements +{} + +contract ValidatorsTest_SetValidatorScoreParameters_Setup is ValidatorsTest { + ValidatorScoreParameters newParams = + ValidatorScoreParameters({ + exponent: originalValidatorScoreParameters.exponent + 1, + adjustmentSpeed: FixidityLib.newFixedFraction(6, 20) + }); + + event ValidatorScoreParametersSet(uint256 exponent, uint256 adjustmentSpeed); +} + +contract ValidatorsTest_SetValidatorScoreParameters_L1 is + ValidatorsTest_SetValidatorScoreParameters_Setup +{ + function test_ShouldSetExponentAndAdjustmentSpeed() public { + validators.setValidatorScoreParameters(newParams.exponent, newParams.adjustmentSpeed.unwrap()); + (uint256 _exponent, uint256 _adjustmentSpeed) = validators.getValidatorScoreParameters(); + assertEq(_exponent, newParams.exponent, "Incorrect Exponent"); + assertEq(_adjustmentSpeed, newParams.adjustmentSpeed.unwrap(), "Incorrect AdjustmentSpeed"); + } + + function test_Emits_ValidatorScoreParametersSet() public { + vm.expectEmit(true, true, true, true); + emit ValidatorScoreParametersSet(newParams.exponent, newParams.adjustmentSpeed.unwrap()); + validators.setValidatorScoreParameters(newParams.exponent, newParams.adjustmentSpeed.unwrap()); + } + + function test_Reverts_WhenCalledByNonOwner() public { + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + validators.setValidatorScoreParameters(newParams.exponent, newParams.adjustmentSpeed.unwrap()); + } + + function test_Reverts_WhenLockupsAreUnchanged() public { + vm.expectRevert("Adjustment speed and exponent not changed"); + validators.setValidatorScoreParameters( + originalValidatorScoreParameters.exponent, + originalValidatorScoreParameters.adjustmentSpeed.unwrap() + ); + } +} + +contract ValidatorsTest_SetValidatorScoreParameters_L2 is + ValidatorsTest_L2, + ValidatorsTest_SetValidatorScoreParameters_Setup +{ + function test_Reverts() public { + vm.expectRevert("This method is no longer supported in L2."); + validators.setValidatorScoreParameters(newParams.exponent, newParams.adjustmentSpeed.unwrap()); + } +} + +contract ValidatorsTest_RegisterValidator is ValidatorsTest { + function setUp() public { + super.setUp(); + + lockedGold.setAccountTotalLockedGold(validator, originalValidatorLockedGoldRequirements.value); + } + + function test_Reverts_WhenVoteOverMaxNumberOfGroupsSetToTrue() public { + vm.prank(validator); + election.setAllowedToVoteOverMaxNumberOfGroups(validator, true); + + (uint8 v, bytes32 r, bytes32 s) = getParsedSignatureOfAddress(validator, signerPk); + + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); + bytes memory pubKey = addressToPublicKey("random msg", v, r, s); + + vm.expectRevert("Cannot vote for more than max number of groups"); + vm.prank(validator); + validators.registerValidator(pubKey, blsPublicKey, blsPop); + } + + function test_Reverts_WhenDelagatingCELO() public { + lockedGold.setAccountTotalDelegatedAmountInPercents(validator, 10); + (uint8 v, bytes32 r, bytes32 s) = getParsedSignatureOfAddress(validator, signerPk); + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); + bytes memory pubKey = addressToPublicKey("random msg", v, r, s); + + vm.expectRevert("Cannot delegate governance power"); + vm.prank(validator); + validators.registerValidator(pubKey, blsPublicKey, blsPop); + } + + function test_ShouldMarkAccountAsValidator_WhenAccountHasAuthorizedValidatorSigner() public { + _registerValidatorWithSignerHelper(validator, signer, signerPk); + + assertTrue(validators.isValidator(validator)); + } + + function test_ShouldAddAccountToValidatorList_WhenAccountHasAuthorizedValidatorSigner() public { + address[] memory ExpectedRegisteredValidators = new address[](1); + ExpectedRegisteredValidators[0] = validator; + _registerValidatorWithSignerHelper(validator, signer, signerPk); + assertEq(validators.getRegisteredValidators().length, ExpectedRegisteredValidators.length); + assertEq(validators.getRegisteredValidators()[0], ExpectedRegisteredValidators[0]); + } + + function test_ShouldSetValidatorEcdsaPublicKey_WhenAccountHasAuthorizedValidatorSigner() public { + bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper( + validator, + signer, + signerPk + ); + (bytes memory actualEcdsaPubKey, , , , ) = validators.getValidator(validator); + + assertEq(actualEcdsaPubKey, _registeredEcdsaPubKey); + } + + function test_ShouldSetValidatorBlsPublicKey_WhenAccountHasAuthorizedValidatorSigner() public { + _registerValidatorWithSignerHelper(validator, signer, signerPk); + (, bytes memory actualBlsPubKey, , , ) = validators.getValidator(validator); + + assertEq(actualBlsPubKey, blsPublicKey); + } + + function test_ShouldSetValidatorSigner_WhenAccountHasAuthorizedValidatorSigner() public { + _registerValidatorWithSignerHelper(validator, signer, signerPk); + (, , , , address ActualSigner) = validators.getValidator(validator); + + assertEq(ActualSigner, signer); + } + + function test_ShouldSetLockGoldRequirements_WhenAccountHasAuthorizedValidatorSigner() public { + _registerValidatorWithSignerHelper(validator, signer, signerPk); + uint256 _lockedGoldReq = validators.getAccountLockedGoldRequirement(validator); + + assertEq(_lockedGoldReq, originalValidatorLockedGoldRequirements.value); + } + + function test_ShouldSetValidatorMembershipHistory_WhenAccountHasAuthorizedValidatorSigner() + public + { + _registerValidatorWithSignerHelper(validator, signer, signerPk); + (uint256[] memory _epoch, address[] memory _membershipGroups, , ) = validators + .getMembershipHistory(validator); + + uint256[] memory validatorRegistrationEpochNumberList = new uint256[](1); + validatorRegistrationEpochNumberList[0] = validatorRegistrationEpochNumber; + address[] memory expectedMembershipGroups = new address[](1); + expectedMembershipGroups[0] = address(0); + + assertEq(_epoch, validatorRegistrationEpochNumberList); + assertEq(_membershipGroups, expectedMembershipGroups); + } + + function test_Emits_ValidatorBlsPublicKeyUpdatedEvent() public { + (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( + validator, + signerPk + ); + + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); + + ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(validator, blsPublicKey, blsPop)); + + vm.expectEmit(true, true, true, true); + emit ValidatorBlsPublicKeyUpdated(validator, blsPublicKey); + + vm.prank(validator); + validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); + } + + function test_Emits_ValidatorRegisteredEvent() public { + (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( + validator, + signerPk + ); + + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); + + ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(validator, blsPublicKey, blsPop)); + + vm.expectEmit(true, true, true, true); + emit ValidatorRegistered(validator); + + vm.prank(validator); + validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); + } + + function test_Reverts_WhenAccountAlreadyRegisteredAsValidator() public { + bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper( + validator, + signer, + signerPk + ); + vm.expectRevert("Already registered"); + vm.prank(validator); + validators.registerValidator(_registeredEcdsaPubKey, blsPublicKey, blsPop); + } + + function test_Reverts_WhenAccountAlreadyRegisteredAsValidatorGroup() public { + _registerValidatorGroupHelper(validator, 1); + vm.expectRevert("Already registered"); + vm.prank(validator); + validators.registerValidator( + abi.encodePacked(bytes32(0x0101010101010101010101010101010101010101010101010101010101010101)), + blsPublicKey, + blsPop + ); + } + + function test_Reverts_WhenAccountDoesNotMeetLockedGoldRequirements() public { + lockedGold.setAccountTotalLockedGold( + validator, + originalValidatorLockedGoldRequirements.value.sub(11) + ); + vm.expectRevert("Deposit too small"); + vm.prank(validator); + validators.registerValidator( + abi.encodePacked(bytes32(0x0101010101010101010101010101010101010101010101010101010101010101)), + blsPublicKey, + blsPop + ); + } +} + +contract ValidatorsTest_RegisterValidator_L2 is ValidatorsTest_L2 { + function test_shouldRevert() public { + lockedGold.setAccountTotalLockedGold(validator, originalValidatorLockedGoldRequirements.value); - event ValidatorScoreParametersSet(uint256 exponent, uint256 adjustmentSpeed); + (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( + validator, + signerPk + ); - function test_ShouldSetExponentAndAdjustmentSpeed() public { - validators.setValidatorScoreParameters(newParams.exponent, newParams.adjustmentSpeed.unwrap()); - (uint256 _exponent, uint256 _adjustmentSpeed) = validators.getValidatorScoreParameters(); - assertEq(_exponent, newParams.exponent, "Incorrect Exponent"); - assertEq(_adjustmentSpeed, newParams.adjustmentSpeed.unwrap(), "Incorrect AdjustmentSpeed"); - } + ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(validator, blsPublicKey, blsPop)); - function test_Reverts_SetExponentAndAdjustmentSpeed_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - validators.setValidatorScoreParameters(newParams.exponent, newParams.adjustmentSpeed.unwrap()); - } + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); - function test_Emits_ValidatorScoreParametersSet() public { - vm.expectEmit(true, true, true, true); - emit ValidatorScoreParametersSet(newParams.exponent, newParams.adjustmentSpeed.unwrap()); - validators.setValidatorScoreParameters(newParams.exponent, newParams.adjustmentSpeed.unwrap()); + vm.prank(validator); + vm.expectRevert("This method is no longer supported in L2."); + validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); } +} - function test_Reverts_WhenCalledByNonOwner() public { - vm.prank(nonOwner); - vm.expectRevert("Ownable: caller is not the owner"); - validators.setValidatorScoreParameters(newParams.exponent, newParams.adjustmentSpeed.unwrap()); - } +contract ValidatorsTest_RegisterValidatorNoBls is ValidatorsTest { + function test_ShouldRevert_WhenInL1() public { + lockedGold.setAccountTotalLockedGold(validator, originalValidatorLockedGoldRequirements.value); - function test_Reverts_WhenLockupsAreUnchanged() public { - vm.expectRevert("Adjustment speed and exponent not changed"); - validators.setValidatorScoreParameters( - originalValidatorScoreParameters.exponent, - originalValidatorScoreParameters.adjustmentSpeed.unwrap() + (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( + validator, + signerPk ); + + vm.prank(validator); + accounts.authorizeValidatorSigner(signer, v, r, s); + + vm.expectRevert("This method is not supported in L1."); + vm.prank(validator); + validators.registerValidatorNoBls(_ecdsaPubKey); } } -contract ValidatorsTest_RegisterValidator is ValidatorsTest { +contract ValidatorsTest_RegisterValidatorNoBls_L2 is ValidatorsTest_L2 { function setUp() public { super.setUp(); @@ -646,7 +946,7 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { vm.expectRevert("Cannot vote for more than max number of groups"); vm.prank(validator); - validators.registerValidator(pubKey, blsPublicKey, blsPop); + validators.registerValidatorNoBls(pubKey); } function test_Reverts_WhenDelagatingCELO() public { @@ -658,67 +958,46 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { vm.expectRevert("Cannot delegate governance power"); vm.prank(validator); - validators.registerValidator(pubKey, blsPublicKey, blsPop); + validators.registerValidatorNoBls(pubKey); } function test_ShouldMarkAccountAsValidator_WhenAccountHasAuthorizedValidatorSigner() public { - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper_noBls(); assertTrue(validators.isValidator(validator)); } - function test_ShouldRevert_WhenInL2_WhenAccountHasAuthorizedValidatorSigner() public { - lockedGold.setAccountTotalLockedGold(validator, originalValidatorLockedGoldRequirements.value); - - (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( - validator, - signerPk - ); - - ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(validator, blsPublicKey, blsPop)); - - vm.prank(validator); - accounts.authorizeValidatorSigner(signer, v, r, s); - - _whenL2(); - - vm.expectRevert("This method is no longer supported in L2."); - vm.prank(validator); - validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); - validatorRegistrationEpochNumber = validators.getEpochNumber(); - } - function test_ShouldAddAccountToValidatorList_WhenAccountHasAuthorizedValidatorSigner() public { address[] memory ExpectedRegisteredValidators = new address[](1); ExpectedRegisteredValidators[0] = validator; - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper_noBls(); assertEq(validators.getRegisteredValidators().length, ExpectedRegisteredValidators.length); assertEq(validators.getRegisteredValidators()[0], ExpectedRegisteredValidators[0]); } function test_ShouldSetValidatorEcdsaPublicKey_WhenAccountHasAuthorizedValidatorSigner() public { - bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper(); + bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper_noBls(); (bytes memory actualEcdsaPubKey, , , , ) = validators.getValidator(validator); assertEq(actualEcdsaPubKey, _registeredEcdsaPubKey); } - function test_ShouldSetValidatorBlsPublicKey_WhenAccountHasAuthorizedValidatorSigner() public { - _registerValidatorWithSignerHelper(); + function test_ShouldNotSetValidatorBlsPublicKey_WhenAccountHasAuthorizedValidatorSigner() public { + _registerValidatorWithSignerHelper_noBls(); (, bytes memory actualBlsPubKey, , , ) = validators.getValidator(validator); - assertEq(actualBlsPubKey, blsPublicKey); + assertEq(actualBlsPubKey, ""); } function test_ShouldSetValidatorSigner_WhenAccountHasAuthorizedValidatorSigner() public { - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper_noBls(); (, , , , address ActualSigner) = validators.getValidator(validator); assertEq(ActualSigner, signer); } function test_ShouldSetLockGoldRequirements_WhenAccountHasAuthorizedValidatorSigner() public { - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper_noBls(); uint256 _lockedGoldReq = validators.getAccountLockedGoldRequirement(validator); assertEq(_lockedGoldReq, originalValidatorLockedGoldRequirements.value); @@ -727,7 +1006,7 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { function test_ShouldSetValidatorMembershipHistory_WhenAccountHasAuthorizedValidatorSigner() public { - _registerValidatorWithSignerHelper(); + _registerValidatorWithSignerHelper_noBls(); (uint256[] memory _epoch, address[] memory _membershipGroups, , ) = validators .getMembershipHistory(validator); @@ -740,7 +1019,7 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { assertEq(_membershipGroups, expectedMembershipGroups); } - function test_Emits_ValidatorBlsPublicKeyUpdatedEvent() public { + function testFail_DoesNotEmit_ValidatorBlsPublicKeyUpdatedEvent() public { (bytes memory _ecdsaPubKey, uint8 v, bytes32 r, bytes32 s) = _generateEcdsaPubKeyWithSigner( validator, signerPk @@ -749,13 +1028,11 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { vm.prank(validator); accounts.authorizeValidatorSigner(signer, v, r, s); - ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(validator, blsPublicKey, blsPop)); - vm.expectEmit(true, true, true, true); emit ValidatorBlsPublicKeyUpdated(validator, blsPublicKey); vm.prank(validator); - validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); + validators.registerValidatorNoBls(_ecdsaPubKey); } function test_Emits_ValidatorRegisteredEvent() public { @@ -767,30 +1044,26 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { vm.prank(validator); accounts.authorizeValidatorSigner(signer, v, r, s); - ph.mockSuccess(ph.PROOF_OF_POSSESSION(), abi.encodePacked(validator, blsPublicKey, blsPop)); - vm.expectEmit(true, true, true, true); emit ValidatorRegistered(validator); vm.prank(validator); - validators.registerValidator(_ecdsaPubKey, blsPublicKey, blsPop); + validators.registerValidatorNoBls(_ecdsaPubKey); } function test_Reverts_WhenAccountAlreadyRegisteredAsValidator() public { - bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper(); - vm.expectRevert("Already registered"); + bytes memory _registeredEcdsaPubKey = _registerValidatorWithSignerHelper_noBls(); vm.prank(validator); - validators.registerValidator(_registeredEcdsaPubKey, blsPublicKey, blsPop); + vm.expectRevert("Already registered"); + validators.registerValidatorNoBls(_registeredEcdsaPubKey); } function test_Reverts_WhenAccountAlreadyRegisteredAsValidatorGroup() public { _registerValidatorGroupHelper(validator, 1); - vm.expectRevert("Already registered"); vm.prank(validator); - validators.registerValidator( - abi.encodePacked(bytes32(0x0101010101010101010101010101010101010101010101010101010101010101)), - blsPublicKey, - blsPop + vm.expectRevert("Already registered"); + validators.registerValidatorNoBls( + abi.encodePacked(bytes32(0x0101010101010101010101010101010101010101010101010101010101010101)) ); } @@ -801,10 +1074,8 @@ contract ValidatorsTest_RegisterValidator is ValidatorsTest { ); vm.expectRevert("Deposit too small"); vm.prank(validator); - validators.registerValidator( - abi.encodePacked(bytes32(0x0101010101010101010101010101010101010101010101010101010101010101)), - blsPublicKey, - blsPop + validators.registerValidatorNoBls( + abi.encodePacked(bytes32(0x0101010101010101010101010101010101010101010101010101010101010101)) ); } } @@ -822,11 +1093,6 @@ contract ValidatorsTest_DeregisterValidator_WhenAccountHasNeverBeenMemberOfValid timeTravel(originalValidatorLockedGoldRequirements.duration); } - function _deregisterValidator(address _validator) internal { - vm.prank(_validator); - validators.deregisterValidator(INDEX); - } - function test_ShouldMarkAccountAsNotValidator_WhenAccountHasNeverBeenMemberOfValidatorGroup() public { @@ -875,8 +1141,18 @@ contract ValidatorsTest_DeregisterValidator_WhenAccountHasNeverBeenMemberOfValid vm.prank(validator); validators.deregisterValidator(INDEX + 1); } + + function _deregisterValidator(address _validator) internal { + vm.prank(_validator); + validators.deregisterValidator(INDEX); + } } +contract ValidatorsTest_DeregisterValidator_WhenAccountHasNeverBeenMemberOfValidatorGroup_L2 is + ValidatorsTest_L2, + ValidatorsTest_DeregisterValidator_WhenAccountHasNeverBeenMemberOfValidatorGroup +{} + contract ValidatorsTest_DeregisterValidator_WhenAccountHasBeenMemberOfValidatorGroup is ValidatorsTest { @@ -896,11 +1172,6 @@ contract ValidatorsTest_DeregisterValidator_WhenAccountHasBeenMemberOfValidatorG validators.addFirstMember(validator, address(0), address(0)); } - function _deregisterValidator(address _validator) internal { - vm.prank(_validator); - validators.deregisterValidator(INDEX); - } - function test_ShouldMarkAccountAsNotValidator_WhenValidatorNoLongerMemberOfValidatorGroup() public { @@ -970,8 +1241,18 @@ contract ValidatorsTest_DeregisterValidator_WhenAccountHasBeenMemberOfValidatorG vm.expectRevert("Has been group member recently"); _deregisterValidator(validator); } + + function _deregisterValidator(address _validator) internal { + vm.prank(_validator); + validators.deregisterValidator(INDEX); + } } +contract ValidatorsTest_DeregisterValidator_WhenAccountHasBeenMemberOfValidatorGroup_L2 is + ValidatorsTest_L2, + ValidatorsTest_DeregisterValidator_WhenAccountHasBeenMemberOfValidatorGroup +{} + contract ValidatorsTest_Affiliate_WhenGroupAndValidatorMeetLockedGoldRequirements is ValidatorsTest { @@ -994,13 +1275,6 @@ contract ValidatorsTest_Affiliate_WhenGroupAndValidatorMeetLockedGoldRequirement assertEq(affiliation, group); } - function test_Reverts_WhenL2_WhenAffiliatingWithRegisteredValidatorGroup() public { - _whenL2(); - vm.prank(validator); - vm.expectRevert("This method is no longer supported in L2."); - validators.affiliate(group); - } - function test_Emits_ValidatorAffiliatedEvent() public { vm.expectEmit(true, true, true, true); emit ValidatorAffiliated(validator, group); @@ -1042,7 +1316,12 @@ contract ValidatorsTest_Affiliate_WhenGroupAndValidatorMeetLockedGoldRequirement } } -contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorGroup is +contract ValidatorsTest_Affiliate_WhenGroupAndValidatorMeetLockedGoldRequirements_L2 is + ValidatorsTest_L2, + ValidatorsTest_Affiliate_WhenGroupAndValidatorMeetLockedGoldRequirements +{} + +contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorGroup_Setup is ValidatorsTest { address otherGroup; @@ -1065,7 +1344,11 @@ contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorG vm.prank(validator); validators.affiliate(group); } +} +contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorGroup is + ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorGroup_Setup +{ function test_ShouldSetAffiliate_WhenValidatorNotMemberOfThatValidatorGroup() public { vm.prank(validator); validators.affiliate(otherGroup); @@ -1073,13 +1356,6 @@ contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorG assertEq(affiliation, otherGroup); } - function test_ShouldRevert_WhenL2_WhenValidatorNotMemberOfThatValidatorGroup() public { - _whenL2(); - vm.prank(validator); - vm.expectRevert("This method is no longer supported in L2."); - validators.affiliate(otherGroup); - } - function test_Emits_ValidatorDeaffiliatedEvent_WhenValidatorNotMemberOfThatValidatorGroup() public { @@ -1115,13 +1391,12 @@ contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorG vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - validatorAdditionEpochNumber = validators.getEpochNumber(); - + validatorAdditionEpochNumber = getEpochNumber(); timeTravel(10); vm.prank(validator); validators.affiliate(otherGroup); - validatorAffiliationEpochNumber = validators.getEpochNumber(); + validatorAffiliationEpochNumber = getEpochNumber(); ( uint256[] memory epochs, @@ -1168,7 +1443,32 @@ contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorG } } -contract ValidatorsTest_Deaffiliate is ValidatorsTest { +contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorGroup_L1 is + ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorGroup_Setup +{ + function _performAffiliation() internal { + vm.prank(validator); + validators.affiliate(group); + } + + function test_ShouldNotTryToSendValidatorPayment() public { + assertDoesNotEmit(_performAffiliation, "SendValidatorPaymentCalled(address)"); + } +} + +contract ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorGroup_L2 is + ValidatorsTest_L2, + ValidatorsTest_Affiliate_WhenValidatorIsAlreadyAffiliatedWithValidatorGroup +{ + function test_ShouldSendValidatorPayment() public { + vm.expectEmit(true, true, true, true); + emit SendValidatorPaymentCalled(validator); + vm.prank(validator); + validators.affiliate(group); + } +} + +contract ValidatorsTest_Deaffiliate_Setup is ValidatorsTest { uint256 additionEpoch; uint256 deaffiliationEpoch; @@ -1184,7 +1484,9 @@ contract ValidatorsTest_Deaffiliate is ValidatorsTest { require(_affiliation == group, "Affiliation failed."); } +} +contract ValidatorsTest_Deaffiliate is ValidatorsTest_Deaffiliate_Setup { function test_ShouldClearAffiliate() public { vm.prank(validator); validators.deaffiliate(); @@ -1222,11 +1524,11 @@ contract ValidatorsTest_Deaffiliate is ValidatorsTest { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - additionEpoch = validators.getEpochNumber(); + additionEpoch = getEpochNumber(); vm.prank(validator); validators.deaffiliate(); - deaffiliationEpoch = validators.getEpochNumber(); + deaffiliationEpoch = getEpochNumber(); (address[] memory members, , , , , , ) = validators.getValidatorGroup(group); assertEq(members, expectedMembersList); @@ -1238,13 +1540,13 @@ contract ValidatorsTest_Deaffiliate is ValidatorsTest { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - additionEpoch = validators.getEpochNumber(); + additionEpoch = getEpochNumber(); timeTravel(10); vm.prank(validator); validators.deaffiliate(); - deaffiliationEpoch = validators.getEpochNumber(); + deaffiliationEpoch = getEpochNumber(); ( uint256[] memory epochs, @@ -1272,7 +1574,7 @@ contract ValidatorsTest_Deaffiliate is ValidatorsTest { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - additionEpoch = validators.getEpochNumber(); + additionEpoch = getEpochNumber(); timeTravel(10); @@ -1293,6 +1595,26 @@ contract ValidatorsTest_Deaffiliate is ValidatorsTest { } } +contract ValidatorsTest_Deaffiliate_L1 is ValidatorsTest_Deaffiliate_Setup { + function _performDeaffiliation() internal { + vm.prank(validator); + validators.deaffiliate(); + } + + function test_ShouldNotTryToSendValidatorPayment() public { + assertDoesNotEmit(_performDeaffiliation, "SendValidatorPaymentCalled(address)"); + } +} + +contract ValidatorsTest_Deaffiliate_L2 is ValidatorsTest_Deaffiliate, ValidatorsTest_L2 { + function test_ShouldSendValidatorPayment() public { + vm.expectEmit(true, true, true, true); + emit SendValidatorPaymentCalled(validator); + vm.prank(validator); + validators.deaffiliate(); + } +} + contract ValidatorsTest_UpdateEcdsaPublicKey is ValidatorsTest { bytes validatorEcdsaPubKey; @@ -1312,25 +1634,10 @@ contract ValidatorsTest_UpdateEcdsaPublicKey is ValidatorsTest { ); vm.prank(address(accounts)); validators.updateEcdsaPublicKey(validator, signer, _newEcdsaPubKey); - (bytes memory actualEcdsaPubKey, , , , ) = validators.getValidator(validator); - assertEq(actualEcdsaPubKey, _newEcdsaPubKey); } - function test_Reverts_SetValidatorEcdsaPubKey_WhenCalledByRegisteredAccountsContract_WhenL2() - public - { - _whenL2(); - (bytes memory _newEcdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner( - address(accounts), - signerPk - ); - vm.prank(address(accounts)); - vm.expectRevert("This method is no longer supported in L2."); - validators.updateEcdsaPublicKey(validator, signer, _newEcdsaPubKey); - } - function test_Emits_ValidatorEcdsaPublicKeyUpdatedEvent_WhenCalledByRegisteredAccountsContract() public { @@ -1368,7 +1675,12 @@ contract ValidatorsTest_UpdateEcdsaPublicKey is ValidatorsTest { } } -contract ValidatorsTest_UpdatePublicKeys is ValidatorsTest { +contract ValidatorsTest_UpdateEcdsaPublicKey_L2 is + ValidatorsTest_L2, + ValidatorsTest_UpdateEcdsaPublicKey +{} + +contract ValidatorsTest_UpdatePublicKeys_Setup is ValidatorsTest { bytes validatorEcdsaPubKey; bytes public constant newBlsPublicKey = @@ -1392,7 +1704,9 @@ contract ValidatorsTest_UpdatePublicKeys is ValidatorsTest { validatorEcdsaPubKey = _registerValidatorHelper(validator, validatorPk); } +} +contract ValidatorsTest_UpdatePublicKeys_L1 is ValidatorsTest_UpdatePublicKeys_Setup { function test_ShouldSetValidatorNewBlsPubKeyAndEcdsaPubKey_WhenCalledByRegisteredAccountsContract() public { @@ -1416,25 +1730,6 @@ contract ValidatorsTest_UpdatePublicKeys is ValidatorsTest { assertEq(actualBlsPublicKey, newBlsPublicKey); } - function test_Reverts_SetValidatorNewBlsPubKeyAndEcdsaPubKey_WhenCalledByRegisteredAccountsContract_WhenL2() - public - { - _whenL2(); - (bytes memory _newEcdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner( - address(accounts), - signerPk - ); - - ph.mockSuccess( - ph.PROOF_OF_POSSESSION(), - abi.encodePacked(validator, newBlsPublicKey, newBlsPop) - ); - - vm.prank(address(accounts)); - vm.expectRevert("This method is no longer supported in L2."); - validators.updatePublicKeys(validator, signer, _newEcdsaPubKey, newBlsPublicKey, newBlsPop); - } - function test_Emits_ValidatorEcdsaPublicKeyUpdatedAndValidatorBlsPublicKeyUpdatedEvent_WhenCalledByRegisteredAccountsContract() public { @@ -1487,7 +1782,23 @@ contract ValidatorsTest_UpdatePublicKeys is ValidatorsTest { } } -contract ValidatorsTest_UpdateBlsPublicKey is ValidatorsTest { +contract ValidatorsTest_UpdatePublicKeys_L2 is + ValidatorsTest_UpdatePublicKeys_Setup, + ValidatorsTest_L2 +{ + function test_Reverts() public { + (bytes memory _newEcdsaPubKey, , , ) = _generateEcdsaPubKeyWithSigner( + address(accounts), + signerPk + ); + + vm.expectRevert("This method is no longer supported in L2."); + vm.prank(address(accounts)); + validators.updatePublicKeys(validator, signer, _newEcdsaPubKey, newBlsPublicKey, newBlsPop); + } +} + +contract ValidatorsTest_UpdateBlsPublicKey_Setup is ValidatorsTest { bytes validatorEcdsaPubKey; bytes public constant newBlsPublicKey = @@ -1523,7 +1834,9 @@ contract ValidatorsTest_UpdateBlsPublicKey is ValidatorsTest { validatorEcdsaPubKey = _registerValidatorHelper(validator, validatorPk); } +} +contract ValidatorsTest_UpdateBlsPublicKey_L1 is ValidatorsTest_UpdateBlsPublicKey_Setup { function test_ShouldSetNewValidatorBlsPubKey() public { ph.mockSuccess( ph.PROOF_OF_POSSESSION(), @@ -1538,18 +1851,6 @@ contract ValidatorsTest_UpdateBlsPublicKey is ValidatorsTest { assertEq(actualBlsPublicKey, newBlsPublicKey); } - function test_Reverts_SetNewValidatorBlsPubKey_WhenL2() public { - _whenL2(); - ph.mockSuccess( - ph.PROOF_OF_POSSESSION(), - abi.encodePacked(validator, newBlsPublicKey, newBlsPop) - ); - - vm.prank(validator); - vm.expectRevert("This method is no longer supported in L2."); - validators.updateBlsPublicKey(newBlsPublicKey, newBlsPop); - } - function test_Emits_ValidatorValidatorBlsPublicKeyUpdatedEvent() public { ph.mockSuccess( ph.PROOF_OF_POSSESSION(), @@ -1586,6 +1887,18 @@ contract ValidatorsTest_UpdateBlsPublicKey is ValidatorsTest { } } +contract ValidatorsTest_UpdateBlsPublicKey_L2 is + ValidatorsTest_UpdateBlsPublicKey_Setup, + ValidatorsTest_L2 +{ + function test_Reverts() public { + vm.expectRevert("This method is no longer supported in L2."); + + vm.prank(validator); + validators.updateBlsPublicKey(newBlsPublicKey, newBlsPop); + } +} + contract ValidatorsTest_RegisterValidatorGroup is ValidatorsTest { function setUp() public { super.setUp(); @@ -1678,6 +1991,11 @@ contract ValidatorsTest_RegisterValidatorGroup is ValidatorsTest { } } +contract ValidatorsTest_RegisterValidatorGroup_L2 is + ValidatorsTest_L2, + ValidatorsTest_RegisterValidatorGroup +{} + contract ValidatorsTest_DeregisterValidatorGroup_WhenGroupHasNeverHadMembers is ValidatorsTest { uint256 public constant INDEX = 0; @@ -1730,6 +2048,11 @@ contract ValidatorsTest_DeregisterValidatorGroup_WhenGroupHasNeverHadMembers is } } +contract ValidatorsTest_DeregisterValidatorGroup_WhenGroupHasNeverHadMembers_L2 is + ValidatorsTest_L2, + ValidatorsTest_DeregisterValidatorGroup_WhenGroupHasNeverHadMembers +{} + contract ValidatorsTest_DeregisterValidatorGroup_WhenGroupHasHadMembers is ValidatorsTest { uint256 public constant INDEX = 0; @@ -1824,17 +2147,24 @@ contract ValidatorsTest_DeregisterValidatorGroup_WhenGroupHasHadMembers is Valid } } +contract ValidatorsTest_DeregisterValidatorGroup_WhenGroupHasHadMembers_L2 is + ValidatorsTest_L2, + ValidatorsTest_DeregisterValidatorGroup_WhenGroupHasHadMembers +{} + contract ValidatorsTest_AddMember is ValidatorsTest { uint256 _registrationEpoch; uint256 _additionEpoch; + uint256[] expectedSizeHistory; + function setUp() public { super.setUp(); _registerValidatorGroupHelper(group, 1); _registerValidatorHelper(validator, validatorPk); - _registrationEpoch = validators.getEpochNumber(); + _registrationEpoch = getEpochNumber(); vm.prank(validator); validators.affiliate(group); @@ -1847,38 +2177,19 @@ contract ValidatorsTest_AddMember is ValidatorsTest { expectedMembersList[0] = validator; vm.prank(group); - validators.addFirstMember(validator, address(0), address(0)); - _additionEpoch = validators.getEpochNumber(); - - (address[] memory members, , , , , , ) = validators.getValidatorGroup(group); - - assertEq(members, expectedMembersList); - } - - function test_Reverts_AddFirstMemberToTheList_WhenL2() public { - _whenL2(); - address[] memory expectedMembersList = new address[](1); - expectedMembersList[0] = validator; - - vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); - validators.addFirstMember(validator, address(0), address(0)); - } - - function test_Reverts_AddMemberToTheList_WhenL2() public { - _whenL2(); - address[] memory expectedMembersList = new address[](1); - expectedMembersList[0] = validator; - - vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); - validators.addMember(validator); + validators.addFirstMember(validator, address(0), address(0)); + _additionEpoch = getEpochNumber(); + + (address[] memory members, , , , , , ) = validators.getValidatorGroup(group); + + assertEq(members, expectedMembersList); } function test_ShouldUpdateGroupSizeHistory() public { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - _additionEpoch = validators.getEpochNumber(); + _additionEpoch = getEpochNumber(); + (, , , , uint256[] memory _sizeHistory, , ) = validators.getValidatorGroup(group); assertEq(_sizeHistory.length, 1); @@ -1888,7 +2199,7 @@ contract ValidatorsTest_AddMember is ValidatorsTest { function test_ShouldUpdateMembershipHistoryOfMember() public { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - _additionEpoch = validators.getEpochNumber(); + _additionEpoch = getEpochNumber(); uint256 expectedEntries = 1; @@ -1908,7 +2219,7 @@ contract ValidatorsTest_AddMember is ValidatorsTest { function test_ShouldMarkGroupAsEligible() public { vm.prank(group); validators.addFirstMember(validator, address(0), address(0)); - _additionEpoch = validators.getEpochNumber(); + _additionEpoch = getEpochNumber(); assertTrue(election.isEligible(group)); } @@ -1935,8 +2246,6 @@ contract ValidatorsTest_AddMember is ValidatorsTest { validators.addMember(otherValidator); } - uint256[] expectedSizeHistory; - function test_ShouldUpdateGroupsSizeHistoryAndBalanceRequirements_WhenAddingManyValidatorsAffiliatedWithGroup() public { @@ -2040,7 +2349,12 @@ contract ValidatorsTest_AddMember is ValidatorsTest { } } +contract ValidatorsTest_AddMember_L2 is ValidatorsTest_L2, ValidatorsTest_AddMember {} + contract ValidatorsTest_RemoveMember is ValidatorsTest { + uint256 _registrationEpoch; + uint256 _additionEpoch; + function setUp() public { super.setUp(); _registerValidatorGroupWithMembers(group, 1); @@ -2058,14 +2372,10 @@ contract ValidatorsTest_RemoveMember is ValidatorsTest { assertEq(members.length, expectedMembersList.length); } - uint256 _registrationEpoch; - uint256 _additionEpoch; - function test_ShouldUpdateMemberMembershipHistory() public { vm.prank(group); validators.removeMember(validator); - uint256 _expectedEpoch = validators.getEpochNumber(); - + uint256 _expectedEpoch = getEpochNumber(); ( uint256[] memory _epochs, address[] memory _membershipGroups, @@ -2129,6 +2439,8 @@ contract ValidatorsTest_RemoveMember is ValidatorsTest { } } +contract ValidatorsTest_RemoveMember_L2 is ValidatorsTest_L2, ValidatorsTest_RemoveMember {} + contract ValidatorsTest_ReorderMember is ValidatorsTest { function setUp() public { super.setUp(); @@ -2148,17 +2460,6 @@ contract ValidatorsTest_ReorderMember is ValidatorsTest { assertEq(expectedMembersList.length, members.length); } - function test_Reverts_ReorderGroupMemberList_WhenL2() public { - _whenL2(); - address[] memory expectedMembersList = new address[](2); - expectedMembersList[0] = vm.addr(1); - expectedMembersList[1] = validator; - - vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); - validators.reorderMember(vm.addr(1), validator, address(0)); - } - function test_Emits_ValidatorGroupMemberReorderedEvent() public { vm.expectEmit(true, true, true, true); emit ValidatorGroupMemberReordered(group, vm.addr(1)); @@ -2189,6 +2490,8 @@ contract ValidatorsTest_ReorderMember is ValidatorsTest { } } +contract ValidatorsTest_ReorderMember_L2 is ValidatorsTest_L2, ValidatorsTest_ReorderMember {} + contract ValidatorsTest_SetNextCommissionUpdate is ValidatorsTest { uint256 newCommission = commission.unwrap().add(1); @@ -2206,13 +2509,6 @@ contract ValidatorsTest_SetNextCommissionUpdate is ValidatorsTest { assertEq(_commission, commission.unwrap()); } - function test_Reverts_SetValidatorGroupCommission_WhenL2() public { - _whenL2(); - vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); - validators.setNextCommissionUpdate(newCommission); - } - function test_ShouldSetValidatorGroupNextCommission() public { vm.prank(group); validators.setNextCommissionUpdate(newCommission); @@ -2247,15 +2543,36 @@ contract ValidatorsTest_SetNextCommissionUpdate is ValidatorsTest { } } -contract ValidatorsTest_UpdateCommission is ValidatorsTest { +contract ValidatorsTest_SetNextCommissionUpdate_L2 is + ValidatorsTest_L2, + ValidatorsTest_SetNextCommissionUpdate +{} + +contract ValidatorsTest_UpdateCommission_Setup is ValidatorsTest { uint256 newCommission = commission.unwrap().add(1); function setUp() public { super.setUp(); - _registerValidatorGroupHelper(group, 1); + _registerValidatorGroupHelper(group, 2); + + _registerValidatorHelper(validator, validatorPk); + _registerValidatorHelper(otherValidator, otherValidatorPk); + + vm.prank(validator); + validators.affiliate(group); + (, , address _affiliation1, , ) = validators.getValidator(validator); + + vm.prank(otherValidator); + validators.affiliate(group); + (, , address _affiliation2, , ) = validators.getValidator(otherValidator); + + require(_affiliation1 == group, "Affiliation failed."); + require(_affiliation2 == group, "Affiliation failed."); } +} +contract ValidatorsTest_UpdateCommission is ValidatorsTest_UpdateCommission_Setup { function test_ShouldSetValidatorGroupCommission() public { vm.prank(group); validators.setNextCommissionUpdate(newCommission); @@ -2270,13 +2587,6 @@ contract ValidatorsTest_UpdateCommission is ValidatorsTest { assertEq(_commission, newCommission); } - function test_Reverts_SetValidatorGroupCommission_WhenL2() public { - _whenL2(); - vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); - validators.setNextCommissionUpdate(newCommission); - } - function test_Emits_ValidatorGroupCommissionUpdated() public { vm.prank(group); validators.setNextCommissionUpdate(newCommission); @@ -2299,7 +2609,7 @@ contract ValidatorsTest_UpdateCommission is ValidatorsTest { validators.updateCommission(); } - function test_Reverts_WhennoCommissionHasBeenQueued() public { + function test_Reverts_WhenNoCommissionHasBeenQueued() public { vm.expectRevert("No commission update queued"); vm.prank(group); @@ -2321,6 +2631,43 @@ contract ValidatorsTest_UpdateCommission is ValidatorsTest { } } +contract ValidatorsTest_UpdateCommission_L1 is ValidatorsTest_UpdateCommission_Setup { + function _performCommissionUpdate() internal { + vm.prank(group); + validators.addFirstMember(validator, address(0), address(0)); + + vm.prank(group); + validators.setNextCommissionUpdate(newCommission); + blockTravel(commissionUpdateDelay); + + vm.prank(group); + validators.updateCommission(); + } + + function test_ShouldNotTryTodSendMultipleValidatorPayments_WhenL1() public { + assertDoesNotEmit(_performCommissionUpdate, "SendValidatorPaymentCalled(address)"); + } +} + +contract ValidatorsTest_UpdateCommission_L2 is ValidatorsTest_L2, ValidatorsTest_UpdateCommission { + function test_ShouldSendMultipleValidatorPayments_WhenL2() public { + vm.prank(group); + validators.addFirstMember(validator, address(0), address(0)); + vm.prank(group); + validators.addMember(otherValidator); + vm.prank(group); + validators.setNextCommissionUpdate(newCommission); + blockTravel(commissionUpdateDelay); + + vm.expectEmit(true, true, true, true); + emit SendValidatorPaymentCalled(validator); + vm.expectEmit(true, true, true, true); + emit SendValidatorPaymentCalled(otherValidator); + vm.prank(group); + validators.updateCommission(); + } +} + contract ValidatorsTest_CalculateEpochScore is ValidatorsTest { function setUp() public { super.setUp(); @@ -2412,7 +2759,14 @@ contract ValidatorsTest_CalculateEpochScore is ValidatorsTest { } } -contract ValidatorsTest_CalculateGroupEpochScore is ValidatorsTest { +contract ValidatorsTest_CalculateEpochScore_L2 is ValidatorsTest_L2 { + function test_Reverts_WhenL2() public { + vm.expectRevert("This method is no longer supported in L2."); + validators.calculateEpochScore(1); + } +} + +contract ValidatorsTest_CalculateGroupEpochScore_Setup is ValidatorsTest { function setUp() public { super.setUp(); @@ -2454,7 +2808,11 @@ contract ValidatorsTest_CalculateGroupEpochScore is ValidatorsTest { return (unwrapedUptimes, expectedScore); } +} +contract ValidatorsTest_CalculateGroupEpochScore_L1 is + ValidatorsTest_CalculateGroupEpochScore_Setup +{ function test_ShouldCalculateGroupScoreCorrectly_WhenThereIs1ValidatorGroup() public { FixidityLib.Fraction[] memory uptimes = new FixidityLib.Fraction[](1); uptimes[0] = FixidityLib.newFixedFraction(969, 1000); @@ -2557,7 +2915,24 @@ contract ValidatorsTest_CalculateGroupEpochScore is ValidatorsTest { } } -contract ValidatorsTest_UpdateValidatorScoreFromSigner is ValidatorsTest { +contract ValidatorsTest_CalculateGroupEpochScore_L2 is + ValidatorsTest_CalculateGroupEpochScore_Setup, + ValidatorsTest_L2 +{ + function test_Reverts() public { + FixidityLib.Fraction[] memory uptimes = new FixidityLib.Fraction[](5); + uptimes[0] = FixidityLib.newFixedFraction(9, 10); + uptimes[1] = FixidityLib.newFixedFraction(9, 10); + uptimes[3] = FixidityLib.newFixedFraction(9, 10); + uptimes[4] = FixidityLib.newFixedFraction(9, 10); + + (uint256[] memory unwrapedUptimes, ) = _computeGroupUptimeCalculation(uptimes); + vm.expectRevert("This method is no longer supported in L2."); + validators.calculateGroupEpochScore(unwrapedUptimes); + } +} + +contract ValidatorsTest_UpdateValidatorScoreFromSigner_Setup is ValidatorsTest { FixidityLib.Fraction public gracePeriod; FixidityLib.Fraction public uptime; uint256 public _epochScore; @@ -2585,7 +2960,11 @@ contract ValidatorsTest_UpdateValidatorScoreFromSigner is ValidatorsTest { abi.encodePacked(_epochScore, FixidityLib.fixed1().unwrap()) ); } +} +contract ValidatorsTest_UpdateValidatorScoreFromSigner_L1 is + ValidatorsTest_UpdateValidatorScoreFromSigner_Setup +{ function test_ShouldUpdateValidatorScore_WhenUptimeInRange0And1() public { uint256 _expectedScore = FixidityLib .multiply( @@ -2594,6 +2973,7 @@ contract ValidatorsTest_UpdateValidatorScoreFromSigner is ValidatorsTest { ) .unwrap(); + vm.prank(address(0)); validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); (, , , uint256 _actualScore, ) = validators.getValidator(validator); @@ -2602,6 +2982,7 @@ contract ValidatorsTest_UpdateValidatorScoreFromSigner is ValidatorsTest { } function test_ShouldUpdateValidatorScore_WhenValidatorHasNonZeroScore() public { + vm.prank(address(0)); validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); uint256 _expectedScore = FixidityLib @@ -2624,6 +3005,7 @@ contract ValidatorsTest_UpdateValidatorScoreFromSigner is ValidatorsTest { ) .unwrap(); + vm.prank(address(0)); validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); (, , , uint256 _actualScore, ) = validators.getValidator(validator); @@ -2632,12 +3014,31 @@ contract ValidatorsTest_UpdateValidatorScoreFromSigner is ValidatorsTest { function test_Reverts_WhenUptimeGreaterThan1() public { uptime = FixidityLib.add(FixidityLib.fixed1(), FixidityLib.newFixedFraction(1, 10)); + vm.prank(address(0)); vm.expectRevert("Uptime cannot be larger than one"); validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); } } +contract ValidatorsTest_UpdateValidatorScoreFromSigner is + ValidatorsTest_UpdateValidatorScoreFromSigner_Setup, + ValidatorsTest_L2 +{ + function test_Reverts_WhenL2() public { + vm.expectRevert("This method is no longer supported in L2."); + + vm.prank(address(0)); + validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); + } +} + contract ValidatorsTest_UpdateMembershipHistory is ValidatorsTest { + address[] public expectedMembershipHistoryGroups; + uint256[] public expectedMembershipHistoryEpochs; + + address[] public actualMembershipHistoryGroups; + uint256[] public actualMembershipHistoryEpochs; + function setUp() public { super.setUp(); _registerValidatorHelper(validator, validatorPk); @@ -2648,12 +3049,6 @@ contract ValidatorsTest_UpdateMembershipHistory is ValidatorsTest { } } - address[] public expectedMembershipHistoryGroups; - uint256[] public expectedMembershipHistoryEpochs; - - address[] public actualMembershipHistoryGroups; - uint256[] public actualMembershipHistoryEpochs; - function test_ShouldOverwritePreviousEntry_WhenChangingGroupsInSameEpoch() public { uint256 numTest = 10; @@ -2661,8 +3056,8 @@ contract ValidatorsTest_UpdateMembershipHistory is ValidatorsTest { expectedMembershipHistoryEpochs.push(validatorRegistrationEpochNumber); for (uint256 i = 0; i < numTest; i++) { - blockTravel(ph.epochSize()); - uint256 epochNumber = validators.getEpochNumber(); + travelNEpoch(1); + uint256 epochNumber = getEpochNumber(); vm.prank(validator); validators.affiliate(group); @@ -2711,9 +3106,8 @@ contract ValidatorsTest_UpdateMembershipHistory is ValidatorsTest { expectedMembershipHistoryEpochs.push(validatorRegistrationEpochNumber); for (uint256 i = 0; i < membershipHistoryLength.add(1); i++) { - blockTravel(ph.epochSize()); - uint256 epochNumber = validators.getEpochNumber(); - + travelNEpoch(1); + uint256 epochNumber = getEpochNumber(); vm.prank(validator); validators.affiliate(vm.addr(i + 1)); vm.prank(vm.addr(i + 1)); @@ -2742,7 +3136,12 @@ contract ValidatorsTest_UpdateMembershipHistory is ValidatorsTest { } } -contract ValidatorsTest_GetMembershipInLastEpoch is ValidatorsTest { +contract ValidatorsTest_UpdateMembershipHistory_L2 is + ValidatorsTest_L2, + ValidatorsTest_UpdateMembershipHistory +{} + +contract ValidatorsTest_GetMembershipInLastEpoch_Setup is ValidatorsTest { function setUp() public { super.setUp(); @@ -2753,10 +3152,35 @@ contract ValidatorsTest_GetMembershipInLastEpoch is ValidatorsTest { _registerValidatorGroupHelper(vm.addr(i), 1); } } +} +contract ValidatorsTest_GetMembershipInLastEpoch is ValidatorsTest_GetMembershipInLastEpoch_Setup { function test_ShouldAlwaysReturnCorrectMembershipForLastEpoch_WhenChangingMoreTimesThanMembershipHistoryLength() public { + for (uint256 i = 0; i < membershipHistoryLength.add(1); i++) { + travelNEpoch(1); + + vm.prank(validator); + validators.affiliate(vm.addr(i + 1)); + vm.prank(vm.addr(i + 1)); + validators.addFirstMember(validator, address(0), address(0)); + + if (i == 0) { + assertEq(validators.getMembershipInLastEpoch(validator), address(0)); + } else { + assertEq(validators.getMembershipInLastEpoch(validator), vm.addr(i)); + } + } + } +} + +contract ValidatorsTest_GetMembershipInLastEpoch_L1 is + ValidatorsTest_GetMembershipInLastEpoch_Setup +{ + function test_MaintainsMembershipAfterL2Transition() public { + address lastValidatorGroup; + address nextValidatorGroup; for (uint256 i = 0; i < membershipHistoryLength.add(1); i++) { blockTravel(ph.epochSize()); @@ -2768,28 +3192,68 @@ contract ValidatorsTest_GetMembershipInLastEpoch is ValidatorsTest { if (i == 0) { assertEq(validators.getMembershipInLastEpoch(validator), address(0)); } else { + lastValidatorGroup = vm.addr(i); + nextValidatorGroup = vm.addr(i + 1); assertEq(validators.getMembershipInLastEpoch(validator), vm.addr(i)); } } + + whenL2WithEpochManagerInitialization(); + + assertEq(validators.getMembershipInLastEpoch(validator), lastValidatorGroup); + epochManager.setCurrentEpochNumber(epochManager.getCurrentEpochNumber() + 1); + assertEq(validators.getMembershipInLastEpoch(validator), nextValidatorGroup); } +} - function test_Reverts_getMembershipInLastEpoch_WhenL2() public { - blockTravel(ph.epochSize()); +contract ValidatorsTest_GetMembershipInLastEpoch_L2 is + ValidatorsTest_L2, + ValidatorsTest_GetMembershipInLastEpoch +{} - vm.prank(validator); - validators.affiliate(vm.addr(1)); - vm.prank(vm.addr(1)); - validators.addFirstMember(validator, address(0), address(0)); +contract ValidatorsTest_GetTopGroupValidators is ValidatorsTest { + function setUp() public { + super.setUp(); - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - validators.getMembershipInLastEpoch(validator); + _registerValidatorGroupWithMembersHavingSigners(group, 5); + } + + function test_ShouldReturnTheSigner() public { + address[] memory _validatorSigner = validators.getTopGroupValidators(group, 3); + assertEq(_validatorSigner[0], accounts.getValidatorSigner(validator)); + assertEq(_validatorSigner[1], accounts.getValidatorSigner(vm.addr(1))); + assertFalse(_validatorSigner[0] == validator); + } +} + +contract ValidatorsTest_GetTopGroupValidators_L2 is + ValidatorsTest_L2, + ValidatorsTest_GetTopGroupValidators +{} + +contract ValidatorsTest_GetTopGroupValidatorsAccounts is ValidatorsTest { + function setUp() public { + super.setUp(); + + _registerValidatorGroupWithMembersHavingSigners(group, 5); + } + + function test_ShouldReturnTheAccount() public { + address[] memory validatorAccount = validators.getTopGroupValidatorsAccounts(group, 3); + assertEq(validatorAccount[0], validator); + assertEq(validatorAccount[1], vm.addr(1)); + assertFalse(validatorAccount[0] == accounts.getValidatorSigner(validator)); } } +contract ValidatorsTest_GetTopGroupValidatorsAccounts_L2 is + ValidatorsTest_GetTopGroupValidatorsAccounts, + ValidatorsTest_L2 +{} + contract ValidatorsTest_GetEpochSize is ValidatorsTest { function test_ShouldReturn17280() public { - assertEq(validators.getEpochSize(), 17280); + assertEq(IPrecompiles(address(validators)).getEpochSize(), 17280); } } @@ -2854,6 +3318,11 @@ contract ValidatorsTest_GetAccountLockedGoldRequirement is ValidatorsTest { } } +contract ValidatorsTest_GetAccountLockedGoldRequirement_L2 is + ValidatorsTest_L2, + ValidatorsTest_GetAccountLockedGoldRequirement +{} + contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { uint256 public numMembers = 5; uint256 public maxPayment = 20122394876; @@ -2949,26 +3418,24 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { ) ); + vm.prank(address(0)); validators.updateValidatorScoreFromSigner(validator, uptime.unwrap()); } - function test_Reverts_WhenL2_WhenValidatorAndGroupMeetBalanceRequirements() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - validators.distributeEpochPaymentsFromSigner(validator, maxPayment); - } - function test_ShouldPayValidator_WhenValidatorAndGroupMeetBalanceRequirements() public { + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(validator), expectedValidatorPayment); } function test_ShouldPayGroup_WhenValidatorAndGroupMeetBalanceRequirements() public { + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(group), expectedGroupPayment); } function test_ShouldPayDelegatee_WhenValidatorAndGroupMeetBalanceRequirements() public { + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(paymentDelegatee), expectedDelegatedPayment); } @@ -2976,7 +3443,8 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { function test_ShouldReturnTheExpectedTotalPayment_WhenValidatorAndGroupMeetBalanceRequirements() public { - validators.distributeEpochPaymentsFromSigner(validator, maxPayment); + // validators.distributeEpochPaymentsFromSigner(validator, maxPayment); + vm.prank(address(0)); assertEq( validators.distributeEpochPaymentsFromSigner(validator, maxPayment), expectedTotalPayment @@ -2992,6 +3460,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { vm.prank(validator); accounts.deletePaymentDelegation(); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(validator), expectedValidatorPayment); } @@ -3005,7 +3474,8 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { vm.prank(validator); accounts.deletePaymentDelegation(); - validators.distributeEpochPaymentsFromSigner(validator, maxPayment); + // validators.distributeEpochPaymentsFromSigner(validator, maxPayment); + vm.prank(address(0)); assertEq( validators.distributeEpochPaymentsFromSigner(validator, maxPayment), expectedTotalPayment @@ -3021,6 +3491,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { vm.prank(validator); accounts.deletePaymentDelegation(); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(group), expectedGroupPayment); } @@ -3028,6 +3499,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { function test_shouldPayValidatorOnlyHalf_WhenSlashingMultiplierIsHalved() public { vm.prank(paymentDelegatee); validators.halveSlashingMultiplier(group); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(validator), halfExpectedValidatorPayment); @@ -3036,6 +3508,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { function test_shouldPayGroupOnlyHalf_WhenSlashingMultiplierIsHalved() public { vm.prank(paymentDelegatee); validators.halveSlashingMultiplier(group); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(group), halfExpectedGroupPayment); @@ -3044,6 +3517,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { function test_shouldPayDelegateeOnlyHalf_WhenSlashingMultiplierIsHalved() public { vm.prank(paymentDelegatee); validators.halveSlashingMultiplier(group); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(paymentDelegatee), halfExpectedDelegatedPayment); @@ -3052,8 +3526,8 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { function test_shouldReturnHalfExpectedTotalPayment_WhenSlashingMultiplierIsHalved() public { vm.prank(paymentDelegatee); validators.halveSlashingMultiplier(group); - validators.distributeEpochPaymentsFromSigner(validator, maxPayment); + vm.prank(address(0)); assertEq( validators.distributeEpochPaymentsFromSigner(validator, maxPayment), halfExpectedTotalPayment @@ -3066,6 +3540,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalValidatorLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(validator), 0); } @@ -3076,6 +3551,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalValidatorLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(group), 0); } @@ -3086,6 +3562,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalValidatorLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(paymentDelegatee), 0); } @@ -3096,6 +3573,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalValidatorLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); assertEq(validators.distributeEpochPaymentsFromSigner(validator, maxPayment), 0); } @@ -3105,6 +3583,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalGroupLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(validator), 0); } @@ -3115,6 +3594,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalGroupLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(group), 0); } @@ -3125,6 +3605,7 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalGroupLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); validators.distributeEpochPaymentsFromSigner(validator, maxPayment); assertEq(stableToken.balanceOf(paymentDelegatee), 0); } @@ -3135,11 +3616,45 @@ contract ValidatorsTest_DistributeEpochPaymentsFromSigner is ValidatorsTest { originalGroupLockedGoldRequirements.value.sub(11) ); + vm.prank(address(0)); assertEq(validators.distributeEpochPaymentsFromSigner(validator, maxPayment), 0); } } -contract ValidatorsTest_ForceDeaffiliateIfValidator is ValidatorsTest { +contract ValidatorsTest_DistributeEpochPaymentsFromSigner_L2 is ValidatorsTest_L2 { + function test_Reverts_WhenL2() public { + vm.prank(address(0)); + vm.expectRevert("This method is no longer supported in L2."); + validators.distributeEpochPaymentsFromSigner(validator, 100); + } +} + +contract ValidatorsTest_MintStableToEpochManager_L1 is ValidatorsTest { + function test_Reverts_WhenL1() public { + vm.expectRevert("This method is not supported in L1."); + validators.mintStableToEpochManager(5); + } +} + +contract ValidatorsTest_MintStableToEpochManager_L2 is ValidatorsTest_L2 { + function test_Reverts_WhenCalledByOtherThanEpochManager() public { + vm.expectRevert("only registered contract"); + validators.mintStableToEpochManager(5); + } + + function test_WhenMintAmountIsZero() public { + vm.prank(address(epochManager)); + validators.mintStableToEpochManager(0); + } + + function test_ShouldMintStableToEpochManager() public { + vm.prank(address(epochManager)); + validators.mintStableToEpochManager(5); + assertEq(stableToken.balanceOf(address(epochManager)), 5); + } +} + +contract ValidatorsTest_ForceDeaffiliateIfValidator_Setup is ValidatorsTest { function setUp() public { super.setUp(); @@ -3151,7 +3666,11 @@ contract ValidatorsTest_ForceDeaffiliateIfValidator is ValidatorsTest { lockedGold.addSlasherTest(paymentDelegatee); } +} +contract ValidatorsTest_ForceDeaffiliateIfValidator is + ValidatorsTest_ForceDeaffiliateIfValidator_Setup +{ function test_ShouldSucceed_WhenSenderIsWhitelistedSlashingAddress() public { vm.prank(paymentDelegatee); validators.forceDeaffiliateIfValidator(validator); @@ -3159,31 +3678,49 @@ contract ValidatorsTest_ForceDeaffiliateIfValidator is ValidatorsTest { assertEq(affiliation, address(0)); } - function test_Reverts_WhenSenderIsWhitelistedSlashingAddress_WhenL2() public { - _whenL2(); + function test_Reverts_WhenSenderNotApprovedAddress() public { + vm.expectRevert("Only registered slasher can call"); + validators.forceDeaffiliateIfValidator(validator); + } +} + +contract ValidatorsTest_ForceDeaffiliateIfValidator_L1 is + ValidatorsTest_ForceDeaffiliateIfValidator_Setup +{ + function _performForcedDeaffiliation() internal { vm.prank(paymentDelegatee); - vm.expectRevert("This method is no longer supported in L2."); validators.forceDeaffiliateIfValidator(validator); } - function test_Reverts_WhenSenderNotApprovedAddress() public { - vm.expectRevert("Only registered slasher can call"); + function test_ShouldNotTryToSendValidatorPayment_WhenL1() public { + assertDoesNotEmit(_performForcedDeaffiliation, "SendValidatorPaymentCalled(address)"); + } +} + +contract ValidatorsTest_ForceDeaffiliateIfValidator_L2 is + ValidatorsTest_ForceDeaffiliateIfValidator, + ValidatorsTest_L2 +{ + function test_ShouldSendValidatorPayment_WhenL2() public { + vm.expectEmit(true, true, true, true); + emit SendValidatorPaymentCalled(validator); + vm.prank(paymentDelegatee); validators.forceDeaffiliateIfValidator(validator); } } contract ValidatorsTest_GroupMembershipInEpoch is ValidatorsTest { + struct EpochInfo { + uint256 epochNumber; + address groupy; + } + uint256 totalEpochs = 24; uint256 gapSize = 3; uint256 contractIndex; EpochInfo[] public epochInfoList; - struct EpochInfo { - uint256 epochNumber; - address groupy; - } - function setUp() public { super.setUp(); @@ -3197,7 +3734,7 @@ contract ValidatorsTest_GroupMembershipInEpoch is ValidatorsTest { for (uint256 i = 1; i < totalEpochs; i++) { blockTravel(ph.epochSize()); - uint256 epochNumber = validators.getEpochNumber(); + uint256 epochNumber = getEpochNumber(); if (i % gapSize == 0) { address _group = (i % gapSize.mul(gapSize)) != 0 @@ -3246,29 +3783,6 @@ contract ValidatorsTest_GroupMembershipInEpoch is ValidatorsTest { } } - function test_Reverts_GroupMembershipInEpoch_WhenL2() public { - _whenL2(); - for (uint256 i = 0; i < epochInfoList.length; i++) { - address _group = epochInfoList[i].groupy; - - if (epochInfoList.length.sub(i) <= membershipHistoryLength) { - vm.expectRevert("This method is no longer supported in L2."); - validators.groupMembershipInEpoch( - validator, - epochInfoList[i].epochNumber, - uint256(1).add(i) - ); - } else { - vm.expectRevert("This method is no longer supported in L2."); - validators.groupMembershipInEpoch( - validator, - epochInfoList[i].epochNumber, - uint256(1).add(i) - ); - } - } - } - function test_Reverts_WhenEpochNumberAtGivenIndexIsGreaterThanProvidedEpochNumber() public { vm.expectRevert("index out of bounds"); validators.groupMembershipInEpoch( @@ -3288,13 +3802,13 @@ contract ValidatorsTest_GroupMembershipInEpoch is ValidatorsTest { } function test_Reverts_WhenProvidedEpochNumberGreaterThanCurrentEpochNumber() public { - uint256 _epochNumber = validators.getEpochNumber(); + uint256 _epochNumber = getEpochNumber(); vm.expectRevert("Epoch cannot be larger than current"); validators.groupMembershipInEpoch(validator, _epochNumber.add(1), contractIndex); } function test_Reverts_WhenProvidedIndexGreaterThanIndexOnChain() public { - uint256 _epochNumber = validators.getEpochNumber(); + uint256 _epochNumber = getEpochNumber(); vm.expectRevert("index out of bounds"); validators.groupMembershipInEpoch(validator, _epochNumber, contractIndex.add(1)); } @@ -3309,6 +3823,11 @@ contract ValidatorsTest_GroupMembershipInEpoch is ValidatorsTest { } } +contract ValidatorsTest_GroupMembershipInEpoch_L2 is + ValidatorsTest_GroupMembershipInEpoch, + ValidatorsTest_L2 +{} + contract ValidatorsTest_HalveSlashingMultiplier is ValidatorsTest { function setUp() public { super.setUp(); @@ -3330,14 +3849,6 @@ contract ValidatorsTest_HalveSlashingMultiplier is ValidatorsTest { } } - function test_Reverts_HalveSlashingMultiplier_WhenL2() public { - _whenL2(); - FixidityLib.Fraction memory expectedMultiplier = FixidityLib.fixed1(); - vm.prank(paymentDelegatee); - vm.expectRevert("This method is no longer supported in L2."); - validators.halveSlashingMultiplier(group); - } - function test_ShouldUpdateLastSlashedTimestamp() public { (, , , , , , uint256 initialLastSlashed) = validators.getValidatorGroup(group); @@ -3354,6 +3865,11 @@ contract ValidatorsTest_HalveSlashingMultiplier is ValidatorsTest { } } +contract ValidatorsTest_HalveSlashingMultiplier_L2 is + ValidatorsTest_HalveSlashingMultiplier, + ValidatorsTest_L2 +{} + contract ValidatorsTest_ResetSlashingMultiplier is ValidatorsTest { function setUp() public { super.setUp(); @@ -3385,15 +3901,6 @@ contract ValidatorsTest_ResetSlashingMultiplier is ValidatorsTest { assertEq(actualMultiplier, FixidityLib.fixed1().unwrap()); } - function test_Reverts_WhenSlashingMultiplierIsResetAfterResetPeriod_WhenL2() public { - _whenL2(); - timeTravel(slashingMultiplierResetPeriod); - - vm.prank(group); - vm.expectRevert("This method is no longer supported in L2."); - validators.resetSlashingMultiplier(); - } - function test_Reverts_WhenSlashingMultiplierIsResetBeforeResetPeriod() public { vm.expectRevert("`resetSlashingMultiplier` called before resetPeriod expired"); vm.prank(group); @@ -3409,11 +3916,9 @@ contract ValidatorsTest_ResetSlashingMultiplier is ValidatorsTest { (, , , , , uint256 actualMultiplier, ) = validators.getValidatorGroup(group); assertEq(actualMultiplier, FixidityLib.fixed1().unwrap()); } - - function test_Reverts_SetSlashingMultiplierResetPeriod_WhenL2() public { - _whenL2(); - uint256 newResetPeriod = 10 * DAY; - vm.expectRevert("This method is no longer supported in L2."); - validators.setSlashingMultiplierResetPeriod(newResetPeriod); - } } + +contract ValidatorsTest_ResetSlashingMultiplier_L2 is + ValidatorsTest_ResetSlashingMultiplier, + ValidatorsTest_L2 +{} diff --git a/packages/protocol/test-sol/unit/governance/validators/mocks/CompileValidatorIntegrationMock.t.sol b/packages/protocol/test-sol/unit/governance/validators/mocks/CompileValidatorIntegrationMock.t.sol new file mode 100644 index 00000000000..46209af1d95 --- /dev/null +++ b/packages/protocol/test-sol/unit/governance/validators/mocks/CompileValidatorIntegrationMock.t.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "celo-foundry-8/Test.sol"; +import "forge-std/console.sol"; + +// here only to forge compile of ValidatorsMock +import "@test-sol/unit/governance/validators/mocks/ValidatorsMock.sol"; + +contract CompileValidatorIntegrationMock is Test { + function test_nop() public view { + console.log("nop"); + } +} diff --git a/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMock.sol b/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMock.sol new file mode 100644 index 00000000000..55470bc89c4 --- /dev/null +++ b/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMock.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.7 <0.8.20; + +import "@celo-contracts-8/governance/Validators.sol"; +import "@celo-contracts/common/FixidityLib.sol"; + +/** + * @title A wrapper around Validators that exposes onlyVm functions for testing. + */ +contract ValidatorsMock is Validators(true) { + function computeEpochReward(address, uint256, uint256) external pure override returns (uint256) { + return 1; + } +} diff --git a/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMockTunnel.sol b/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMockTunnel.sol new file mode 100644 index 00000000000..be98aa05d75 --- /dev/null +++ b/packages/protocol/test-sol/unit/governance/validators/mocks/ValidatorsMockTunnel.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.5.13; +pragma experimental ABIEncoderV2; + +import "@celo-contracts/governance/interfaces/IValidators.sol"; +import { Test as ForgeTest } from "forge-std/Test.sol"; + +contract ValidatorsMockTunnel is ForgeTest { + IValidators private tunnelValidators; + address validatorContractAddress; + + struct InitParamsTunnel { + // The number of blocks to delay a ValidatorGroup's commission + uint256 commissionUpdateDelay; + uint256 downtimeGracePeriod; + } + + constructor(address _validatorContractAddress) public { + validatorContractAddress = _validatorContractAddress; + tunnelValidators = IValidators(validatorContractAddress); + } + + struct InitParams { + address registryAddress; + uint256 groupRequirementValue; + uint256 groupRequirementDuration; + uint256 validatorRequirementValue; + uint256 validatorRequirementDuration; + uint256 validatorScoreExponent; + uint256 validatorScoreAdjustmentSpeed; + } + + struct InitParams2 { + uint256 _membershipHistoryLength; + uint256 _slashingMultiplierResetPeriod; + uint256 _maxGroupSize; + uint256 _commissionUpdateDelay; + uint256 _downtimeGracePeriod; + } + + // TODO move this to a generic Tunnel helper contract, add to other tunnels. + /* + * Recovers the string encoded in return data from a failing call. + * The data is the RLP encoding of Error(). + * See https://docs.soliditylang.org/en/v0.5.17/control-structures.html#revert + * for details. + */ + function recoverErrorString(bytes memory errorData) internal returns (string memory) { + // Offset in `errorData` due to it starting with the signature for Error(string) + uint256 signatureLength = 4; + uint256 stringEncodingLength = errorData.length - signatureLength; + // Buffer to store the encoded string + bytes memory stringEncodingData = new bytes(stringEncodingLength); + + // The string encoding should be 32-byte aligned, as per RLP encoding + assert(stringEncodingLength % 32 == 0); + + // Start the offset at 32, since the first 32 bytes of a `bytes` variable in + // memory are used to store its length + for (uint256 offset = 32; offset <= stringEncodingLength; offset += 32) { + assembly { + mstore(add(stringEncodingData, offset), mload(add(errorData, add(signatureLength, offset)))) + } + } + string memory errorString = abi.decode(stringEncodingData, (string)); + return errorString; + } + + function MockInitialize( + address sender, + InitParams calldata params, + InitParams2 calldata params2 + ) external returns (bool, bytes memory) { + InitParamsTunnel memory initParamsTunnel = InitParamsTunnel({ + commissionUpdateDelay: params2._commissionUpdateDelay, + downtimeGracePeriod: params2._downtimeGracePeriod + }); + + bytes memory data = abi.encodeWithSignature( + "initialize(address,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,(uint256,uint256))", + params.registryAddress, + params.groupRequirementValue, + params.groupRequirementDuration, + params.validatorRequirementValue, + params.validatorRequirementDuration, + params.validatorScoreExponent, + params.validatorScoreAdjustmentSpeed, + params2._membershipHistoryLength, + params2._slashingMultiplierResetPeriod, + params2._maxGroupSize, + initParamsTunnel + ); + vm.prank(sender); + (bool success, bytes memory errorData) = address(tunnelValidators).call(data); + if (!success) { + string memory errorString = recoverErrorString(errorData); + require(success, errorString); + } + } +} diff --git a/packages/protocol/test-sol/voting/Election.t.sol b/packages/protocol/test-sol/unit/governance/voting/Election.t.sol similarity index 89% rename from packages/protocol/test-sol/voting/Election.t.sol rename to packages/protocol/test-sol/unit/governance/voting/Election.t.sol index 5f9086b8c72..3a58b4a26f8 100644 --- a/packages/protocol/test-sol/voting/Election.t.sol +++ b/packages/protocol/test-sol/unit/governance/voting/Election.t.sol @@ -2,8 +2,10 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; -import { Test } from "celo-foundry/Test.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; + import "@celo-contracts/common/FixidityLib.sol"; +import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/governance/Election.sol"; import "@celo-contracts/governance/test/MockLockedGold.sol"; import "@celo-contracts/governance/test/MockValidators.sol"; @@ -11,8 +13,10 @@ import "@celo-contracts/common/Accounts.sol"; import "@celo-contracts/common/linkedlists/AddressSortedLinkedList.sol"; import "@celo-contracts/identity/test/MockRandom.sol"; import "@celo-contracts/common/Freezer.sol"; -import { Constants } from "../constants.sol"; -import "../utils.sol"; +import "@test-sol/unit/common/mocks/MockEpochManager.sol"; +import "@test-sol/utils/WhenL2.sol"; + +import { TestBlocker } from "@test-sol/unit/common/Blockable.t.sol"; contract ElectionMock is Election(true) { function distributeEpochRewards( @@ -25,46 +29,16 @@ contract ElectionMock is Election(true) { } } -contract ElectionTest is Utils, Constants { +contract ElectionTest is TestWithUtils { using FixidityLib for FixidityLib.Fraction; - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; - - event ElectableValidatorsSet(uint256 min, uint256 max); - event MaxNumGroupsVotedForSet(uint256 maxNumGroupsVotedFor); - event ElectabilityThresholdSet(uint256 electabilityThreshold); - event AllowedToVoteOverMaxNumberOfGroups(address indexed account, bool flag); - event ValidatorGroupMarkedEligible(address indexed group); - event ValidatorGroupMarkedIneligible(address indexed group); - event ValidatorGroupVoteCast(address indexed account, address indexed group, uint256 value); - event ValidatorGroupVoteActivated( - address indexed account, - address indexed group, - uint256 value, - uint256 units - ); - event ValidatorGroupPendingVoteRevoked( - address indexed account, - address indexed group, - uint256 value - ); - event ValidatorGroupActiveVoteRevoked( - address indexed account, - address indexed group, - uint256 value, - uint256 units - ); - event EpochRewardsDistributedToVoters(address indexed group, uint256 value); - Accounts accounts; ElectionMock election; Freezer freezer; MockLockedGold lockedGold; MockValidators validators; MockRandom random; - IRegistry registry; - address registryAddress = 0x000000000000000000000000000000000000ce10; address nonOwner = actor("nonOwner"); address owner = address(this); uint256 electableValidatorsMin = 4; @@ -83,8 +57,38 @@ contract ElectionTest is Utils, Constants { address account9 = actor("account9"); address account10 = actor("account10"); + address epochManagerAddress = actor("epochManagerAddress"); + address[] accountsArray; + TestBlocker blocker; + + event ElectableValidatorsSet(uint256 min, uint256 max); + event MaxNumGroupsVotedForSet(uint256 maxNumGroupsVotedFor); + event ElectabilityThresholdSet(uint256 electabilityThreshold); + event AllowedToVoteOverMaxNumberOfGroups(address indexed account, bool flag); + event ValidatorGroupMarkedEligible(address indexed group); + event ValidatorGroupMarkedIneligible(address indexed group); + event ValidatorGroupVoteCast(address indexed account, address indexed group, uint256 value); + event ValidatorGroupVoteActivated( + address indexed account, + address indexed group, + uint256 value, + uint256 units + ); + event ValidatorGroupPendingVoteRevoked( + address indexed account, + address indexed group, + uint256 value + ); + event ValidatorGroupActiveVoteRevoked( + address indexed account, + address indexed group, + uint256 value, + uint256 units + ); + event EpochRewardsDistributedToVoters(address indexed group, uint256 value); + function createAccount(address account) public { vm.prank(account); accounts.createAccount(); @@ -97,9 +101,9 @@ contract ElectionTest is Utils, Constants { bool vote ) public { validators.setMembers(newGroup, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(newGroup, oldGroup, address(0)); - registry.setAddressFor("Validators", address(validators)); + if (vote) { election.vote(newGroup, 1, oldGroup, address(0)); } @@ -107,7 +111,8 @@ contract ElectionTest is Utils, Constants { function setUp() public { ph.setEpochSize(DAY / 5); - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); + setupRegistry(); + setupEpochManager(); accounts = new Accounts(true); @@ -132,7 +137,6 @@ contract ElectionTest is Utils, Constants { freezer = new Freezer(true); lockedGold = new MockLockedGold(); validators = new MockValidators(); - registry = IRegistry(registryAddress); random = new MockRandom(); registry.setAddressFor("Accounts", address(accounts)); @@ -142,19 +146,20 @@ contract ElectionTest is Utils, Constants { registry.setAddressFor("Random", address(random)); election.initialize( - registryAddress, + REGISTRY_ADDRESS, electableValidatorsMin, electableValidatorsMax, maxNumGroupsVotedFor, electabilityThreshold ); - } - function _whenL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); + blocker = new TestBlocker(); + election.setBlockedByContract(address(blocker)); } } +contract ElectionTest_L2 is ElectionTest, WhenL2 {} + contract ElectionTest_Initialize is ElectionTest { function test_shouldHaveSetOwner() public { assertEq(election.owner(), owner); @@ -177,7 +182,7 @@ contract ElectionTest_Initialize is ElectionTest { function test_shouldRevertWhenCalledAgain() public { vm.expectRevert("contract already initialized"); election.initialize( - registryAddress, + REGISTRY_ADDRESS, electableValidatorsMin, electableValidatorsMax, maxNumGroupsVotedFor, @@ -197,14 +202,13 @@ contract ElectionTest_SetElectabilityThreshold is ElectionTest { vm.expectRevert("Electability threshold must be lower than 100%"); election.setElectabilityThreshold(FixidityLib.fixed1().unwrap() + 1); } - - function test_Revert_setElectabilityThreshold_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - election.setElectabilityThreshold(FixidityLib.fixed1().unwrap() + 1); - } } +contract ElectionTest_SetElectabilityThreshold_L2 is + ElectionTest_SetElectabilityThreshold, + ElectionTest_L2 +{} + contract ElectionTest_SetElectableValidators is ElectionTest { function test_shouldSetElectableValidators() public { uint256 newElectableValidatorsMin = 2; @@ -215,14 +219,6 @@ contract ElectionTest_SetElectableValidators is ElectionTest { assertEq(max, newElectableValidatorsMax); } - function test_Reverts_shouldSetElectableValidators_WhenL2() public { - _whenL2(); - uint256 newElectableValidatorsMin = 2; - uint256 newElectableValidatorsMax = 4; - vm.expectRevert("This method is no longer supported in L2."); - election.setElectableValidators(newElectableValidatorsMin, newElectableValidatorsMax); - } - function test_ShouldEmitTheElectableValidatorsSetEvent() public { uint256 newElectableValidatorsMin = 2; uint256 newElectableValidatorsMax = 4; @@ -253,6 +249,11 @@ contract ElectionTest_SetElectableValidators is ElectionTest { } } +contract ElectionTest_SetElectableValidators_L2 is + ElectionTest_SetElectableValidators, + ElectionTest_L2 +{} + contract ElectionTest_SetMaxNumGroupsVotedFor is ElectionTest { function test_shouldSetMaxNumGroupsVotedFor() public { uint256 newMaxNumGroupsVotedFor = 4; @@ -260,13 +261,6 @@ contract ElectionTest_SetMaxNumGroupsVotedFor is ElectionTest { assertEq(election.maxNumGroupsVotedFor(), newMaxNumGroupsVotedFor); } - function test_Revert_SetMaxNumGroupsVotedFor_WhenL2() public { - _whenL2(); - uint256 newMaxNumGroupsVotedFor = 4; - vm.expectRevert("This method is no longer supported in L2."); - election.setMaxNumGroupsVotedFor(newMaxNumGroupsVotedFor); - } - function test_ShouldEmitMaxNumGroupsVotedForSetEvent() public { uint256 newMaxNumGroupsVotedFor = 4; vm.expectEmit(true, false, false, false); @@ -286,18 +280,17 @@ contract ElectionTest_SetMaxNumGroupsVotedFor is ElectionTest { } } +contract ElectionTest_SetMaxNumGroupsVotedFor_L2 is + ElectionTest_SetMaxNumGroupsVotedFor, + ElectionTest_L2 +{} + contract ElectionTest_SetAllowedToVoteOverMaxNumberOfGroups is ElectionTest { function test_shouldSetAllowedToVoteOverMaxNumberOfGroups() public { election.setAllowedToVoteOverMaxNumberOfGroups(true); assertEq(election.allowedToVoteOverMaxNumberOfGroups(address(this)), true); } - function test_Revert_SetAllowedToVoteOverMaxNumberOfGroups_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - election.setAllowedToVoteOverMaxNumberOfGroups(true); - } - function test_ShouldRevertWhenCalledByValidator() public { validators.setValidator(address(this)); vm.expectRevert("Validators cannot vote for more than max number of groups"); @@ -331,6 +324,11 @@ contract ElectionTest_SetAllowedToVoteOverMaxNumberOfGroups is ElectionTest { } } +contract ElectionTest_SetAllowedToVoteOverMaxNumberOfGroups_L2 is + ElectionTest_SetAllowedToVoteOverMaxNumberOfGroups, + ElectionTest_L2 +{} + contract ElectionTest_MarkGroupEligible is ElectionTest { function setUp() public { super.setUp(); @@ -346,13 +344,6 @@ contract ElectionTest_MarkGroupEligible is ElectionTest { assertEq(eligibleGroups[0], group); } - function test_Revert_MarkGroupEligible_WhenL2() public { - _whenL2(); - address group = address(this); - vm.expectRevert("This method is no longer supported in L2."); - election.markGroupEligible(group, address(0), address(0)); - } - function test_ShouldEmitValidatorGroupMarkedEligibleEvent() public { address group = address(this); vm.expectEmit(true, false, false, false); @@ -374,6 +365,8 @@ contract ElectionTest_MarkGroupEligible is ElectionTest { } } +contract ElectionTest_MarkGroupEligible_L2 is ElectionTest_MarkGroupEligible, ElectionTest_L2 {} + contract ElectionTest_MarkGroupInEligible is ElectionTest { function setUp() public { super.setUp(); @@ -409,6 +402,9 @@ contract ElectionTest_MarkGroupInEligible is ElectionTest { election.markGroupIneligible(address(this)); } } + +contract ElectionTest_MarkGroupInEligible_L2 is ElectionTest_MarkGroupInEligible, ElectionTest_L2 {} + contract ElectionTest_Vote_WhenGroupEligible is ElectionTest { address voter = address(this); address group = account1; @@ -425,9 +421,8 @@ contract ElectionTest_Vote_WhenGroupEligible is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); } function test_ShouldRevert_WhenTheVoterDoesNotHaveSufficientNonVotingBalance() public { @@ -466,11 +461,14 @@ contract ElectionTest_Vote_WhenGroupEligible is ElectionTest { assertEq(election.getPendingVotesForGroupByAccount(group, voter), value - maxNumGroupsVotedFor); } - function test_Revert_Vote_WhenL2() public { - _whenL2(); - - vm.expectRevert("This method is no longer supported in L2."); - election.vote(group, value - maxNumGroupsVotedFor, address(0), address(0)); + function test_Reverts_WhenBlocked_WhenTheVoterIsOverMaxNumberGroupsVotedForButCanVoteForAdditionalGroup() + public + { + address newGroup = WhenVotedForMaxNumberOfGroups(); + election.setAllowedToVoteOverMaxNumberOfGroups(true); + blocker.mockSetBlocked(true); + vm.expectRevert("Contract is blocked from performing this action"); + election.vote(group, value - maxNumGroupsVotedFor, newGroup, address(0)); } function test_ShouldSetTotalVotesByAccount_WhenMaxNumberOfGroupsWasNotReached() public { @@ -528,7 +526,8 @@ contract ElectionTest_Vote_WhenGroupEligible is ElectionTest { function WhenVotesAreBeingActivated() public returns (address newGroup) { newGroup = WhenVotedForMoreThanMaxNumberOfGroups(); - blockTravel(ph.epochSize() + 1); + + travelNEpoch(1); election.activateForAccount(group, voter); } @@ -605,6 +604,11 @@ contract ElectionTest_Vote_WhenGroupEligible is ElectionTest { } } +contract ElectionTest_Vote_WhenGroupEligible_L2 is + ElectionTest_L2, + ElectionTest_Vote_WhenGroupEligible +{} + contract ElectionTest_Vote_WhenGroupEligible_WhenGroupCanReceiveVotes is ElectionTest { address voter = address(this); address group = account1; @@ -621,9 +625,8 @@ contract ElectionTest_Vote_WhenGroupEligible_WhenGroupCanReceiveVotes is Electio members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setNumRegisteredValidators(1); @@ -760,6 +763,11 @@ contract ElectionTest_Vote_WhenGroupEligible_WhenGroupCanReceiveVotes is Electio } } +contract ElectionTest_Vote_WhenGroupEligible_WhenGroupCanReceiveVotes_L2 is + ElectionTest_Vote_WhenGroupEligible_WhenGroupCanReceiveVotes, + ElectionTest_L2 +{} + contract ElectionTest_Vote_GroupNotEligible is ElectionTest { address voter = address(this); address group = account1; @@ -783,11 +791,19 @@ contract ElectionTest_Vote_GroupNotEligible is ElectionTest { } } +contract ElectionTest_Vote_GroupNotEligible_L2 is + ElectionTest_Vote_GroupNotEligible, + ElectionTest_L2 +{} + contract ElectionTest_Activate is ElectionTest { address voter = address(this); address group = account1; uint256 value = 1000; + address voter2 = account2; + uint256 value2 = 573; + function setUp() public { super.setUp(); @@ -795,9 +811,8 @@ contract ElectionTest_Activate is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setMembers(group, members); @@ -811,7 +826,7 @@ contract ElectionTest_Activate is ElectionTest { function WhenEpochBoundaryHasPassed() public { WhenVoterHasPendingVotes(); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group); } @@ -853,28 +868,27 @@ contract ElectionTest_Activate is ElectionTest { function test_ShouldEmitValidatorGroupVoteActivatedEvent_WhenEpochBoundaryHasPassed() public { WhenVoterHasPendingVotes(); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); vm.expectEmit(true, true, true, false); emit ValidatorGroupVoteActivated(voter, group, value, value * 100000000000000000000); election.activate(group); } - function test_Revert_Activate_WhenL2() public { + function test_Reverts_WhenBlocked() public { WhenVoterHasPendingVotes(); - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); + travelNEpoch(1); + + blocker.mockSetBlocked(true); + vm.expectRevert("Contract is blocked from performing this action"); election.activate(group); } - address voter2 = account2; - uint256 value2 = 573; - function WhenAnotherVoterActivatesVotes() public { WhenEpochBoundaryHasPassed(); lockedGold.incrementNonvotingAccountBalance(voter2, value2); vm.prank(voter2); election.vote(group, value2, address(0), address(0)); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); vm.prank(voter2); election.activate(group); } @@ -940,23 +954,22 @@ contract ElectionTest_Activate is ElectionTest { election.activateForAccount(group, voter); } - function test_Revert_ActivateForAccount_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - election.activateForAccount(group, voter); - } - function test_ShouldRevert_WhenTheVoterDoesNotHavePendingVotes() public { vm.expectRevert("Vote value cannot be zero"); election.activate(group); } } +contract ElectionTest_Activate_L2 is ElectionTest_L2, ElectionTest_Activate {} + contract ElectionTest_ActivateForAccount is ElectionTest { address voter = address(this); address group = account1; uint256 value = 1000; + address voter2 = account2; + uint256 value2 = 573; + function setUp() public { super.setUp(); @@ -964,9 +977,8 @@ contract ElectionTest_ActivateForAccount is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setMembers(group, members); @@ -980,7 +992,7 @@ contract ElectionTest_ActivateForAccount is ElectionTest { function WhenEpochBoundaryHasPassed() public { WhenVoterHasPendingVotes(); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activateForAccount(group, voter); } @@ -1022,21 +1034,18 @@ contract ElectionTest_ActivateForAccount is ElectionTest { function test_ShouldEmitValidatorGroupVoteActivatedEvent_WhenEpochBoundaryHasPassed() public { WhenVoterHasPendingVotes(); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); vm.expectEmit(true, true, true, false); emit ValidatorGroupVoteActivated(voter, group, value, value * 100000000000000000000); election.activate(group); } - address voter2 = account2; - uint256 value2 = 573; - function WhenAnotherVoterActivatesVotes() public { WhenEpochBoundaryHasPassed(); lockedGold.incrementNonvotingAccountBalance(voter2, value2); vm.prank(voter2); election.vote(group, value2, address(0), address(0)); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activateForAccount(group, voter2); } @@ -1107,6 +1116,8 @@ contract ElectionTest_ActivateForAccount is ElectionTest { } } +contract ElectionTest_ActivateForAccount_L2 is ElectionTest_L2, ElectionTest_ActivateForAccount {} + contract ElectionTest_RevokePending is ElectionTest { address voter = address(this); address group = account1; @@ -1123,9 +1134,8 @@ contract ElectionTest_RevokePending is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setMembers(group, members); @@ -1264,6 +1274,8 @@ contract ElectionTest_RevokePending is ElectionTest { } } +contract ElectionTest_RevokePending_L2 is ElectionTest_RevokePending, ElectionTest_L2 {} + contract ElectionTest_RevokeActive is ElectionTest { address voter0 = address(this); address voter1 = account1; @@ -1294,9 +1306,8 @@ contract ElectionTest_RevokeActive is ElectionTest { members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(voteValue0 + voteValue1); validators.setNumRegisteredValidators(1); @@ -1306,7 +1317,7 @@ contract ElectionTest_RevokeActive is ElectionTest { // Gives 1000 units to voter 0 election.vote(group, voteValue0, address(0), address(0)); assertConsistentSums(); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group); assertConsistentSums(); @@ -1318,14 +1329,14 @@ contract ElectionTest_RevokeActive is ElectionTest { vm.prank(voter1); election.vote(group, voteValue1, address(0), address(0)); assertConsistentSums(); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); vm.prank(voter1); election.activate(group); assertConsistentSums(); } function WhenTheValidatorGroupHasVotesButIsIneligible() public { - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupIneligible(group); election.revokeActive(group, revokedValue, address(0), address(0), 0); } @@ -1381,7 +1392,7 @@ contract ElectionTest_RevokeActive is ElectionTest { function test_ShouldEmitValidatorGroupActiveVoteRevokedEvent_WhenTheValidatorGroupHasVotesButIsIneligible() public { - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupIneligible(group); vm.expectEmit(true, true, true, false); emit ValidatorGroupActiveVoteRevoked( @@ -1393,6 +1404,12 @@ contract ElectionTest_RevokeActive is ElectionTest { election.revokeActive(group, revokedValue, address(0), address(0), 0); } + function test_Reverts_WhenBlocked() public { + blocker.mockSetBlocked(true); + vm.expectRevert("Contract is blocked from performing this action"); + election.revokeAllActive(group, address(0), address(0), 0); + } + function WhenRevokedValueIsLessThanTheActiveVotesButGroupIsEligible() public { election.revokeActive(group, revokedValue, address(0), address(0), 0); } @@ -1452,7 +1469,7 @@ contract ElectionTest_RevokeActive is ElectionTest { function test_ShouldEmitValidatorGroupActiveVoteRevokedEvent_WhenRevokedValueIsLessThanTheActiveVotesButGroupIsEligible() public { - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupIneligible(group); vm.expectEmit(true, true, true, false); emit ValidatorGroupActiveVoteRevoked( @@ -1537,7 +1554,14 @@ contract ElectionTest_RevokeActive is ElectionTest { } } -contract ElectionTest_ElectionValidatorSigners is ElectionTest { +contract ElectionTest_RevokeActive_L2 is ElectionTest_L2, ElectionTest_RevokeActive {} + +contract ElectionTest_ElectValidatorsAbstract is ElectionTest { + struct MemberWithVotes { + address member; + uint256 votes; + } + address group1 = address(this); address group2 = account1; address group3 = account2; @@ -1571,11 +1595,6 @@ contract ElectionTest_ElectionValidatorSigners is ElectionTest { uint256 totalLockedGold = voter1Weight + voter2Weight + voter3Weight; - struct MemberWithVotes { - address member; - uint256 votes; - } - mapping(address => uint256) votesConsideredForElection; MemberWithVotes[] membersWithVotes; @@ -1598,37 +1617,6 @@ contract ElectionTest_ElectionValidatorSigners is ElectionTest { random.addTestRandomness(block.number + 1, hash); } - // Helper function to sort an array of uint256 - function sort(uint256[] memory data) internal pure returns (uint256[] memory) { - uint256 length = data.length; - for (uint256 i = 0; i < length; i++) { - for (uint256 j = i + 1; j < length; j++) { - if (data[i] > data[j]) { - uint256 temp = data[i]; - data[i] = data[j]; - data[j] = temp; - } - } - } - return data; - } - - function sortMembersWithVotesDesc( - MemberWithVotes[] memory data - ) internal pure returns (MemberWithVotes[] memory) { - uint256 length = data.length; - for (uint256 i = 0; i < length; i++) { - for (uint256 j = i + 1; j < length; j++) { - if (data[i].votes < data[j].votes) { - MemberWithVotes memory temp = data[i]; - data[i] = data[j]; - data[j] = temp; - } - } - } - return data; - } - function WhenThereIsALargeNumberOfGroups() public { lockedGold.setTotalLockedGold(1e25); validators.setNumRegisteredValidators(400); @@ -1655,42 +1643,24 @@ contract ElectionTest_ElectionValidatorSigners is ElectionTest { membersWithVotes.push(MemberWithVotes(members[j], votesConsideredForElection[members[j]])); } validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), prev); - registry.setAddressFor("Validators", address(validators)); vm.prank(voter1); election.vote(group, randomVotes[i], prev, address(0)); prev = group; } } - function test_ShouldElectCorrectValidators_WhenThereIsALargeNumberOfGroups() public { - WhenThereIsALargeNumberOfGroups(); - address[] memory elected = election.electValidatorSigners(); - MemberWithVotes[] memory sortedMembersWithVotes = sortMembersWithVotesDesc(membersWithVotes); - MemberWithVotes[] memory electedUnsorted = new MemberWithVotes[](100); - - for (uint256 i = 0; i < 100; i++) { - electedUnsorted[i] = MemberWithVotes(elected[i], votesConsideredForElection[elected[i]]); - } - MemberWithVotes[] memory electedSorted = sortMembersWithVotesDesc(electedUnsorted); - - for (uint256 i = 0; i < 100; i++) { - assertEq(electedSorted[i].member, sortedMembersWithVotes[i].member); - assertEq(electedSorted[i].votes, sortedMembersWithVotes[i].votes); - } - } - function WhenThereAreSomeGroups() public { validators.setMembers(group1, group1Members); validators.setMembers(group2, group2Members); validators.setMembers(group3, group3Members); - registry.setAddressFor("Validators", address(this)); + vm.startPrank(address(validators)); election.markGroupEligible(group1, address(0), address(0)); election.markGroupEligible(group2, address(0), group1); election.markGroupEligible(group3, address(0), group2); - registry.setAddressFor("Validators", address(validators)); + vm.stopPrank(); lockedGold.incrementNonvotingAccountBalance(address(voter1), voter1Weight); lockedGold.incrementNonvotingAccountBalance(address(voter2), voter2Weight); @@ -1700,36 +1670,89 @@ contract ElectionTest_ElectionValidatorSigners is ElectionTest { validators.setNumRegisteredValidators(7); } - function test_ShouldReturnThatGroupsMemberLIst_WhenASingleGroupHasMoreOrEqualToMinElectableValidatorsAsMembersAndReceivedVotes() - public - { - WhenThereAreSomeGroups(); - vm.prank(voter1); - election.vote(group1, voter1Weight, group2, address(0)); - setRandomness(); - arraysEqual(election.electValidatorSigners(), group1Members); - } - - function test_ShouldReturnMaxElectableValidatorsElectedValidators_WhenGroupWithMoreThenMaxElectableValidatorsMembersReceivesVotes() - public - { - WhenThereAreSomeGroups(); - vm.prank(voter1); - election.vote(group1, voter1Weight, group2, address(0)); - vm.prank(voter2); - election.vote(group2, voter2Weight, address(0), group1); - vm.prank(voter3); - election.vote(group3, voter3Weight, address(0), group2); - - setRandomness(); - address[] memory expected = new address[](6); - expected[0] = validator1; - expected[1] = validator2; - expected[2] = validator3; - expected[3] = validator5; - expected[4] = validator6; - expected[5] = validator7; - arraysEqual(election.electValidatorSigners(), expected); + // Helper function to sort an array of uint256 + function sort(uint256[] memory data) internal pure returns (uint256[] memory) { + uint256 length = data.length; + for (uint256 i = 0; i < length; i++) { + for (uint256 j = i + 1; j < length; j++) { + if (data[i] > data[j]) { + uint256 temp = data[i]; + data[i] = data[j]; + data[j] = temp; + } + } + } + return data; + } + + function sortMembersWithVotesDesc( + MemberWithVotes[] memory data + ) internal pure returns (MemberWithVotes[] memory) { + uint256 length = data.length; + for (uint256 i = 0; i < length; i++) { + for (uint256 j = i + 1; j < length; j++) { + if (data[i].votes < data[j].votes) { + MemberWithVotes memory temp = data[i]; + data[i] = data[j]; + data[j] = temp; + } + } + } + return data; + } +} + +contract ElectionTest_ElectValidatorSigners is ElectionTest_ElectValidatorsAbstract { + function test_ShouldElectCorrectValidators_WhenThereIsALargeNumberOfGroupsSig() public { + WhenThereIsALargeNumberOfGroups(); + address[] memory elected = election.electValidatorSigners(); + + MemberWithVotes[] memory sortedMembersWithVotes = sortMembersWithVotesDesc(membersWithVotes); + + MemberWithVotes[] memory electedUnsorted = new MemberWithVotes[](100); + + for (uint256 i = 0; i < 100; i++) { + electedUnsorted[i] = MemberWithVotes(elected[i], votesConsideredForElection[elected[i]]); + } + + MemberWithVotes[] memory electedSorted = sortMembersWithVotesDesc(electedUnsorted); + + for (uint256 i = 0; i < 100; i++) { + assertEq(electedSorted[i].member, sortedMembersWithVotes[i].member); + assertEq(electedSorted[i].votes, sortedMembersWithVotes[i].votes); + } + } + + function test_ShouldReturnThatGroupsMemberLIst_WhenASingleGroupHasMoreOrEqualToMinElectableValidatorsAsMembersAndReceivedVotes() + public + { + WhenThereAreSomeGroups(); + vm.prank(voter1); + election.vote(group1, voter1Weight, group2, address(0)); + setRandomness(); + arraysEqual(election.electValidatorSigners(), group1Members); + } + + function test_ShouldReturnMaxElectableValidatorsElectedValidators_WhenGroupWithMoreThenMaxElectableValidatorsMembersReceivesVotes() + public + { + WhenThereAreSomeGroups(); + vm.prank(voter1); + election.vote(group1, voter1Weight, group2, address(0)); + vm.prank(voter2); + election.vote(group2, voter2Weight, address(0), group1); + vm.prank(voter3); + election.vote(group3, voter3Weight, address(0), group2); + + setRandomness(); + address[] memory expected = new address[](6); + expected[0] = validator1; + expected[1] = validator2; + expected[2] = validator3; + expected[3] = validator5; + expected[4] = validator6; + expected[5] = validator7; + arraysEqual(election.electValidatorSigners(), expected); } function test_ShouldElectOnlyNMembersFromThatGroup_WhenAGroupReceivesEnoughVotesForMoreThanNSeatsButOnlyHasNMembers() @@ -1793,6 +1816,127 @@ contract ElectionTest_ElectionValidatorSigners is ElectionTest { } } +contract ElectionTest_ElectValidatorSignersL2 is + ElectionTest_ElectValidatorSigners, + ElectionTest_L2 +{} + +contract ElectionTest_ElectValidatorsAccounts is ElectionTest_ElectValidatorsAbstract { + function test_ShouldElectCorrectValidators_WhenThereIsALargeNumberOfGroups() public { + WhenThereIsALargeNumberOfGroups(); + address[] memory elected = election.electValidatorAccounts(); + MemberWithVotes[] memory sortedMembersWithVotes = sortMembersWithVotesDesc(membersWithVotes); + MemberWithVotes[] memory electedUnsorted = new MemberWithVotes[](100); + + for (uint256 i = 0; i < 100; i++) { + electedUnsorted[i] = MemberWithVotes(elected[i], votesConsideredForElection[elected[i]]); + } + MemberWithVotes[] memory electedSorted = sortMembersWithVotesDesc(electedUnsorted); + + for (uint256 i = 0; i < 100; i++) { + assertEq(electedSorted[i].member, sortedMembersWithVotes[i].member); + assertEq(electedSorted[i].votes, sortedMembersWithVotes[i].votes); + } + } + + function test_ShouldReturnThatGroupsMemberLIst_WhenASingleGroupHasMoreOrEqualToMinElectableValidatorsAsMembersAndReceivedVotes() + public + { + WhenThereAreSomeGroups(); + vm.prank(voter1); + election.vote(group1, voter1Weight, group2, address(0)); + setRandomness(); + arraysEqual(election.electValidatorAccounts(), group1Members); + } + + function test_ShouldReturnMaxElectableValidatorsElectedValidators_WhenGroupWithMoreThenMaxElectableValidatorsMembersReceivesVotes() + public + { + WhenThereAreSomeGroups(); + vm.prank(voter1); + election.vote(group1, voter1Weight, group2, address(0)); + vm.prank(voter2); + election.vote(group2, voter2Weight, address(0), group1); + vm.prank(voter3); + election.vote(group3, voter3Weight, address(0), group2); + + setRandomness(); + address[] memory expected = new address[](6); + expected[0] = validator1; + expected[1] = validator2; + expected[2] = validator3; + expected[3] = validator5; + expected[4] = validator6; + expected[5] = validator7; + arraysEqual(election.electValidatorAccounts(), expected); + } + + function test_ShouldElectOnlyNMembersFromThatGroup_WhenAGroupReceivesEnoughVotesForMoreThanNSeatsButOnlyHasNMembers() + public + { + WhenThereAreSomeGroups(); + uint256 increment = 80; + uint256 votes = 80; + lockedGold.incrementNonvotingAccountBalance(address(voter3), increment); + lockedGold.setTotalLockedGold(totalLockedGold + increment); + vm.prank(voter3); + election.vote(group3, votes, group2, address(0)); + vm.prank(voter1); + election.vote(group1, voter1Weight, address(0), group3); + vm.prank(voter2); + election.vote(group2, voter2Weight, address(0), group1); + setRandomness(); + + address[] memory expected = new address[](6); + expected[0] = validator1; + expected[1] = validator2; + expected[2] = validator3; + expected[3] = validator5; + expected[4] = validator6; + expected[5] = validator7; + arraysEqual(election.electValidatorAccounts(), expected); + } + + function test_ShouldNotElectAnyMembersFromThatGroup_WhenAGroupDoesNotReceiveElectabilityThresholdVotes() + public + { + WhenThereAreSomeGroups(); + uint256 thresholdExcludingGroup3 = (voter3Weight + 1) / totalLockedGold; + election.setElectabilityThreshold(thresholdExcludingGroup3); + vm.prank(voter1); + election.vote(group1, voter1Weight, group2, address(0)); + vm.prank(voter2); + election.vote(group2, voter2Weight, address(0), group1); + vm.prank(voter3); + election.vote(group3, voter3Weight, address(0), group2); + + address[] memory expected = new address[](6); + expected[0] = validator1; + expected[1] = validator2; + expected[2] = validator3; + expected[3] = validator4; + expected[4] = validator5; + expected[5] = validator6; + arraysEqual(election.electValidatorAccounts(), expected); + } + + function test_ShouldRevert_WhenThereAnoNotEnoughElectableValidators() public { + WhenThereAreSomeGroups(); + vm.prank(voter2); + election.vote(group2, voter2Weight, group1, address(0)); + vm.prank(voter3); + election.vote(group3, voter3Weight, address(0), group2); + setRandomness(); + vm.expectRevert("Not enough elected validators"); + election.electValidatorAccounts(); + } +} + +contract ElectionTest_ElectValidatorsAccountsL2 is + ElectionTest_ElectValidatorsAccounts, + ElectionTest_L2 +{} + contract ElectionTest_GetGroupEpochRewards is ElectionTest { address voter = address(this); address group1 = account2; @@ -1801,11 +1945,18 @@ contract ElectionTest_GetGroupEpochRewards is ElectionTest { uint256 voteValue2 = 1000000000; uint256 totalRewardValue = 3000000000; + uint256 expectedGroup1EpochRewards = + FixidityLib + .newFixedFraction(voteValue1, voteValue1 + voteValue2) + .multiply(FixidityLib.newFixed(totalRewardValue)) + .fromFixed(); + function setUp() public { super.setUp(); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group1, address(0), address(0)); + vm.prank(address(validators)); election.markGroupEligible(group2, address(0), group1); registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(voteValue1 + voteValue2); @@ -1825,7 +1976,7 @@ contract ElectionTest_GetGroupEpochRewards is ElectionTest { } function WhenOneGroupHasActiveVotes() public { - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group1); } @@ -1864,7 +2015,7 @@ contract ElectionTest_GetGroupEpochRewards is ElectionTest { } function WhenTwoGroupsHaveActiveVotes() public { - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group1); election.activate(group2); } @@ -1880,12 +2031,6 @@ contract ElectionTest_GetGroupEpochRewards is ElectionTest { assertEq(election.getGroupEpochRewards(group2, totalRewardValue, uptimes), 0); } - uint256 expectedGroup1EpochRewards = - FixidityLib - .newFixedFraction(voteValue1, voteValue1 + voteValue2) - .multiply(FixidityLib.newFixed(totalRewardValue)) - .fromFixed(); - function test_ShouldReturnProportionalRewardValueForOtherGroup_WhenOneGroupDoesNotMeetLockedGoldRequirements_WhenTwoGroupsHaveActiveVotes() public { @@ -1908,6 +2053,14 @@ contract ElectionTest_GetGroupEpochRewards is ElectionTest { uptimes[0] = FIXED1; assertEq(election.getGroupEpochRewards(group1, totalRewardValue, uptimes), 0); } + + function test_Reverts_WhenL2() public { + _whenL2(); + uint256[] memory uptimes = new uint256[](1); + uptimes[0] = FIXED1; + vm.expectRevert("This method is no longer supported in L2."); + election.getGroupEpochRewards(group1, totalRewardValue, uptimes); + } } contract ElectionTest_DistributeEpochRewards is ElectionTest { @@ -1920,12 +2073,18 @@ contract ElectionTest_DistributeEpochRewards is ElectionTest { uint256 rewardValue = 1000000; uint256 rewardValue2 = 10000000; + uint256 expectedGroupTotalActiveVotes = voteValue + voteValue2 / 2 + rewardValue; + uint256 expectedVoterActiveVotesForGroup = + FixidityLib.newFixedFraction(expectedGroupTotalActiveVotes * 2, 3).fromFixed(); + uint256 expectedVoter2ActiveVotesForGroup = + FixidityLib.newFixedFraction(expectedGroupTotalActiveVotes, 3).fromFixed(); + uint256 expectedVoter2ActiveVotesForGroup2 = voteValue / 2 + rewardValue2; + function setUp() public { super.setUp(); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(voteValue); address[] memory membersGroup = new address[](1); @@ -1937,7 +2096,7 @@ contract ElectionTest_DistributeEpochRewards is ElectionTest { lockedGold.incrementNonvotingAccountBalance(voter, voteValue); election.vote(group, voteValue, address(0), address(0)); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group); } @@ -1948,13 +2107,6 @@ contract ElectionTest_DistributeEpochRewards is ElectionTest { assertEq(election.getActiveVotesForGroupByAccount(group, voter), voteValue + rewardValue); } - function test_Revert_DistributeEpochRewards_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - vm.prank(address(0)); - election.distributeEpochRewards(group, rewardValue, address(0), address(0)); - } - function test_ShouldIncrementAccountTotalVotesForGroup_WhenThereIsSingleGroupWithActiveVotes() public { @@ -1977,17 +2129,9 @@ contract ElectionTest_DistributeEpochRewards is ElectionTest { assertEq(election.getTotalVotes(), voteValue + rewardValue); } - uint256 expectedGroupTotalActiveVotes = voteValue + voteValue2 / 2 + rewardValue; - uint256 expectedVoterActiveVotesForGroup = - FixidityLib.newFixedFraction(expectedGroupTotalActiveVotes * 2, 3).fromFixed(); - uint256 expectedVoter2ActiveVotesForGroup = - FixidityLib.newFixedFraction(expectedGroupTotalActiveVotes, 3).fromFixed(); - uint256 expectedVoter2ActiveVotesForGroup2 = voteValue / 2 + rewardValue2; - function WhenThereAreTwoGroupsWithActiveVotes() public { - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group2, address(0), group); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(voteValue + voteValue2); validators.setNumRegisteredValidators(2); @@ -1997,7 +2141,7 @@ contract ElectionTest_DistributeEpochRewards is ElectionTest { // Split voter2's vote between the two groups. election.vote(group, voteValue2 / 2, group2, address(0)); election.vote(group2, voteValue2 / 2, address(0), group); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group); election.activate(group2); vm.stopPrank(); @@ -2077,6 +2221,11 @@ contract ElectionTest_DistributeEpochRewards is ElectionTest { } } +contract ElectionTest_DistributeEpochRewards_L2 is + ElectionTest_L2, + ElectionTest_DistributeEpochRewards +{} + contract ElectionTest_ForceDecrementVotes is ElectionTest { address voter = address(this); address group = account2; @@ -2087,9 +2236,14 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { uint256 slashedValue = value; uint256 remaining = value - slashedValue; - function setUp() public { - super.setUp(); - } + uint256 totalRemaining; + uint256 group1Remaining; + uint256 group2TotalRemaining; + uint256 group2PendingRemaining; + uint256 group2ActiveRemaining; + + uint256 group1RemainingActiveVotes; + address[] initialOrdering; function WhenAccountHasVotedForOneGroup() public { address[] memory membersGroup = new address[](1); @@ -2097,9 +2251,8 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { validators.setMembers(group, membersGroup); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(value); validators.setNumRegisteredValidators(1); lockedGold.incrementNonvotingAccountBalance(voter, value); @@ -2121,6 +2274,21 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { election.forceDecrementVotes(voter, slashedValue, lessers, greaters, indices); } + function test_Reverts_WhenBlocked() public { + WhenAccountHasVotedForOneGroup(); + address[] memory lessers = new address[](1); + lessers[0] = address(0); + address[] memory greaters = new address[](1); + greaters[0] = address(0); + uint256[] memory indices = new uint256[](1); + indices[0] = index; + + blocker.mockSetBlocked(true); + vm.prank(account2); + vm.expectRevert("Contract is blocked from performing this action"); + election.forceDecrementVotes(voter, slashedValue, lessers, greaters, indices); + } + function test_ShouldDecrementPendingVotesToZero_WhenAccountHasOnlyPendingVotes() public { WhenAccountHasOnlyPendingVotes(); assertEq(election.getPendingVotesForGroupByAccount(group, voter), remaining); @@ -2141,7 +2309,7 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { function WhenAccountHasOnlyActiveVotes() public { WhenAccountHasVotedForOneGroup(); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group); vm.prank(account2); @@ -2182,10 +2350,11 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { membersGroup2[0] = account9; validators.setMembers(group2, membersGroup2); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); + vm.prank(address(validators)); election.markGroupEligible(group2, group, address(0)); - registry.setAddressFor("Validators", address(validators)); + lockedGold.setTotalLockedGold(value); validators.setNumRegisteredValidators(2); lockedGold.incrementNonvotingAccountBalance(voter, value); @@ -2255,10 +2424,11 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { membersGroup2[0] = account9; validators.setMembers(group2, membersGroup2); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); + vm.prank(address(validators)); election.markGroupEligible(group2, group, address(0)); - registry.setAddressFor("Validators", address(validators)); + lockedGold.setTotalLockedGold(value + value2); validators.setNumRegisteredValidators(2); lockedGold.incrementNonvotingAccountBalance(voter, value + value2); @@ -2270,9 +2440,9 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { public { WhenAccountHasVotedForMoreThanOneGroupInequally(); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group2); election.vote(group2, value2 / 2, group, address(0)); @@ -2331,12 +2501,6 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { assertEq(election.getActiveVotesForGroupByAccount(group, voter), remaining); } - uint256 totalRemaining; - uint256 group1Remaining; - uint256 group2TotalRemaining; - uint256 group2PendingRemaining; - uint256 group2ActiveRemaining; - function WhenWeSlashAllOfGroup1VotesAndSomeOfGroup2__WhenWeSlash1MoreVoteThanGroup1PendingVoteTotal_WhenAccountHasVotedForMoreThanOneGroupInequally() public { @@ -2373,19 +2537,16 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { assertEq(election.getActiveVotesForGroupByAccount(group2, voter), group2ActiveRemaining); } - uint256 group1RemainingActiveVotes; - address[] initialOrdering; - function WhenSlashAffectsElectionOrder() public { WhenAccountHasVotedForMoreThanOneGroupInequally(); - slashedValue = value / 4; + slashedValue = value / 4 + 1; group1RemainingActiveVotes = value - slashedValue; election.vote(group, value / 2, group2, address(0)); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group); - blockTravel(ph.epochSize() + 1); + travelNEpoch(1); election.activate(group2); (initialOrdering, ) = election.getTotalVotesForEligibleValidatorGroups(); @@ -2490,13 +2651,9 @@ contract ElectionTest_ForceDecrementVotes is ElectionTest { } } -contract ElectionTest_ConsistencyChecks is ElectionTest { - address voter = address(this); - address group = account2; - uint256 rewardValue2 = 10000000; - - AccountStruct[] _accounts; +contract ElectionTest_ForceDecrementVotes_L2 is ElectionTest_L2, ElectionTest_ForceDecrementVotes {} +contract ElectionTest_ConsistencyChecks is ElectionTest { struct AccountStruct { address account; uint256 active; @@ -2511,6 +2668,12 @@ contract ElectionTest_ConsistencyChecks is ElectionTest { RevokeActive } + address voter = address(this); + address group = account2; + uint256 rewardValue2 = 10000000; + + AccountStruct[] _accounts; + function setUp() public { super.setUp(); @@ -2519,9 +2682,8 @@ contract ElectionTest_ConsistencyChecks is ElectionTest { address[] memory members = new address[](1); members[0] = account9; validators.setMembers(group, members); - registry.setAddressFor("Validators", address(this)); + vm.prank(address(validators)); election.markGroupEligible(group, address(0), address(0)); - registry.setAddressFor("Validators", address(validators)); lockedGold.setTotalLockedGold(voterStartBalance * accountsArray.length); validators.setNumRegisteredValidators(1); for (uint256 i = 0; i < accountsArray.length; i++) { @@ -2538,52 +2700,6 @@ contract ElectionTest_ConsistencyChecks is ElectionTest { } } - function makeRandomAction(AccountStruct storage account, uint256 salt) internal { - VoteActionType[] memory actions = new VoteActionType[](4); - uint256 actionCount = 0; - - if (account.nonVoting > 0) { - actions[actionCount++] = VoteActionType.Vote; - } - if (election.hasActivatablePendingVotes(account.account, group)) { - // Assuming this is a view function - actions[actionCount++] = VoteActionType.Activate; - } - if (account.pending > 0) { - actions[actionCount++] = VoteActionType.RevokePending; - } - if (account.active > 0) { - actions[actionCount++] = VoteActionType.RevokeActive; - } - - VoteActionType action = actions[generatePRN(0, actionCount - 1, uint256(account.account))]; - uint256 value; - - vm.startPrank(account.account); - if (action == VoteActionType.Vote) { - value = generatePRN(0, account.nonVoting, uint256(account.account) + salt); - election.vote(group, value, address(0), address(0)); - account.nonVoting -= value; - account.pending += value; - } else if (action == VoteActionType.Activate) { - value = account.pending; - election.activate(group); - account.pending -= value; - account.active += value; - } else if (action == VoteActionType.RevokePending) { - value = generatePRN(0, account.pending, uint256(account.account) + salt); - election.revokePending(group, value, address(0), address(0), 0); - account.pending -= value; - account.nonVoting += value; - } else if (action == VoteActionType.RevokeActive) { - value = generatePRN(0, account.active, uint256(account.account) + salt); - election.revokeActive(group, value, address(0), address(0), 0); - account.active -= value; - account.nonVoting += value; - } - vm.stopPrank(); - } - function checkVoterInvariants(AccountStruct memory account, uint256 delta) public { assertAlmostEqual( election.getPendingVotesForGroupByAccount(group, account.account), @@ -2663,7 +2779,12 @@ contract ElectionTest_ConsistencyChecks is ElectionTest { makeRandomAction(_accounts[j], j); checkVoterInvariants(_accounts[j], 0); checkGroupInvariants(0); - vm.roll((i + 1) * ph.epochSize() + (i + 1)); + + if (isL2()) { + epochManager.setCurrentEpochNumber(i + 1); + } else { + vm.roll((i + 1) * ph.epochSize() + (i + 1)); + } } } revokeAllAndCheckInvariants(0); @@ -2699,7 +2820,11 @@ contract ElectionTest_ConsistencyChecks is ElectionTest { } distributeEpochRewards(i); - vm.roll((i + 1) * ph.epochSize() + (i + 1)); + if (isL2()) { + epochManager.setCurrentEpochNumber(i + 1); + } else { + vm.roll((i + 1) * ph.epochSize() + (i + 1)); + } for (uint256 j = 0; j < _accounts.length; j++) { checkVoterInvariants(_accounts[j], 100); @@ -2708,13 +2833,85 @@ contract ElectionTest_ConsistencyChecks is ElectionTest { } revokeAllAndCheckInvariants(100); } + + function makeRandomAction(AccountStruct storage account, uint256 salt) internal { + VoteActionType[] memory actions = new VoteActionType[](4); + uint256 actionCount = 0; + + if (account.nonVoting > 0) { + actions[actionCount++] = VoteActionType.Vote; + } + if (election.hasActivatablePendingVotes(account.account, group)) { + // Assuming this is a view function + actions[actionCount++] = VoteActionType.Activate; + } + if (account.pending > 0) { + actions[actionCount++] = VoteActionType.RevokePending; + } + if (account.active > 0) { + actions[actionCount++] = VoteActionType.RevokeActive; + } + + VoteActionType action = actions[generatePRN(0, actionCount - 1, uint256(account.account))]; + uint256 value; + + vm.startPrank(account.account); + if (action == VoteActionType.Vote) { + value = generatePRN(0, account.nonVoting, uint256(account.account) + salt); + election.vote(group, value, address(0), address(0)); + account.nonVoting -= value; + account.pending += value; + } else if (action == VoteActionType.Activate) { + value = account.pending; + election.activate(group); + account.pending -= value; + account.active += value; + } else if (action == VoteActionType.RevokePending) { + value = generatePRN(0, account.pending, uint256(account.account) + salt); + election.revokePending(group, value, address(0), address(0), 0); + account.pending -= value; + account.nonVoting += value; + } else if (action == VoteActionType.RevokeActive) { + value = generatePRN(0, account.active, uint256(account.account) + salt); + election.revokeActive(group, value, address(0), address(0), 0); + account.active -= value; + account.nonVoting += value; + } + vm.stopPrank(); + } } +contract ElectionTest_ConsistencyChecks_L2 is ElectionTest_L2, ElectionTest_ConsistencyChecks {} + contract ElectionTest_HasActivatablePendingVotes is ElectionTest { - function test_Revert_hasActivatablePendingVotes_WhenL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); - vm.prank(address(0)); - election.hasActivatablePendingVotes(address(0), address(0)); + address voter = address(this); + address group = account1; + uint256 value = 1000; + + function setUp() public { + super.setUp(); + address[] memory members = new address[](1); + members[0] = account9; + validators.setMembers(group, members); + + vm.prank(address(validators)); + election.markGroupEligible(group, address(0), address(0)); + + lockedGold.setTotalLockedGold(value); + validators.setMembers(group, members); + validators.setNumRegisteredValidators(1); + + lockedGold.incrementNonvotingAccountBalance(voter, value); + election.vote(group, value, address(0), address(0)); + travelNEpoch(1); + } + + function test_ReturnsTrue_WhenUserHasVoted() public { + assertTrue(election.hasActivatablePendingVotes(voter, group)); } } + +contract ElectionTest_HasActivatablePendingVotes_L2 is + ElectionTest_L2, + ElectionTest_HasActivatablePendingVotes +{} diff --git a/packages/protocol/test-sol/governance/voting/LockedGold.t.sol b/packages/protocol/test-sol/unit/governance/voting/LockedGold.t.sol similarity index 94% rename from packages/protocol/test-sol/governance/voting/LockedGold.t.sol rename to packages/protocol/test-sol/unit/governance/voting/LockedGold.t.sol index 6c6d27525f9..d0a6cb8f57d 100644 --- a/packages/protocol/test-sol/governance/voting/LockedGold.t.sol +++ b/packages/protocol/test-sol/unit/governance/voting/LockedGold.t.sol @@ -2,12 +2,12 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; -import "celo-foundry/Test.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; +import "@test-sol/utils/WhenL2.sol"; import "@celo-contracts/common/FixidityLib.sol"; -import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Accounts.sol"; -import "@test-sol/common/GoldTokenMock.sol"; +import "@test-sol/unit/common/CeloTokenMock.sol"; import "@celo-contracts/governance/LockedGold.sol"; import "@celo-contracts/governance/ReleaseGold.sol"; import "@celo-contracts/governance/Election.sol"; @@ -16,12 +16,13 @@ import "@celo-contracts/governance/test/MockElection.sol"; import "@celo-contracts/governance/test/MockGovernance.sol"; import "@celo-contracts/governance/test/MockValidators.sol"; -contract LockedGoldTest is Test { +import { TestBlocker } from "@test-sol/unit/common/Blockable.t.sol"; + +contract LockedGoldTest is TestWithUtils { using FixidityLib for FixidityLib.Fraction; - Registry registry; Accounts accounts; - GoldToken goldToken; + GoldToken celoToken; MockStableToken stableToken; MockElection election; MockGovernance governance; @@ -35,6 +36,24 @@ contract LockedGoldTest is Test { address randomAddress = actor("randomAddress"); address caller = address(this); + TestBlocker blocker; + + address delegatee1 = actor("delegatee1"); + address delegatee2 = actor("delegatee2"); + address delegatee3 = actor("delegatee3"); + address delegator = actor("delegator"); + address delegator2 = actor("delegator2"); + address reporter = actor("reporter"); + string slasherName = "DowntimeSlasher"; + address downtimeSlasher = actor(slasherName); + address delegatorSigner; + uint256 delegatorSignerPK; + address delegatorSigner2; + uint256 delegatorSigner2PK; + address delegateeSigner1; + uint256 delegateeSigner1PK; + address delegateeSigner2; + uint256 delegateeSigner2PK; event UnlockingPeriodSet(uint256 period); event GoldLocked(address indexed account, uint256 value); @@ -64,11 +83,9 @@ contract LockedGoldTest is Test { event MaxDelegateesCountSet(uint256 value); function setUp() public { - address registryAddress = 0x000000000000000000000000000000000000ce10; - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); - registry = Registry(registryAddress); + super.setUp(); - goldToken = new GoldTokenMock(); + celoToken = new CeloTokenMock(); accounts = new Accounts(true); lockedGold = new LockedGold(true); election = new MockElection(); @@ -78,13 +95,23 @@ contract LockedGoldTest is Test { registry.setAddressFor("Accounts", address(accounts)); registry.setAddressFor("Election", address(election)); - registry.setAddressFor("GoldToken", address(goldToken)); registry.setAddressFor("Governance", address(governance)); registry.setAddressFor("LockedGold", address(lockedGold)); registry.setAddressFor("Validators", address(validators)); registry.setAddressFor("StableToken", address(stableToken)); lockedGold.initialize(address(registry), unlockingPeriod); accounts.createAccount(); + + blocker = new TestBlocker(); + + lockedGold.setBlockedByContract(address(blocker)); + + (delegatorSigner, delegatorSignerPK) = actorWithPK("delegatorSigner"); + (delegatorSigner2, delegatorSigner2PK) = actorWithPK("delegatorSigner2"); + (delegateeSigner1, delegateeSigner1PK) = actorWithPK("delegateeSigner1"); + (delegateeSigner2, delegateeSigner2PK) = actorWithPK("delegateeSigner2"); + vm.deal(delegator, 10 ether); + vm.deal(delegator2, 10 ether); } function getParsedSignatureOfAddress( @@ -208,8 +235,43 @@ contract LockedGoldTest is Test { vm.prank(celoOwner); lockedGold.lock.value(value)(); } + + function whenVoteSigner_LockedGoldDelegateGovernanceVotes() public { + helper_WhenVoteSigners( + WhenVoteSignerStruct( + delegator, + delegator2, + delegatee1, + delegatee2, + delegatorSignerPK, + delegateeSigner1PK, + delegatorSigner2PK, + delegateeSigner2PK, + true + ) + ); + } + + function helper_WhenAccountIsSlashedForAllOfItsLockedGold( + uint256 penalty, + uint256 reward, + address accountToSlash + ) public { + address[] memory lessers = new address[](1); + lessers[0] = address(0); + address[] memory greaters = new address[](1); + greaters[0] = address(0); + + uint256[] memory indices = new uint256[](1); + indices[0] = 0; + + vm.prank(downtimeSlasher); + lockedGold.slash(accountToSlash, penalty, reporter, reward, lessers, greaters, indices); + } } +contract LockedGoldTest_L2 is WhenL2, LockedGoldTest {} + contract LockedGoldTest_initialize is LockedGoldTest { function setUp() public { super.setUp(); @@ -251,6 +313,8 @@ contract LockedGoldTest_setRegistry is LockedGoldTest { } } +contract LockedGoldTest_setRegistry_L2 is LockedGoldTest_L2, LockedGoldTest_setRegistry {} + contract LockedGoldTest_setUnlockingPeriod is LockedGoldTest { function setUp() public { super.setUp(); @@ -281,6 +345,11 @@ contract LockedGoldTest_setUnlockingPeriod is LockedGoldTest { } } +contract LockedGoldTest_setUnlockingPeriod_L2 is + LockedGoldTest_L2, + LockedGoldTest_setUnlockingPeriod +{} + contract LockedGoldTest_lock is LockedGoldTest { uint256 value = 1000; function setUp() public { @@ -326,6 +395,8 @@ contract LockedGoldTest_lock is LockedGoldTest { } } +contract LockedGoldTest_lock_L2 is LockedGoldTest_L2, LockedGoldTest_lock {} + contract LockedGoldTest_unlock is LockedGoldTest { uint256 value = 1000; uint256 availabilityTime = unlockingPeriod + block.timestamp; @@ -508,6 +579,8 @@ contract LockedGoldTest_unlock is LockedGoldTest { } } +contract LockedGoldTest_unlock_L2 is LockedGoldTest_L2, LockedGoldTest_unlock {} + contract LockedGoldTest_unlockDelegation is LockedGoldTest { uint256 value = 1000; uint256 availabilityTime = unlockingPeriod + block.timestamp; @@ -583,6 +656,8 @@ contract LockedGoldTest_unlockDelegation is LockedGoldTest { } } +contract LockedGoldTest_unlockDelegation_L2 is LockedGoldTest_L2, LockedGoldTest_unlockDelegation {} + contract LockedGoldTest_unlock_WhenDelegation2Delegatees is LockedGoldTest { uint256 value = 1000; uint256 availabilityTime = unlockingPeriod + block.timestamp; @@ -648,6 +723,11 @@ contract LockedGoldTest_unlock_WhenDelegation2Delegatees is LockedGoldTest { } } +contract LockedGoldTest_unlock_WhenDelegation2Delegatees_L2 is + LockedGoldTest_L2, + LockedGoldTest_unlock_WhenDelegation2Delegatees +{} + contract LockedGoldTest_unlock_WhenDelegatingTo3Delegatees is LockedGoldTest { uint256 value = 5; uint256 availabilityTime = unlockingPeriod + block.timestamp; @@ -781,6 +861,11 @@ contract LockedGoldTest_unlock_WhenDelegatingTo3Delegatees is LockedGoldTest { } } +contract LockedGoldTest_unlock_WhenDelegatingTo3Delegatees_L2 is + LockedGoldTest_L2, + LockedGoldTest_unlock_WhenDelegatingTo3Delegatees +{} + contract LockedGoldTest_lock_AfterUnlocking is LockedGoldTest { uint256 pendingWithdrawalValue = 100; uint256 index = 0; @@ -944,6 +1029,11 @@ contract LockedGoldTest_lock_AfterUnlocking is LockedGoldTest { } } +contract LockedGoldTest_lock_AfterUnlocking_L2 is + LockedGoldTest_L2, + LockedGoldTest_lock_AfterUnlocking +{} + contract LockedGoldTest_withdraw is LockedGoldTest { uint256 value = 1000; uint256 index = 0; @@ -984,6 +1074,8 @@ contract LockedGoldTest_withdraw is LockedGoldTest { function() external payable {} } +contract LockedGoldTest_withdraw_L2 is LockedGoldTest_L2, LockedGoldTest_withdraw {} + contract LockedGoldTest_addSlasher is LockedGoldTest { string slasherName = "DowntimeSlasher"; address downtimeSlasher = actor(slasherName); @@ -1012,6 +1104,8 @@ contract LockedGoldTest_addSlasher is LockedGoldTest { } } +contract LockedGoldTest_addSlasher_L2 is LockedGoldTest_L2, LockedGoldTest_addSlasher {} + contract LockedGoldTest_removeSlasher is LockedGoldTest { string slasherName = "DowntimeSlasher"; string governanceSlasherName = "GovernanceSlasher"; @@ -1054,13 +1148,12 @@ contract LockedGoldTest_removeSlasher is LockedGoldTest { } } +contract LockedGoldTest_removeSlasher_L2 is LockedGoldTest_L2, LockedGoldTest_removeSlasher {} + contract LockedGoldTest_slash is LockedGoldTest { - string slasherName = "DowntimeSlasher"; uint256 value = 1000; address group = actor("group"); address groupMember = actor("groupMember"); - address reporter = actor("reporter"); - address downtimeSlasher = actor(slasherName); address delegatee = actor("delegatee"); Election electionSlashTest; @@ -1092,22 +1185,25 @@ contract LockedGoldTest_slash is LockedGoldTest { vm.prank(reporter); accounts.createAccount(); - } - - function helper_WhenAccountIsSlashedForAllOfItsLockedGold( - uint256 penalty, - uint256 reward - ) public { - address[] memory lessers = new address[](1); - lessers[0] = address(0); - address[] memory greaters = new address[](1); - greaters[0] = address(0); - uint256[] memory indices = new uint256[](1); - indices[0] = 0; + vm.prank(delegatee1); + accounts.createAccount(); + vm.prank(delegatee2); + accounts.createAccount(); + vm.prank(delegatee3); + accounts.createAccount(); + vm.prank(delegator); + accounts.createAccount(); + vm.prank(delegator2); + accounts.createAccount(); + } - vm.prank(downtimeSlasher); - lockedGold.slash(caller, penalty, reporter, reward, lessers, greaters, indices); + function test_Reverts_WhenBlocked() public { + uint256 penalty = value; + uint256 reward = value / 2; + blocker.mockSetBlocked(true); + vm.expectRevert("Contract is blocked from performing this action"); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); } function test_ShouldReduceAccountsLockedGoldBalance_WhenAccountIsSlashedForAllOfItsLockedGold() @@ -1115,7 +1211,7 @@ contract LockedGoldTest_slash is LockedGoldTest { { uint256 penalty = value; uint256 reward = value / 2; - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(lockedGold.getAccountNonvotingLockedGold(caller), value - penalty); assertEq(lockedGold.getAccountTotalLockedGold(caller), value - penalty); @@ -1126,7 +1222,7 @@ contract LockedGoldTest_slash is LockedGoldTest { { uint256 penalty = value; uint256 reward = value / 2; - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(lockedGold.getAccountNonvotingLockedGold(reporter), reward); assertEq(lockedGold.getAccountTotalLockedGold(reporter), reward); @@ -1137,7 +1233,7 @@ contract LockedGoldTest_slash is LockedGoldTest { { uint256 penalty = value; uint256 reward = value / 2; - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(address(governance).balance, penalty - reward); } @@ -1149,7 +1245,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 reward = value / 2; lockedGold.removeSlasher(slasherName, 0); vm.expectRevert("Caller is not a whitelisted slasher."); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); } function test_ShouldReduceAccountsNonVotingLockedGoldBalance_WhenAccountIsSlashedForOnlyItsNonvotingBalance_WhenTheAccountHasHalfVotingAndHalfNonVotingGold() @@ -1161,7 +1257,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(lockedGold.getAccountNonvotingLockedGold(caller), nonVoting - penalty); } @@ -1173,7 +1269,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 penalty = nonVoting; uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(lockedGold.getAccountTotalLockedGold(caller), value - penalty); assertEq(electionSlashTest.getTotalVotesByAccount(caller), voting); @@ -1187,7 +1283,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 penalty = nonVoting; uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(lockedGold.getAccountNonvotingLockedGold(reporter), reward); assertEq(lockedGold.getAccountTotalLockedGold(reporter), reward); @@ -1201,7 +1297,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 penalty = nonVoting; uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(address(governance).balance, penalty - reward); } @@ -1214,7 +1310,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(lockedGold.getAccountNonvotingLockedGold(caller), 0); } @@ -1225,7 +1321,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 penalty = value; uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(lockedGold.getAccountTotalLockedGold(caller), 0); assertEq(electionSlashTest.getTotalVotesByAccount(caller), 0); @@ -1238,7 +1334,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 penalty = value; uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(lockedGold.getAccountNonvotingLockedGold(reporter), reward); assertEq(lockedGold.getAccountTotalLockedGold(reporter), reward); @@ -1251,7 +1347,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 penalty = value; uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(address(governance).balance, penalty - reward); } @@ -1263,7 +1359,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 penalty = value * 2; uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(lockedGold.getAccountNonvotingLockedGold(caller), 0); assertEq(lockedGold.getAccountTotalLockedGold(caller), 0); @@ -1277,7 +1373,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 penalty = value * 2; uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(lockedGold.getAccountNonvotingLockedGold(reporter), reward); assertEq(lockedGold.getAccountTotalLockedGold(reporter), reward); @@ -1290,7 +1386,7 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 penalty = value * 2; uint256 reward = penalty / 2; electionSlashTest.vote(group, voting, address(0), address(0)); - helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, caller); assertEq(address(governance).balance, value - reward); } @@ -1319,28 +1415,32 @@ contract LockedGoldTest_slash is LockedGoldTest { uint256 reward = value / 2; - helper_WhenAccountIsSlashedForAllOfItsLockedGold(value, reward); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(value, reward, caller); assertEq(lockedGold.getAccountNonvotingLockedGold(reporter), reward); assertEq(lockedGold.getAccountTotalLockedGold(reporter), reward); } -} -contract LockedGoldTest_delegateGovernanceVotes is LockedGoldTest { - address delegatee1 = actor("delegatee1"); - address delegatee2 = actor("delegatee2"); - address delegatee3 = actor("delegatee3"); - address delegator = actor("delegator"); - address delegator2 = actor("delegator2"); + function test_ShouldReduceAccountsLockedGoldBalance_WhenAccountIsSlashedForAllOfItsLockedGoldAndIsDelegating() + public + { + uint256 penalty = value; + uint256 reward = value / 2; + whenVoteSigner_LockedGoldDelegateGovernanceVotes(); + vm.prank(delegator); + lockedGold.delegateGovernanceVotes(delegatee1, FixidityLib.newFixedFraction(30, 100).unwrap()); + assertEq(lockedGold.getAccountNonvotingLockedGold(delegator), 1000); + assertEq(lockedGold.getAccountTotalLockedGold(delegator), 1000); + assertEq(lockedGold.getAccountTotalGovernanceVotingPower(delegatee1), 300); + helper_WhenAccountIsSlashedForAllOfItsLockedGold(penalty, reward, delegator); + assertEq(lockedGold.getAccountNonvotingLockedGold(delegator), 0); + assertEq(lockedGold.getAccountTotalLockedGold(delegator), 0); + assertEq(lockedGold.getAccountTotalGovernanceVotingPower(delegatee1), 0); + } +} - address delegatorSigner; - uint256 delegatorSignerPK; - address delegatorSigner2; - uint256 delegatorSigner2PK; - address delegateeSigner1; - uint256 delegateeSigner1PK; - address delegateeSigner2; - uint256 delegateeSigner2PK; +contract LockedGoldTest_slash_L2 is LockedGoldTest_L2, LockedGoldTest_slash {} +contract LockedGoldTest_delegateGovernanceVotes is LockedGoldTest { uint256 value = 1000; uint256 percentToDelegate = 30; uint256 delegatedAmount = (value * percentToDelegate) / 100; @@ -1375,22 +1475,6 @@ contract LockedGoldTest_delegateGovernanceVotes is LockedGoldTest { vm.deal(delegator2, 10 ether); } - function whenVoteSigner_LockedGoldDelegateGovernanceVotes() public { - helper_WhenVoteSigners( - WhenVoteSignerStruct( - delegator, - delegator2, - delegatee1, - delegatee2, - delegatorSignerPK, - delegateeSigner1PK, - delegatorSigner2PK, - delegateeSigner2PK, - true - ) - ); - } - function test_ShouldRevertWhenDelegateeIsNotAccount() public { vm.expectRevert("Must first register address with Account.createAccount"); lockedGold.delegateGovernanceVotes(randomAddress, FixidityLib.newFixedFraction(1, 1).unwrap()); @@ -1739,22 +1823,12 @@ contract LockedGoldTest_delegateGovernanceVotes is LockedGoldTest { } } -contract LockedGoldTest_revokeDelegatedGovernanceVotes is LockedGoldTest { - address delegatee1 = actor("delegatee1"); - address delegatee2 = actor("delegatee2"); - address delegatee3 = actor("delegatee3"); - address delegator = actor("delegator"); - address delegator2 = actor("delegator2"); - - address delegatorSigner; - uint256 delegatorSignerPK; - address delegatorSigner2; - uint256 delegatorSigner2PK; - address delegateeSigner1; - uint256 delegateeSigner1PK; - address delegateeSigner2; - uint256 delegateeSigner2PK; +contract LockedGoldTest_delegateGovernanceVotes_L2 is + LockedGoldTest_L2, + LockedGoldTest_delegateGovernanceVotes +{} +contract LockedGoldTest_revokeDelegatedGovernanceVotes is LockedGoldTest { uint256 value = 1000; uint256 percentageToRevoke = 2; uint256 percentageToDelegate = 10; @@ -2108,6 +2182,11 @@ contract LockedGoldTest_revokeDelegatedGovernanceVotes is LockedGoldTest { } } +contract LockedGoldTest_revokeDelegatedGovernanceVotes_L2 is + LockedGoldTest_L2, + LockedGoldTest_revokeDelegatedGovernanceVotes +{} + contract LockedGoldTest_getAccountTotalGovernanceVotingPower is LockedGoldTest { address delegator = actor("delegator"); address delegatee = actor("delegatee"); @@ -2155,6 +2234,11 @@ contract LockedGoldTest_getAccountTotalGovernanceVotingPower is LockedGoldTest { } } +contract LockedGoldTest_getAccountTotalGovernanceVotingPower_L2 is + LockedGoldTest_L2, + LockedGoldTest_getAccountTotalGovernanceVotingPower +{} + contract LockedGoldTest_getDelegatorDelegateeInfo is LockedGoldTest { address delegator = actor("delegator"); address delegatee = actor("delegatee"); @@ -2197,6 +2281,11 @@ contract LockedGoldTest_getDelegatorDelegateeInfo is LockedGoldTest { } } +contract LockedGoldTest_getDelegatorDelegateeInfo_L2 is + LockedGoldTest_L2, + LockedGoldTest_getDelegatorDelegateeInfo +{} + contract LockedGoldTest_getDelegatorDelegateeExpectedAndRealAmount is LockedGoldTest { address delegator = actor("delegator"); address delegatee = actor("delegatee"); @@ -2313,6 +2402,11 @@ contract LockedGoldTest_getDelegatorDelegateeExpectedAndRealAmount is LockedGold } } +contract LockedGoldTest_getDelegatorDelegateeExpectedAndRealAmount_L2 is + LockedGoldTest_L2, + LockedGoldTest_getDelegatorDelegateeExpectedAndRealAmount +{} + contract LockedGoldTest_updateDelegatedAmount is LockedGoldTest { address delegator = actor("delegator"); address delegatee = actor("delegatee"); @@ -2390,6 +2484,11 @@ contract LockedGoldTest_updateDelegatedAmount is LockedGoldTest { } } +contract LockedGoldTest_updateDelegatedAmount_L2 is + LockedGoldTest_L2, + LockedGoldTest_updateDelegatedAmount +{} + contract LockedGoldTest_getTotalPendingWithdrawalsCount is LockedGoldTest { uint256 value = 1000; address account = actor("account"); @@ -2420,6 +2519,11 @@ contract LockedGoldTest_getTotalPendingWithdrawalsCount is LockedGoldTest { } } +contract LockedGoldTest_getTotalPendingWithdrawalsCount_L2 is + LockedGoldTest_L2, + LockedGoldTest_getTotalPendingWithdrawalsCount +{} + contract LockedGoldTestGetPendingWithdrawalsInBatch is LockedGoldTest { uint256 value = 1000; @@ -2502,3 +2606,8 @@ contract LockedGoldTestGetPendingWithdrawalsInBatch is LockedGoldTest { assertEq(timestamps.length, 0); } } + +contract LockedGoldTestGetPendingWithdrawalsInBatch_L2 is + LockedGoldTest_L2, + LockedGoldTestGetPendingWithdrawalsInBatch +{} diff --git a/packages/protocol/test-sol/governance/voting/ReleaseGold.t.sol b/packages/protocol/test-sol/unit/governance/voting/ReleaseGold.t.sol similarity index 90% rename from packages/protocol/test-sol/governance/voting/ReleaseGold.t.sol rename to packages/protocol/test-sol/unit/governance/voting/ReleaseGold.t.sol index bd76502b832..f29213431a5 100644 --- a/packages/protocol/test-sol/governance/voting/ReleaseGold.t.sol +++ b/packages/protocol/test-sol/unit/governance/voting/ReleaseGold.t.sol @@ -2,30 +2,30 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; -import "celo-foundry/Test.sol"; -import "../../../contracts/identity/Escrow.sol"; -import "../../../contracts/identity/FederatedAttestations.sol"; -import "../../../contracts/identity/test/MockAttestations.sol"; -import "../../../contracts/identity/test/MockERC20Token.sol"; -import "../../../contracts/common/FixidityLib.sol"; - -import "../../../contracts/common/Registry.sol"; -import "../../../contracts/common/Accounts.sol"; -import "../../../contracts/common/Freezer.sol"; -import "../../../contracts/common/GoldToken.sol"; -import "../../../contracts/governance/LockedGold.sol"; -import "../../../contracts/governance/ReleaseGold.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; +import "@test-sol/utils/WhenL2.sol"; +import { ECDSAHelper } from "@test-sol/utils/ECDSAHelper.sol"; + +import "@celo-contracts/identity/Escrow.sol"; +import "@celo-contracts/identity/FederatedAttestations.sol"; +import "@celo-contracts/identity/test/MockAttestations.sol"; +import "@celo-contracts/identity/test/MockERC20Token.sol"; +import "@celo-contracts/common/FixidityLib.sol"; + +import "@celo-contracts/common/Accounts.sol"; +import "@celo-contracts/common/Freezer.sol"; +import "@celo-contracts/common/GoldToken.sol"; +import "@celo-contracts/governance/LockedGold.sol"; +import "@celo-contracts/governance/ReleaseGold.sol"; import "./mocks/ReleaseGoldMockTunnel.sol"; -import "../../../contracts/stability/test/MockStableToken.sol"; -import "../../../contracts/governance/test/MockElection.sol"; -import "../../../contracts/governance/test/MockGovernance.sol"; -import "../../../contracts/governance/test/MockValidators.sol"; -import "@test-sol/utils/ECDSAHelper.sol"; +import "@celo-contracts/stability/test/MockStableToken.sol"; +import "@celo-contracts/governance/test/MockElection.sol"; +import "@celo-contracts/governance/test/MockGovernance.sol"; +import "@celo-contracts/governance/test/MockValidators.sol"; -contract ReleaseGoldTest is Test, ECDSAHelper { +contract ReleaseGoldTest is TestWithUtils, ECDSAHelper { using FixidityLib for FixidityLib.Fraction; - Registry registry; Accounts accounts; Freezer freezer; GoldToken goldToken; @@ -45,13 +45,9 @@ contract ReleaseGoldTest is Test, ECDSAHelper { address refundAddress = actor("refundAddress"); address newBeneficiary = actor("newBeneficiary"); address randomAddress = actor("randomAddress"); + address celoUnreleasedTreasury = actor("CeloUnreleasedTreasury"); uint256 constant TOTAL_AMOUNT = 1 ether * 10; - - uint256 constant MINUTE = 60; - uint256 constant HOUR = 60 * 60; - uint256 constant DAY = 24 * HOUR; - uint256 constant MONTH = 30 * DAY; uint256 constant UNLOCKING_PERIOD = 3 * DAY; ReleaseGoldMockTunnel.InitParams initParams; @@ -65,24 +61,6 @@ contract ReleaseGoldTest is Test, ECDSAHelper { event CanExpireSet(bool canExpire); event BeneficiarySet(address indexed beneficiary); - address constant proxyAdminAddress = 0x4200000000000000000000000000000000000018; - function _whenL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); - registry = Registry(proxyAdminAddress); - registry.setAddressFor("Accounts", address(accounts)); - registry.setAddressFor("Election", address(election)); - registry.setAddressFor("Freezer", address(freezer)); - registry.setAddressFor("GoldToken", address(goldToken)); - registry.setAddressFor("Governance", address(governance)); - registry.setAddressFor("LockedGold", address(lockedGold)); - registry.setAddressFor("Validators", address(validators)); - registry.setAddressFor("StableToken", address(stableToken)); - - lockedGold.setRegistry(proxyAdminAddress); - goldToken.setRegistry(proxyAdminAddress); - accounts.setRegistry(proxyAdminAddress); - } - function newReleaseGold(bool prefund, bool startReleasing) internal returns (ReleaseGold) { releaseGold = new ReleaseGold(true); @@ -93,7 +71,6 @@ contract ReleaseGoldTest is Test, ECDSAHelper { ); } - // releaseGold.initialize(config); ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold)); tunnel.MockInitialize(owner, initParams, initParams2); @@ -112,13 +89,10 @@ contract ReleaseGoldTest is Test, ECDSAHelper { } function setUp() public { + super.setUp(); (beneficiary, beneficiaryPrivateKey) = actorWithPK("beneficiary"); walletAddress = beneficiary; - address registryAddress = 0x000000000000000000000000000000000000ce10; - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); - registry = Registry(registryAddress); - accounts = new Accounts(true); freezer = new Freezer(true); goldToken = new GoldToken(true); @@ -132,14 +106,16 @@ contract ReleaseGoldTest is Test, ECDSAHelper { registry.setAddressFor("Election", address(election)); registry.setAddressFor("Freezer", address(freezer)); registry.setAddressFor("GoldToken", address(goldToken)); + registry.setAddressFor("CeloToken", address(goldToken)); registry.setAddressFor("Governance", address(governance)); registry.setAddressFor("LockedGold", address(lockedGold)); registry.setAddressFor("Validators", address(validators)); registry.setAddressFor("StableToken", address(stableToken)); + registry.setAddressFor("CeloUnreleasedTreasury", celoUnreleasedTreasury); - lockedGold.initialize(registryAddress, UNLOCKING_PERIOD); - goldToken.initialize(registryAddress); - accounts.initialize(registryAddress); + lockedGold.initialize(REGISTRY_ADDRESS, UNLOCKING_PERIOD); + goldToken.initialize(REGISTRY_ADDRESS); + accounts.initialize(REGISTRY_ADDRESS); vm.prank(beneficiary); accounts.createAccount(); @@ -160,18 +136,16 @@ contract ReleaseGoldTest is Test, ECDSAHelper { initialDistributionRatio: 1000, _canValidate: false, _canVote: true, - registryAddress: registryAddress + registryAddress: REGISTRY_ADDRESS }); vm.deal(randomAddress, 1000 ether); } } -contract ReleaseGoldTest_Initialize is ReleaseGoldTest { - function setUp() public { - super.setUp(); - } +contract ReleaseGoldTest_L2 is WhenL2, ReleaseGoldTest {} +contract ReleaseGoldTest_Initialize is ReleaseGoldTest { function test_ShouldIndicateIsFundedIfDeploymentIsPrefunded() public { newReleaseGold(true, false); assertTrue(releaseGold.isFunded()); @@ -183,11 +157,9 @@ contract ReleaseGoldTest_Initialize is ReleaseGoldTest { } } -contract ReleaseGoldTest_Payable is ReleaseGoldTest { - function setUp() public { - super.setUp(); - } +contract ReleaseGoldTest_Initialize_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_Initialize {} +contract ReleaseGoldTest_Payable is ReleaseGoldTest { function test_ShouldAcceptGoldTransferByDefaultFromAnyone() public { newReleaseGold(true, false); uint256 originalBalance = goldToken.balanceOf(address(releaseGold)); @@ -222,6 +194,8 @@ contract ReleaseGoldTest_Payable is ReleaseGoldTest { } } +contract ReleaseGoldTest_Payable_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_Payable {} + contract ReleaseGoldTest_Transfer is ReleaseGoldTest { address receiver = actor("receiver"); uint256 transferAmount = 10; @@ -240,6 +214,8 @@ contract ReleaseGoldTest_Transfer is ReleaseGoldTest { } } +contract ReleaseGoldTest_Transfer_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_Transfer {} + contract ReleaseGoldTest_GenericTransfer is ReleaseGoldTest { address receiver = actor("receiver"); uint256 transferAmount = 10; @@ -273,13 +249,14 @@ contract ReleaseGoldTest_GenericTransfer is ReleaseGoldTest { } } +contract ReleaseGoldTest_GenericTransfer_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_GenericTransfer +{} + contract ReleaseGoldTest_Creation is ReleaseGoldTest { uint256 public maxUint256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - function setUp() public { - super.setUp(); - } - function test_ShouldHaveAssociatedFundsWithAScheduleUponCreation() public { newReleaseGold(true, false); assertEq( @@ -398,6 +375,8 @@ contract ReleaseGoldTest_Creation is ReleaseGoldTest { } } +contract ReleaseGoldTest_Creation_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_Creation {} + contract ReleaseGoldTest_SetBeneficiary is ReleaseGoldTest { function setUp() public { super.setUp(); @@ -422,6 +401,8 @@ contract ReleaseGoldTest_SetBeneficiary is ReleaseGoldTest { } } +contract ReleaseGoldTest_SetBeneficiary_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_SetBeneficiary {} + contract ReleaseGoldTest_CreateAccount is ReleaseGoldTest { function setUp() public { super.setUp(); @@ -450,7 +431,9 @@ contract ReleaseGoldTest_CreateAccount is ReleaseGoldTest { } } -contract SetAccount is ReleaseGoldTest { +contract ReleaseGoldTest_CreateAccount_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_CreateAccount {} + +contract ReleaseGoldTest_SetAccount is ReleaseGoldTest { uint8 v; bytes32 r; bytes32 s; @@ -496,6 +479,8 @@ contract SetAccount is ReleaseGoldTest { } } +contract ReleaseGoldTest_SetAccount_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_SetAccount {} + contract ReleaseGoldTest_SetAccountName is ReleaseGoldTest { function setUp() public { super.setUp(); @@ -537,6 +522,8 @@ contract ReleaseGoldTest_SetAccountName is ReleaseGoldTest { } } +contract ReleaseGoldTest_SetAccountName_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_SetAccountName {} + contract ReleaseGoldTest_SetAccountWalletAddress is ReleaseGoldTest { uint8 v; bytes32 r; @@ -595,6 +582,11 @@ contract ReleaseGoldTest_SetAccountWalletAddress is ReleaseGoldTest { } } +contract ReleaseGoldTest_SetAccountWalletAddress_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_SetAccountWalletAddress +{} + contract ReleaseGoldTest_SetAccountMetadataURL is ReleaseGoldTest { function setUp() public { super.setUp(); @@ -640,6 +632,11 @@ contract ReleaseGoldTest_SetAccountMetadataURL is ReleaseGoldTest { } } +contract ReleaseGoldTest_SetAccountMetadataURL_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_SetAccountMetadataURL +{} + contract ReleaseGoldTest_SetAccountDataEncryptionKey is ReleaseGoldTest { bytes dataEncryptionKey = hex"02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111"; bytes longDataEncryptionKey = @@ -690,6 +687,11 @@ contract ReleaseGoldTest_SetAccountDataEncryptionKey is ReleaseGoldTest { } } +contract ReleaseGoldTest_SetAccountDataEncryptionKey_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_SetAccountDataEncryptionKey +{} + contract ReleaseGoldTest_SetMaxDistribution is ReleaseGoldTest { function setUp() public { super.setUp(); @@ -720,6 +722,11 @@ contract ReleaseGoldTest_SetMaxDistribution is ReleaseGoldTest { } } +contract ReleaseGoldTest_SetMaxDistribution_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_SetMaxDistribution +{} + contract ReleaseGoldTest_AuthorizationTests is ReleaseGoldTest { uint256 initialReleaseGoldAmount; @@ -856,7 +863,7 @@ contract ReleaseGoldTest_AuthorizationTests is ReleaseGoldTest { } function test_ShouldRevertIfTheSignatureIsIncorrect() public { - (address otherAccount, uint256 otherAccountPK) = actorWithPK("otherAccount"); + (, uint256 otherAccountPK) = actorWithPK("otherAccount"); (uint8 otherV, bytes32 otherR, bytes32 otherS) = getParsedSignatureOfAddress( address(releaseGold), otherAccountPK @@ -1039,7 +1046,12 @@ contract ReleaseGoldTest_AuthorizationTests is ReleaseGoldTest { } } -contract ReleaseGoldTest_AuthorizeWithPublicKeys is ReleaseGoldTest { +contract ReleaseGoldTest_AuthorizationTests_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_AuthorizationTests +{} + +contract ReleaseGoldTest_AuthorizeWithPublicKeys_setup is ReleaseGoldTest { uint8 v; bytes32 r; bytes32 s; @@ -1049,7 +1061,7 @@ contract ReleaseGoldTest_AuthorizeWithPublicKeys is ReleaseGoldTest { bytes ecdsaPublicKey; - function _randomBytes32() internal returns (bytes32) { + function _randomBytes32() internal view returns (bytes32) { return keccak256(abi.encodePacked(block.timestamp, block.difficulty, msg.sender)); } @@ -1077,7 +1089,9 @@ contract ReleaseGoldTest_AuthorizeWithPublicKeys is ReleaseGoldTest { (v, r, s) = getParsedSignatureOfAddress(address(releaseGold), authorizedPK); ecdsaPublicKey = addressToPublicKey(keccak256(abi.encodePacked("dummy_msg_data")), v, r, s); } +} +contract ReleaseGoldTest_AuthorizeWithPublicKeys is ReleaseGoldTest_AuthorizeWithPublicKeys_setup { function test_ShouldSetTheAuthorizedKeys_WhenUsingECDSAPublickKey() public { vm.prank(beneficiary); releaseGold.authorizeValidatorSignerWithPublicKey( @@ -1124,10 +1138,13 @@ contract ReleaseGoldTest_AuthorizeWithPublicKeys is ReleaseGoldTest { assertEq(accounts.getValidatorSigner(address(releaseGold)), authorized); assertEq(accounts.validatorSignerToAccount(authorized), address(releaseGold)); } +} - function test_Reverts_WhenAuthorizeValidatorSignerWithPublicKeyCalledOnL2() public { - _whenL2(); - vm.expectRevert("This method is no longer supported in L2."); +contract ReleaseGoldTest_AuthorizeWithPublicKeys_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_AuthorizeWithPublicKeys_setup +{ + function test_ShouldSetTheAuthorizedKeys_WhenUsingECDSAPublickKey() public { vm.prank(beneficiary); releaseGold.authorizeValidatorSignerWithPublicKey( address(uint160(authorized)), @@ -1136,10 +1153,13 @@ contract ReleaseGoldTest_AuthorizeWithPublicKeys is ReleaseGoldTest { s, ecdsaPublicKey ); + + assertEq(accounts.authorizedBy(authorized), address(releaseGold)); + assertEq(accounts.getValidatorSigner(address(releaseGold)), authorized); + assertEq(accounts.validatorSignerToAccount(authorized), address(releaseGold)); } - function test_Reverts_WhenAuthorizeValidatorSignerWithKeysCalledOnL2() public { - _whenL2(); + function test_Reverts_WhenAuthorizeValidatorSignerWithKeys() public { bytes32 newBlsPublicKeyPart1 = _randomBytes32(); bytes32 newBlsPublicKeyPart2 = _randomBytes32(); bytes32 newBlsPublicKeyPart3 = _randomBytes32(); @@ -1171,10 +1191,6 @@ contract ReleaseGoldTest_AuthorizeWithPublicKeys is ReleaseGoldTest { } contract ReleaseGoldTest_Revoke is ReleaseGoldTest { - function setUp() public { - super.setUp(); - } - function test_ShouldAllowReleaseOwnerToRevokeTheReleaseGOld() public { newReleaseGold(true, false); vm.expectEmit(true, true, true, true); @@ -1212,6 +1228,8 @@ contract ReleaseGoldTest_Revoke is ReleaseGoldTest { } } +contract ReleaseGoldTest_Revoke_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_Revoke {} + contract ReleaseGoldTest_Expire is ReleaseGoldTest { function setUp() public { super.setUp(); @@ -1347,6 +1365,8 @@ contract ReleaseGoldTest_Expire is ReleaseGoldTest { } } +contract ReleaseGoldTest_Expire_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_Expire {} + contract ReleaseGoldTest_RefundAndFinalize is ReleaseGoldTest { function setUp() public { super.setUp(); @@ -1403,6 +1423,11 @@ contract ReleaseGoldTest_RefundAndFinalize is ReleaseGoldTest { } } +contract ReleaseGoldTest_RefundAndFinalize_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_RefundAndFinalize +{} + contract ReleaseGoldTest_ExpireSelfDestructTest is ReleaseGoldTest { function setUp() public { super.setUp(); @@ -1423,6 +1448,11 @@ contract ReleaseGoldTest_ExpireSelfDestructTest is ReleaseGoldTest { } } +contract ReleaseGoldTest_ExpireSelfDestructTest_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_ExpireSelfDestructTest +{} + contract ReleaseGoldTest_LockGold is ReleaseGoldTest { uint256 lockAmount; function setUp() public { @@ -1465,6 +1495,8 @@ contract ReleaseGoldTest_LockGold is ReleaseGoldTest { } } +contract ReleaseGoldTest_LockGold_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_LockGold {} + contract ReleaseGoldTest_UnlockGold is ReleaseGoldTest { uint256 lockAmount; function setUp() public { @@ -1528,6 +1560,8 @@ contract ReleaseGoldTest_UnlockGold is ReleaseGoldTest { } } +contract ReleaseGoldTest_UnlockGold_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_UnlockGold {} + contract ReleaseGoldTest_WithdrawLockedGold is ReleaseGoldTest { uint256 value = 1000; uint256 index = 0; @@ -1598,6 +1632,11 @@ contract ReleaseGoldTest_WithdrawLockedGold is ReleaseGoldTest { } } +contract ReleaseGoldTest_WithdrawLockedGold_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_WithdrawLockedGold +{} + contract ReleaseGoldTest_RelockGold is ReleaseGoldTest { uint256 pendingWithdrawalValue = 1000; uint256 index = 0; @@ -1704,6 +1743,8 @@ contract ReleaseGoldTest_RelockGold is ReleaseGoldTest { } } +contract ReleaseGoldTest_RelockGold_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_RelockGold {} + contract ReleaseGoldTest_Withdraw is ReleaseGoldTest { uint256 initialReleaseGoldAmount; @@ -2000,6 +2041,8 @@ contract ReleaseGoldTest_Withdraw is ReleaseGoldTest { } } +contract ReleaseGoldTest_Withdraw_L2 is ReleaseGoldTest_L2, ReleaseGoldTest_Withdraw {} + contract ReleaseGoldTest_WithdrawSelfDestruct_WhenNotRevoked is ReleaseGoldTest { uint256 initialReleaseGoldAmount; @@ -2026,6 +2069,11 @@ contract ReleaseGoldTest_WithdrawSelfDestruct_WhenNotRevoked is ReleaseGoldTest } } +contract ReleaseGoldTest_WithdrawSelfDestruct_WhenNotRevoked_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_WithdrawSelfDestruct_WhenNotRevoked +{} + contract ReleaseGoldTest_WithdrawSelfDestruct_WhenRevoked is ReleaseGoldTest { uint256 initialReleaseGoldAmount; @@ -2054,6 +2102,11 @@ contract ReleaseGoldTest_WithdrawSelfDestruct_WhenRevoked is ReleaseGoldTest { } } +contract ReleaseGoldTest_WithdrawSelfDestruct_WhenRevoked_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_WithdrawSelfDestruct_WhenRevoked +{} + contract ReleaseGoldTest_GetCurrentReleasedTotalAmount is ReleaseGoldTest { uint256 initialReleaseGoldAmount; @@ -2097,6 +2150,11 @@ contract ReleaseGoldTest_GetCurrentReleasedTotalAmount is ReleaseGoldTest { } } +contract ReleaseGoldTest_GetCurrentReleasedTotalAmount_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_GetCurrentReleasedTotalAmount +{} + contract ReleaseGoldTest_GetWithdrawableAmount is ReleaseGoldTest { uint256 initialReleaseGoldAmount; @@ -2200,138 +2258,7 @@ contract ReleaseGoldTest_GetWithdrawableAmount is ReleaseGoldTest { } } -contract ReleaseGoldTest_DeployAndInitializeOnL2 is ReleaseGoldTest { - uint256 public maxUint256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - function setUp() public { - super.setUp(); - _whenL2(); - initParams2.registryAddress = proxyAdminAddress; - } - - function test_ShouldIndicateIsFundedIfDeploymentIsPrefunded() public { - newReleaseGold(true, false); - assertTrue(releaseGold.isFunded()); - } - - function test_ShouldNotIndicateFundedAndNotRevertIfDeploymentIsNotPrefunded() public { - newReleaseGold(false, false); - assertFalse(releaseGold.isFunded()); - } - - function test_ShouldHaveAssociatedFundsWithAScheduleUponCreation() public { - newReleaseGold(true, false); - assertEq( - goldToken.balanceOf(address(releaseGold)), - initParams.numReleasePeriods * initParams.amountReleasedPerPeriod - ); - } - - function test_ShouldSetABeneficiaryToReleaseGoldInstance() public { - newReleaseGold(true, false); - assertEq(releaseGold.beneficiary(), initParams._beneficiary); - } - - function test_ShouldSetAReleaseOwnerToReleaseGoldInstance() public { - newReleaseGold(true, false); - assertEq(releaseGold.releaseOwner(), initParams2._releaseOwner); - } - - function test_ShouldSetReleaseGoldNumberOfPeriods() public { - newReleaseGold(true, false); - (, , uint256 releaseGoldNumPeriods, , ) = releaseGold.releaseSchedule(); - assertEq(releaseGoldNumPeriods, initParams.numReleasePeriods); - } - - function test_ShouldSetReleaseGoldAmountPerPeriod() public { - newReleaseGold(true, false); - (, , , , uint256 releaseGoldAmountPerPeriod) = releaseGold.releaseSchedule(); - assertEq(releaseGoldAmountPerPeriod, initParams.amountReleasedPerPeriod); - } - - function test_ShouldSetReleaseGoldPeriod() public { - newReleaseGold(true, false); - (, , , uint256 releaseGoldPeriod, ) = releaseGold.releaseSchedule(); - assertEq(releaseGoldPeriod, initParams.releasePeriod); - } - - function test_ShouldSetReleaseGoldStartTime() public { - newReleaseGold(true, false); - (uint256 releaseGoldStartTime, , , , ) = releaseGold.releaseSchedule(); - assertEq(releaseGoldStartTime, initParams.releaseStartTime); - } - - function test_ShouldSetReleaseGoldCliffTime() public { - newReleaseGold(true, false); - (, uint256 releaseGoldCliffTime, , , ) = releaseGold.releaseSchedule(); - uint256 expectedCliffTime = initParams.releaseStartTime + initParams.releaseCliffTime; - assertEq(releaseGoldCliffTime, expectedCliffTime); - } - - function test_ShouldSetRevocableFlagToReleaseGoldInstance() public { - newReleaseGold(true, false); - (bool revocable, , , ) = releaseGold.revocationInfo(); - assertEq(revocable, initParams.revocable); - } - - function test_ShouldSetReleaseOwnerToReleaseGoldInstance() public { - newReleaseGold(true, false); - assertEq(releaseGold.releaseOwner(), initParams2._releaseOwner); - } - - function test_ShouldSetLiquidityProvisionMetToTrue() public { - newReleaseGold(true, false); - assertEq(releaseGold.liquidityProvisionMet(), true); - } - - function test_ShouldHaveZeroTotalWithdrawnOnInit() public { - newReleaseGold(true, false); - assertEq(releaseGold.totalWithdrawn(), 0); - } - - function test_ShouldBeUnrevokedOnInitAndHaveRevokeTimeEqualZero() public { - newReleaseGold(true, false); - (, , , uint256 revokeTime) = releaseGold.revocationInfo(); - assertEq(revokeTime, 0); - assertEq(releaseGold.isRevoked(), false); - } - - function test_ShouldHaveReleaseGoldBalanceAtRevokeOnInitEqualToZero() public { - newReleaseGold(true, false); - (, , uint256 releasedBalanceAtRevoke, ) = releaseGold.revocationInfo(); - assertEq(releasedBalanceAtRevoke, 0); - } - - function test_ShouldRevertWhenReleaseGoldBeneficiaryIsTheNullAddress() public { - releaseGold = new ReleaseGold(true); - initParams._beneficiary = address(0); - ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold)); - vm.expectRevert("unsuccessful tunnel call"); - tunnel.MockInitialize(owner, initParams, initParams2); - } - - function test_ShouldRevertWhenReleaseGoldPeriodsAreZero() public { - releaseGold2 = new ReleaseGold(true); - initParams.numReleasePeriods = 0; - ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold2)); - vm.expectRevert("unsuccessful tunnel call"); - tunnel.MockInitialize(owner, initParams, initParams2); - } - - function test_ShouldRevertWhenReleasedAmountPerPeriodIsZero() public { - releaseGold2 = new ReleaseGold(true); - initParams.amountReleasedPerPeriod = 0; - ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold2)); - vm.expectRevert("unsuccessful tunnel call"); - tunnel.MockInitialize(owner, initParams, initParams2); - } - - function test_ShouldOverflowForVeryLargeCombinationsOdReleasePeriodsAndAmountPerTime() public { - releaseGold = new ReleaseGold(true); - initParams.numReleasePeriods = maxUint256; - initParams.amountReleasedPerPeriod = maxUint256; - initParams2.initialDistributionRatio = 999; - ReleaseGoldMockTunnel tunnel = new ReleaseGoldMockTunnel(address(releaseGold)); - vm.expectRevert("unsuccessful tunnel call"); - tunnel.MockInitialize(owner, initParams, initParams2); - } -} +contract ReleaseGoldTest_GetWithdrawableAmount_L2 is + ReleaseGoldTest_L2, + ReleaseGoldTest_GetWithdrawableAmount +{} diff --git a/packages/protocol/test-sol/governance/voting/mocks/ReleaseGoldMockTunnel.sol b/packages/protocol/test-sol/unit/governance/voting/mocks/ReleaseGoldMockTunnel.sol similarity index 97% rename from packages/protocol/test-sol/governance/voting/mocks/ReleaseGoldMockTunnel.sol rename to packages/protocol/test-sol/unit/governance/voting/mocks/ReleaseGoldMockTunnel.sol index 161b377fcb0..c1f4c358bfb 100644 --- a/packages/protocol/test-sol/governance/voting/mocks/ReleaseGoldMockTunnel.sol +++ b/packages/protocol/test-sol/unit/governance/voting/mocks/ReleaseGoldMockTunnel.sol @@ -2,7 +2,7 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; -import "../../../../contracts/governance/ReleaseGold.sol"; +import "@celo-contracts/governance/ReleaseGold.sol"; import { Test as ForgeTest } from "forge-std/Test.sol"; contract ReleaseGoldMockTunnel is ForgeTest { diff --git a/packages/protocol/test-sol/identity/Attestations.t.sol b/packages/protocol/test-sol/unit/identity/Attestations.t.sol similarity index 100% rename from packages/protocol/test-sol/identity/Attestations.t.sol rename to packages/protocol/test-sol/unit/identity/Attestations.t.sol diff --git a/packages/protocol/test-sol/identity/Escrow.t.sol b/packages/protocol/test-sol/unit/identity/Escrow.t.sol similarity index 99% rename from packages/protocol/test-sol/identity/Escrow.t.sol rename to packages/protocol/test-sol/unit/identity/Escrow.t.sol index eec8c27c734..a5a1f3122d6 100644 --- a/packages/protocol/test-sol/identity/Escrow.t.sol +++ b/packages/protocol/test-sol/unit/identity/Escrow.t.sol @@ -3,6 +3,8 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; import "celo-foundry/Test.sol"; +import { TestConstants } from "@test-sol/constants.sol"; + import "@celo-contracts/identity/Escrow.sol"; import "@celo-contracts/identity/FederatedAttestations.sol"; import "@celo-contracts/identity/test/MockAttestations.sol"; @@ -11,7 +13,7 @@ import "@celo-contracts/common/FixidityLib.sol"; import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Signatures.sol"; -contract EscrowTest is Test { +contract EscrowTest is Test, TestConstants { using FixidityLib for FixidityLib.Fraction; Escrow escrowContract; @@ -74,14 +76,12 @@ contract EscrowTest is Test { address trustedIssuer2; function setUp() public { - address registryAddress = 0x000000000000000000000000000000000000ce10; - - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); mockERC20Token = new MockERC20Token(); escrowContract = new Escrow(true); escrowContract.initialize(); - registry = Registry(registryAddress); + registry = Registry(REGISTRY_ADDRESS); (receiver, receiverPK) = actorWithPK("receiver"); (sender, senderPK) = actorWithPK("sender"); trustedIssuer1 = actor("trustedIssuer1"); diff --git a/packages/protocol/test-sol/identity/FederatedAttestations.t.sol b/packages/protocol/test-sol/unit/identity/FederatedAttestations.t.sol similarity index 99% rename from packages/protocol/test-sol/identity/FederatedAttestations.t.sol rename to packages/protocol/test-sol/unit/identity/FederatedAttestations.t.sol index 0c9606acdb4..fee5697e3cb 100644 --- a/packages/protocol/test-sol/identity/FederatedAttestations.t.sol +++ b/packages/protocol/test-sol/unit/identity/FederatedAttestations.t.sol @@ -3,6 +3,8 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; import "celo-foundry/Test.sol"; +import { TestConstants } from "@test-sol/constants.sol"; + import "@celo-contracts/identity/test/AttestationsTest.sol"; import "@celo-contracts/identity/FederatedAttestations.sol"; import "@celo-contracts/identity/test/MockERC20Token.sol"; @@ -13,7 +15,7 @@ import "@celo-contracts/governance/test/MockValidators.sol"; import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Accounts.sol"; -contract FederatedAttestationsFoundryTest is Test { +contract FederatedAttestationsFoundryTest is Test, TestConstants { enum KeyOffsets { NO_OFFSET, VALIDATING_KEY_OFFSET, @@ -326,9 +328,7 @@ contract FederatedAttestationsFoundryTest is Test { phoneHash = keccak256(abi.encodePacked(phoneNumber)); phoneHash2 = keccak256(abi.encodePacked(phoneNumber2)); - address registryAddress = 0x000000000000000000000000000000000000ce10; - - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); attestationsTest = new AttestationsTest(); mockERC20Token = new MockERC20Token(); @@ -337,7 +337,7 @@ contract FederatedAttestationsFoundryTest is Test { mockLockedGold = new MockLockedGold(); mockValidators = new MockValidators(); random = new MockRandom(); - registry = Registry(registryAddress); + registry = Registry(REGISTRY_ADDRESS); accounts = new Accounts(true); federatedAttestations = new FederatedAttestations(true); random.initialize(256); diff --git a/packages/protocol/test-sol/identity/IdentityProxy.t.sol b/packages/protocol/test-sol/unit/identity/IdentityProxy.t.sol similarity index 100% rename from packages/protocol/test-sol/identity/IdentityProxy.t.sol rename to packages/protocol/test-sol/unit/identity/IdentityProxy.t.sol diff --git a/packages/protocol/test-sol/identity/IdentityProxyHub.t.sol b/packages/protocol/test-sol/unit/identity/IdentityProxyHub.t.sol similarity index 100% rename from packages/protocol/test-sol/identity/IdentityProxyHub.t.sol rename to packages/protocol/test-sol/unit/identity/IdentityProxyHub.t.sol diff --git a/packages/protocol/test-sol/identity/OdisPayments.t.sol b/packages/protocol/test-sol/unit/identity/OdisPayments.t.sol similarity index 92% rename from packages/protocol/test-sol/identity/OdisPayments.t.sol rename to packages/protocol/test-sol/unit/identity/OdisPayments.t.sol index dbb730802ad..fb526c3633c 100644 --- a/packages/protocol/test-sol/identity/OdisPayments.t.sol +++ b/packages/protocol/test-sol/unit/identity/OdisPayments.t.sol @@ -3,12 +3,14 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; import "celo-foundry/Test.sol"; +import { TestConstants } from "@test-sol/constants.sol"; + import "@celo-contracts/identity/OdisPayments.sol"; -import { StableToken } from "../../lib/mento-core/contracts/StableToken.sol"; +import { StableToken } from "@mento-core/contracts/StableToken.sol"; import "@celo-contracts/common/Registry.sol"; import "@celo-contracts/common/Freezer.sol"; -contract OdisPaymentsFoundryTest is Test { +contract OdisPaymentsFoundryTest is Test, TestConstants { uint256 FIXED1 = 1000000000000000000000000; uint256 SECONDS_IN_A_DAY = 60 * 60 * 24; uint256 startingBalanceCUSD = 1000; @@ -24,11 +26,9 @@ contract OdisPaymentsFoundryTest is Test { event PaymentMade(address indexed account, uint256 valueInCUSD); function setUp() public { - address registryAddress = 0x000000000000000000000000000000000000ce10; - - deployCodeTo("Registry.sol", abi.encode(false), registryAddress); + deployCodeTo("Registry.sol", abi.encode(false), REGISTRY_ADDRESS); - registry = Registry(registryAddress); + registry = Registry(REGISTRY_ADDRESS); freezer = new Freezer(true); odisPayments = new OdisPayments(true); stableToken = new StableToken(true); diff --git a/packages/protocol/test-sol/identity/Random.t.sol b/packages/protocol/test-sol/unit/identity/Random.t.sol similarity index 85% rename from packages/protocol/test-sol/identity/Random.t.sol rename to packages/protocol/test-sol/unit/identity/Random.t.sol index 23a7216c2dc..3a6b33eb0c2 100644 --- a/packages/protocol/test-sol/identity/Random.t.sol +++ b/packages/protocol/test-sol/unit/identity/Random.t.sol @@ -1,22 +1,27 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.5.13; -import "celo-foundry/Test.sol"; -import { Utils } from "@test-sol/utils.sol"; +import { TestWithUtils } from "@test-sol/TestWithUtils.sol"; import "@celo-contracts/identity/Random.sol"; import "@celo-contracts/identity/test/RandomTest.sol"; -contract RandomnessTest_SetRandomnessRetentionWindow is Test, IsL2Check { - event RandomnessBlockRetentionWindowSet(uint256 value); - +contract RandomTest_ is TestWithUtils { RandomTest random; + event RandomnessBlockRetentionWindowSet(uint256 value); + function setUp() public { random = new RandomTest(); random.initialize(256); } + function commitmentFor(uint256 value) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(bytes32(value))); + } +} + +contract RandomTest_SetRandomnessRetentionWindow is RandomTest_ { function test_ShouldSetTheVariable() public { random.setRandomnessBlockRetentionWindow(1000); assertEq(random.randomnessBlockRetentionWindow(), 1000); @@ -35,23 +40,16 @@ contract RandomnessTest_SetRandomnessRetentionWindow is Test, IsL2Check { } function test_Reverts_WhenCalledOnL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); + _whenL2(); vm.expectRevert("This method is no longer supported in L2."); random.setRandomnessBlockRetentionWindow(1000); } } -contract RandomnessTest_AddTestRandomness is Test, Utils, IsL2Check { +contract RandomTest_AddTestRandomness is RandomTest_ { uint256 constant RETENTION_WINDOW = 5; uint256 constant EPOCH_SIZE = 10; - RandomTest random; - - function setUp() public { - random = new RandomTest(); - random.initialize(256); - } - function test_ShouldBeAbleToSimulateAddingRandomness() public { random.addTestRandomness(1, 0x0000000000000000000000000000000000000000000000000000000000000001); random.addTestRandomness(2, 0x0000000000000000000000000000000000000000000000000000000000000002); @@ -216,7 +214,7 @@ contract RandomnessTest_AddTestRandomness is Test, Utils, IsL2Check { } function test_Reverts_WhenCalledOnL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); + _whenL2(); vm.expectRevert("This method is no longer supported in L2."); random.addTestRandomness(1, 0x0000000000000000000000000000000000000000000000000000000000000001); vm.expectRevert("This method is no longer supported in L2."); @@ -224,22 +222,15 @@ contract RandomnessTest_AddTestRandomness is Test, Utils, IsL2Check { } } -contract RandomnessTest_RevealAndCommit is Test, Utils, IsL2Check { +contract RandomTest_RevealAndCommit is RandomTest_ { address constant ACCOUNT = address(0x01); bytes32 constant RANDONMESS = bytes32(uint256(0x00)); - RandomTest random; - function setUp() public { - random = new RandomTest(); - random.initialize(256); + super.setUp(); random.setRandomnessBlockRetentionWindow(256); } - function commitmentFor(uint256 value) private pure returns (bytes32) { - return keccak256(abi.encodePacked(bytes32(value))); - } - function testRevert_CannotAddZeroCommitment() public { vm.expectRevert("cannot commit zero randomness"); random.testRevealAndCommit(RANDONMESS, commitmentFor(0x00), ACCOUNT); @@ -262,9 +253,50 @@ contract RandomnessTest_RevealAndCommit is Test, Utils, IsL2Check { } function test_Reverts_WhenCalledOnL2() public { - deployCodeTo("Registry.sol", abi.encode(false), proxyAdminAddress); + _whenL2(); vm.expectRevert("This method is no longer supported in L2."); blockTravel(2); random.testRevealAndCommit(RANDONMESS, commitmentFor(0x01), ACCOUNT); } } + +contract RandomTest_Commitments is RandomTest_ { + address constant ACCOUNT = address(0x01); + bytes32 constant RANDONMESS = bytes32(uint256(0x00)); + uint256 randomness2 = 0x01; + bytes32 commitment2 = commitmentFor(randomness2); + + function setUp() public { + super.setUp(); + random.testRevealAndCommit(RANDONMESS, commitment2, ACCOUNT); + } + + function test_returnsACommtiment() public { + bytes32 commitment = random.commitments(ACCOUNT); + assertEq(commitment, commitment2); + } + + function test_Reverts_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + random.commitments(ACCOUNT); + } +} + +contract RandomTest_RandomnessBlockRetentionWindow is RandomTest_ { + function setUp() public { + super.setUp(); + random.setRandomnessBlockRetentionWindow(256); + } + + function test_getsTheRandomnessBlockRetentionWindow() public { + uint256 randomnessBlockRetentionWindow = random.randomnessBlockRetentionWindow(); + assertEq(randomnessBlockRetentionWindow, 256); + } + + function test_Reverts_WhenCalledOnL2() public { + _whenL2(); + vm.expectRevert("This method is no longer supported in L2."); + random.randomnessBlockRetentionWindow(); + } +} diff --git a/packages/protocol/test-sol/stability/FeeCurrencyAdapter.t.sol b/packages/protocol/test-sol/unit/stability/FeeCurrencyAdapter.t.sol similarity index 97% rename from packages/protocol/test-sol/stability/FeeCurrencyAdapter.t.sol rename to packages/protocol/test-sol/unit/stability/FeeCurrencyAdapter.t.sol index e0d6cb69ff6..efddc2c0815 100644 --- a/packages/protocol/test-sol/stability/FeeCurrencyAdapter.t.sol +++ b/packages/protocol/test-sol/unit/stability/FeeCurrencyAdapter.t.sol @@ -3,8 +3,8 @@ pragma solidity >=0.8.7 <=0.8.20; import "celo-foundry-8/Test.sol"; -import "../../contracts/common/FixidityLib.sol"; -import "../../contracts/common/interfaces/IRegistry.sol"; +import "@celo-contracts/common/FixidityLib.sol"; +import "@celo-contracts/common/interfaces/IRegistry.sol"; // Contract to test import "@celo-contracts-8/stability/CeloFeeCurrencyAdapterOwnable.sol"; @@ -106,13 +106,7 @@ contract FeeCurrencyAdapterTest is Test { feeCurrencyAdapter = new CeloFeeCurrencyAdapterTestContract(true); feeCurrencyAdapterForFuzzyTests = new CeloFeeCurrencyAdapterTestContract(true); - address feeCurrencyAddress = actor("feeCurrency"); - - string memory name = "tokenName"; - string memory symbol = "tN"; - feeCurrency = new FeeCurrency6DecimalsTest(initialSupply); - feeCurrencyAdapter.initialize(address(feeCurrency), "wrapper", "wr", 18); } } @@ -130,7 +124,6 @@ contract FeeCurrencyAdapter_Initialize is FeeCurrencyAdapterTest { function test_ShouldSucceed_WhenExpectedDecimalsAreMoreThenDecimals_Fuzz(uint8 amount) public { vm.assume(amount > 6); vm.assume(amount < 50); - console.log("amount", amount); feeCurrencyAdapterForFuzzyTests.initialize(address(feeCurrency), "adapter", "ad", amount); } @@ -303,7 +296,6 @@ contract FeeCurrencyAdapter_CreditGasFees is FeeCurrencyAdapterTest { function creditFuzzHelper(uint8 expectedDigits, uint256 multiplier) public { uint256 originalAmount = 1000; uint256 amount = originalAmount * multiplier; - console.log("amount", amount); address secondAddress = actor("secondAddress"); address thirdAddress = actor("thirdAddress"); diff --git a/packages/protocol/test-sol/stability/SortedOracles.mento.t.sol b/packages/protocol/test-sol/unit/stability/SortedOracles.mento.t.sol similarity index 99% rename from packages/protocol/test-sol/stability/SortedOracles.mento.t.sol rename to packages/protocol/test-sol/unit/stability/SortedOracles.mento.t.sol index 25e26859b4e..9f6ad19e9c8 100644 --- a/packages/protocol/test-sol/stability/SortedOracles.mento.t.sol +++ b/packages/protocol/test-sol/unit/stability/SortedOracles.mento.t.sol @@ -8,8 +8,8 @@ import { Test, console2 as console } from "celo-foundry/Test.sol"; import { SortedLinkedListWithMedian } from "contracts/common/linkedlists/SortedLinkedListWithMedian.sol"; import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { IBreakerBox } from "../../contracts/stability/interfaces/IBreakerBox.sol"; -import { SortedOracles } from "../../contracts/stability/SortedOracles.sol"; +import { IBreakerBox } from "@celo-contracts/stability/interfaces/IBreakerBox.sol"; +import { SortedOracles } from "@celo-contracts/stability/SortedOracles.sol"; contract MockBreakerBox is IBreakerBox { uint256 public tradingMode; diff --git a/packages/protocol/test-sol/stability/SortedOracles.t.sol b/packages/protocol/test-sol/unit/stability/SortedOracles.t.sol similarity index 99% rename from packages/protocol/test-sol/stability/SortedOracles.t.sol rename to packages/protocol/test-sol/unit/stability/SortedOracles.t.sol index 003cb5e3b30..559d10d7901 100644 --- a/packages/protocol/test-sol/stability/SortedOracles.t.sol +++ b/packages/protocol/test-sol/unit/stability/SortedOracles.t.sol @@ -3,14 +3,14 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; import { Test } from "celo-foundry/Test.sol"; -import { SortedOracles } from "../../contracts/stability/SortedOracles.sol"; +import { SortedOracles } from "@celo-contracts/stability/SortedOracles.sol"; import "@celo-contracts/common/FixidityLib.sol"; import "@celo-contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol"; import "@celo-contracts/common/linkedlists/SortedLinkedListWithMedian.sol"; -import { Constants } from "../constants.sol"; +import { TestConstants } from "@test-sol/constants.sol"; import "forge-std/console.sol"; -contract SortedOraclesTest is Test, Constants { +contract SortedOraclesTest is Test, TestConstants { using FixidityLib for FixidityLib.Fraction; using AddressSortedLinkedListWithMedian for SortedLinkedListWithMedian.List; diff --git a/packages/protocol/test-sol/utils.sol b/packages/protocol/test-sol/utils.sol deleted file mode 100644 index 91189b89bab..00000000000 --- a/packages/protocol/test-sol/utils.sol +++ /dev/null @@ -1,78 +0,0 @@ -pragma solidity ^0.5.13; - -import "celo-foundry/Test.sol"; -import "openzeppelin-solidity/contracts/utils/EnumerableSet.sol"; - -contract Utils is Test { - using EnumerableSet for EnumerableSet.AddressSet; - - EnumerableSet.AddressSet addressSet; - - function timeTravel(uint256 timeDelta) public { - vm.warp(block.timestamp + timeDelta); - } - - function blockTravel(uint256 blockDelta) public { - vm.roll(block.number + blockDelta); - } - - function assertAlmostEqual(uint256 actual, uint256 expected, uint256 margin) public { - uint256 diff = actual > expected ? actual - expected : expected - actual; - assertTrue(diff <= margin, string(abi.encodePacked("Difference is ", uintToStr(diff)))); - } - - function uintToStr(uint256 _i) internal pure returns (string memory _uintAsString) { - uint256 number = _i; - if (number == 0) { - return "0"; - } - uint256 j = number; - uint256 len; - while (j != 0) { - len++; - j /= 10; - } - bytes memory bstr = new bytes(len); - uint256 k = len - 1; - while (number != 0) { - bstr[k--] = bytes1(uint8(48 + (number % 10))); - number /= 10; - } - return string(bstr); - } - - function arraysEqual(address[] memory arr1, address[] memory arr2) public returns (bool) { - if (arr1.length != arr2.length) { - return false; // Arrays of different lengths cannot be equal - } - - // Add addresses from arr1 to the set - for (uint256 i = 0; i < arr1.length; i++) { - addressSet.add(arr1[i]); - } - - // Check if each address in arr2 is in the set - for (uint256 i = 0; i < arr2.length; i++) { - if (!addressSet.contains(arr2[i])) { - clearSet(arr1); - return false; - } - } - - clearSet(arr1); - return true; - } - - function clearSet(address[] memory arr1) private { - for (uint256 i = 0; i < arr1.length; i++) { - addressSet.remove(arr1[i]); - } - } - - // Generates pseudo random number in the range [min, max] using block attributes - function generatePRN(uint256 min, uint256 max, uint256 salt) public view returns (uint256) { - return - (uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty, msg.sender, salt))) % - (max - min + 1)) + min; - } -} diff --git a/packages/protocol/test-sol/utils/ECDSAHelper.sol b/packages/protocol/test-sol/utils/ECDSAHelper.sol index cd85d52cccd..aaa7c1119ae 100644 --- a/packages/protocol/test-sol/utils/ECDSAHelper.sol +++ b/packages/protocol/test-sol/utils/ECDSAHelper.sol @@ -13,7 +13,7 @@ contract ECDSAHelper is Test { bytes32 _s ) public returns (bytes memory) { address SECP256K1Address = actor("SECP256K1Address"); - deployCodeTo("out/SECP256K1.sol/SECP256K1.0.5.17.json", SECP256K1Address); + deployCodeTo("SECP256K1.sol:SECP256K1", SECP256K1Address); sECP256K1 = ISECP256K1(SECP256K1Address); string memory header = "\x19Ethereum Signed Message:\n32"; diff --git a/packages/protocol/test-sol/utils/ECDSAHelper08.sol b/packages/protocol/test-sol/utils/ECDSAHelper08.sol new file mode 100644 index 00000000000..3c761bb9e76 --- /dev/null +++ b/packages/protocol/test-sol/utils/ECDSAHelper08.sol @@ -0,0 +1,34 @@ +pragma solidity >=0.5.13 <0.8.20; +import "celo-foundry-8/Test.sol"; +import "@test-sol/utils/SECP256K1.sol"; + +contract ECDSAHelper08 is Test { + ISECP256K1 sECP256K1; + + function addressToPublicKey( + bytes32 message, + uint8 _v, + bytes32 _r, + bytes32 _s + ) public returns (bytes memory) { + address SECP256K1Address = actor("SECP256K1Address"); + deployCodeTo("SECP256K1.sol:SECP256K1", SECP256K1Address); + sECP256K1 = ISECP256K1(SECP256K1Address); + + string memory header = "\x19Ethereum Signed Message:\n32"; + bytes32 _message = keccak256(abi.encodePacked(header, message)); + (uint256 x, uint256 y) = sECP256K1.recover( + uint256(_message), + _v - 27, + uint256(_r), + uint256(_s) + ); + return abi.encodePacked(x, y); + } + + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } +} diff --git a/packages/protocol/test-sol/utils/ReserveSpenderMultiSig.t.sol b/packages/protocol/test-sol/utils/ReserveSpenderMultiSig.t.sol new file mode 100644 index 00000000000..409cf9c0f45 --- /dev/null +++ b/packages/protocol/test-sol/utils/ReserveSpenderMultiSig.t.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.5.13; + +import "@mento-core/contracts/ReserveSpenderMultiSig.sol"; + +/** + The purpose of this file is not to provide test coverage for `ReserveSpenderMultiSig.sol`. + This is an empty test to force foundry to compile `ReserveSpenderMultiSig.sol`, and include its + artifacts in the `/out` directory. `ReserveSpenderMultiSig.sol` is needed in the migrations + script, but because it's on Solidity 0.5 it can't be imported there directly. + If there is a better way to force foundry to compile `ReserveSpenderMultiSig.sol` without + this file, this file can confidently be deleted. + */ +contract ReserveSpenderMultiSigTest {} diff --git a/packages/protocol/test-sol/utils/WhenL2.sol b/packages/protocol/test-sol/utils/WhenL2.sol new file mode 100644 index 00000000000..3891956e13e --- /dev/null +++ b/packages/protocol/test-sol/utils/WhenL2.sol @@ -0,0 +1,11 @@ +pragma solidity ^0.5.13; +pragma experimental ABIEncoderV2; + +import "@test-sol/TestWithUtils.sol"; + +contract WhenL2 is TestWithUtils { + function setUp() public { + super.setUp(); + whenL2WithEpochManagerInitialization(); + } +} diff --git a/packages/protocol/test-sol/utils08.sol b/packages/protocol/test-sol/utils08.sol new file mode 100644 index 00000000000..c9495b72a23 --- /dev/null +++ b/packages/protocol/test-sol/utils08.sol @@ -0,0 +1,47 @@ +pragma solidity >=0.5.13 <0.9.0; + +import "celo-foundry-8/Test.sol"; +import { TestConstants } from "@test-sol/constants.sol"; + +contract Utils08 is TestConstants { + uint256 public constant secondsInOneBlock = 5; + + function timeTravel(Vm vm, uint256 timeDelta) public { + vm.warp(block.timestamp + timeDelta); + } + + function blockTravel(Vm vm, uint256 blockDelta) public { + vm.roll(block.number + blockDelta); + } + + function travelEpochL1(Vm vm) public { + uint256 blocksInEpoch = 17280; + uint256 timeDelta = blocksInEpoch * 5; + blockTravel(vm, blocksInEpoch); + timeTravel(vm, timeDelta); + } + + // XXX: this function only increases the block number and timestamp, but does not actually change epoch. + // XXX: you must start and finish epoch processing to change epochs. + function travelNL2Epoch(Vm vm, uint256 n) public { + uint256 blocksInEpoch = L2_BLOCK_IN_EPOCH; + blockTravel(vm, n * blocksInEpoch); + timeTravel(vm, n * DAY); + } + + function whenL2(Vm vm) public { + vm.etch(0x4200000000000000000000000000000000000018, abi.encodePacked(bytes1(0x01))); + } + + function actorWithPK(Vm vm, string memory name) public returns (address, uint256) { + uint256 pk = uint256(keccak256(bytes(name))); + address addr = vm.addr(pk); + vm.label(addr, name); + return (addr, pk); + } + + // This function can be also found in OpenZeppelin's library, but in a newer version than the one we use. + function compareStrings(string memory a, string memory b) public pure returns (bool) { + return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b)))); + } +} diff --git a/packages/protocol/test/common/feehandlerseller.ts b/packages/protocol/test/common/feehandlerseller.ts deleted file mode 100644 index f0f47aa6e64..00000000000 --- a/packages/protocol/test/common/feehandlerseller.ts +++ /dev/null @@ -1,95 +0,0 @@ -// /* tslint:disable */ - -// TODO remove magic numbers -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { assertEqualBN, assertTransactionRevertWithReason } from '@celo/protocol/lib/test-utils' -import BigNumber from 'bignumber.js' -import { - GoldTokenContract, - GoldTokenInstance, - MentoFeeHandlerSellerContract, - MentoFeeHandlerSellerInstance, - RegistryContract, - RegistryInstance, - UniswapFeeHandlerSellerContract, - UniswapFeeHandlerSellerInstance, -} from 'types' - -const MentoFeeHandlerSeller: MentoFeeHandlerSellerContract = - artifacts.require('MentoFeeHandlerSeller') - -const UniswapFeeHandlerSeller: UniswapFeeHandlerSellerContract = - artifacts.require('UniswapFeeHandlerSeller') - -const GoldToken: GoldTokenContract = artifacts.require('GoldToken') -const Registry: RegistryContract = artifacts.require('Registry') - -const oneCelo = new BigNumber('1e18') - -contract('FeeHandlerSeller', (accounts: string[]) => { - let uniswapFeeHandlerSeller: UniswapFeeHandlerSellerInstance - let mentoFeeHandlerSeller: MentoFeeHandlerSellerInstance - let goldToken: GoldTokenInstance - let registry: RegistryInstance - - let contractsToTest - const user = accounts[1] - - beforeEach(async () => { - registry = await Registry.new(true) - - goldToken = await GoldToken.new(true) - await goldToken.initialize(registry.address) - await registry.setAddressFor(CeloContractName.GoldToken, goldToken.address) - - uniswapFeeHandlerSeller = await UniswapFeeHandlerSeller.new(true) - mentoFeeHandlerSeller = await MentoFeeHandlerSeller.new(true) - contractsToTest = [mentoFeeHandlerSeller, uniswapFeeHandlerSeller] - for (const contract of contractsToTest) { - await contract.initialize(registry.address, [], []) - } - }) - - describe('#transfer()', () => { - it(`transfer works`, async () => { - for (const contract of contractsToTest) { - const receiver = web3.eth.accounts.create().address - assertEqualBN(await goldToken.balanceOf(receiver), new BigNumber(0)) - - await goldToken.transfer(contract.address, oneCelo) - await contract.transfer(goldToken.address, oneCelo, receiver) - assertEqualBN(await goldToken.balanceOf(receiver), oneCelo) - assertEqualBN(await goldToken.balanceOf(contract.address), new BigNumber(0)) - } - }) - - it('only owner can transfer', async () => { - const receiver = web3.eth.accounts.create().address - for (const contract of contractsToTest) { - await goldToken.transfer(contract.address, oneCelo) - await assertTransactionRevertWithReason( - contract.transfer(goldToken.address, oneCelo, receiver, { from: user }), - 'Ownable: caller is not the owner' - ) - } - }) - }) - - describe('#setMinimumReports()', () => { - it(`setMinimumReports works`, async () => { - for (const contract of contractsToTest) { - await contract.setMinimumReports(goldToken.address, 15) - assertEqualBN(await contract.minimumReports(goldToken.address), 15) - } - }) - - it('only owner can setMinimumReports', async () => { - for (const contract of contractsToTest) { - await assertTransactionRevertWithReason( - contract.setMinimumReports(goldToken.address, 1, { from: user }), - 'Ownable: caller is not the owner.' - ) - } - }) - }) -}) diff --git a/packages/protocol/test/common/recoverFunds.ts b/packages/protocol/test/common/recoverFunds.ts index 496b8c1477b..7278ab72d60 100644 --- a/packages/protocol/test/common/recoverFunds.ts +++ b/packages/protocol/test/common/recoverFunds.ts @@ -3,6 +3,7 @@ import { recoverFunds } from '@celo/protocol/lib/recover-funds' import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { expectBigNumberInRange } from '@celo/protocol/lib/test-utils' +import { CeloUnreleasedTreasuryContract } from '@celo/protocol/types/08' import { BigNumber } from 'bignumber.js' import { FreezerContract, @@ -11,6 +12,8 @@ import { ProxyInstance, RegistryContract, } from 'types' +import { SOLIDITY_08_PACKAGE } from '../../contractPackages' +import { ArtifactsSingleton } from '../../lib/artifactsSingleton' const GetSetV0: Truffle.Contract = artifacts.require('GetSetV0') const Proxy: Truffle.Contract = artifacts.require('Proxy') @@ -34,14 +37,22 @@ contract('Proxy', (accounts: string[]) => { it('recovers funds from an incorrectly intialized implementation', async () => { const Freezer: FreezerContract = artifacts.require('Freezer') const GoldToken: GoldTokenContract = artifacts.require('GoldToken') + const CeloUnreleasedTreasury: CeloUnreleasedTreasuryContract = + ArtifactsSingleton.getInstance(SOLIDITY_08_PACKAGE).require('CeloUnreleasedTreasury') // Added because the CeloToken `_transfer` prevents transfers to the celoUnreleasedTreasury. // @ts-ignore GoldToken.numberFormat = 'BigNumber' const Registry: RegistryContract = artifacts.require('Registry') const freezer = await Freezer.new(true) const goldToken = await GoldToken.new(true) + const celoUnreleasedTreasury = await CeloUnreleasedTreasury.new(true) + const registry = await Registry.new(true) await registry.setAddressFor(CeloContractName.Freezer, freezer.address) + await registry.setAddressFor( + CeloContractName.CeloUnreleasedTreasury, + celoUnreleasedTreasury.address + ) await goldToken.initialize(registry.address) const amount = new BigNumber(10) diff --git a/packages/protocol/test/common/uniswapfeehandlerseller.ts b/packages/protocol/test/common/uniswapfeehandlerseller.ts deleted file mode 100644 index 3f410125f51..00000000000 --- a/packages/protocol/test/common/uniswapfeehandlerseller.ts +++ /dev/null @@ -1,103 +0,0 @@ -// /* eslint:disable */ - -// TODO remove magic numbers -import { CeloContractName } from '@celo/protocol/lib/registry-utils' -import { assertTransactionRevertWithReason } from '@celo/protocol/lib/test-utils' -import { - GoldTokenContract, - GoldTokenInstance, - RegistryContract, - RegistryInstance, - UniswapFeeHandlerSellerContract, - UniswapFeeHandlerSellerInstance, -} from 'types' - -const UniswapFeeHandlerSeller: UniswapFeeHandlerSellerContract = - artifacts.require('UniswapFeeHandlerSeller') - -const GoldToken: GoldTokenContract = artifacts.require('GoldToken') -const Registry: RegistryContract = artifacts.require('Registry') - -contract('UniswapFeeHandlerSeller', (accounts: string[]) => { - let uniswapFeeHandlerSeller: UniswapFeeHandlerSellerInstance - let goldToken: GoldTokenInstance - let registry: RegistryInstance - - const addressA = '0xFA907Ed32fC6Ca20408214C0DC8734403738AbDb' - const addressB = '0x61eb0a82C8802090b61381853e7Ec34b985e9b85' - const addressC = '0xb7c771B22A983e19fE7aCAB574F7dF5A6C65cAB1' - const addressD = '0x866284bd3946882CFc23e2F14942f5c293fb5742' - - const user = accounts[1] - - beforeEach(async () => { - registry = await Registry.new(true) - - goldToken = await GoldToken.new(true) - await goldToken.initialize(registry.address) - await registry.setAddressFor(CeloContractName.GoldToken, goldToken.address) - - uniswapFeeHandlerSeller = await UniswapFeeHandlerSeller.new(true) - }) - - describe('#setRouter()', () => { - it('sets pool for exchange', async () => { - await uniswapFeeHandlerSeller.setRouter(addressA, addressB) - assert((await uniswapFeeHandlerSeller.getRoutersForToken(addressA))[0], addressB) - }) - - it('only owner can setRouter', async () => { - await assertTransactionRevertWithReason( - uniswapFeeHandlerSeller.setRouter(addressA, addressB, { from: user }), - 'Ownable: caller is not the owner' - ) - }) - - it("Can't set address zero", async () => { - await assertTransactionRevertWithReason( - uniswapFeeHandlerSeller.setRouter(addressA, '0x0000000000000000000000000000000000000000'), - "Router can't be address zero." - ) - }) - - it("Can't add more than 4 routers", async () => { - await uniswapFeeHandlerSeller.setRouter(addressA, addressB) - await uniswapFeeHandlerSeller.setRouter(addressA, accounts[1]) - await uniswapFeeHandlerSeller.setRouter(addressA, accounts[2]) - await assertTransactionRevertWithReason( - uniswapFeeHandlerSeller.setRouter(addressA, accounts[4]), - 'Max number of routers reached.' - ) - }) - }) - - describe('#removeRouter()', () => { - beforeEach(async () => { - await uniswapFeeHandlerSeller.setRouter(addressA, addressB) - }) - - it('removes a token', async () => { - await uniswapFeeHandlerSeller.removeRouter(addressA, addressB) - expect((await uniswapFeeHandlerSeller.getRoutersForToken(addressA)).toString()).to.equal( - [].toString() - ) - }) - - it('removes when list is big', async () => { - await uniswapFeeHandlerSeller.setRouter(addressA, addressD) - await uniswapFeeHandlerSeller.setRouter(addressA, addressC) - // list for token should be [uniswap, exchange, stabletoken] - await uniswapFeeHandlerSeller.removeRouter(addressA, addressD) - expect((await uniswapFeeHandlerSeller.getRoutersForToken(addressA)).toString()).to.equal( - [addressB, addressC].toString() - ) - }) - - it('only owner can removeRouter', async () => { - await assertTransactionRevertWithReason( - uniswapFeeHandlerSeller.removeRouter(addressA, addressB, { from: user }), - 'Ownable: caller is not the owner.' - ) - }) - }) -}) diff --git a/packages/protocol/truffle-config-parent.js b/packages/protocol/truffle-config-parent.js index aae2d8e8980..3cb57cfa781 100644 --- a/packages/protocol/truffle-config-parent.js +++ b/packages/protocol/truffle-config-parent.js @@ -23,15 +23,13 @@ const INTEGRATION_FROM = '0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95' const INTEGRATION_TESTING_FROM = '0x47e172F6CfB6c7D01C1574fa3E2Be7CC73269D95' const ALFAJORESSTAGING_FROM = '0xf4314cb9046bece6aa54bb9533155434d0c76909' const ALFAJORES_FROM = '0x456f41406B32c45D59E539e4BBA3D7898c3584dA' -const PILOT_FROM = '0x387bCb16Bfcd37AccEcF5c9eB2938E30d3aB8BF2' -const PILOTSTAGING_FROM = '0x545DEBe3030B570731EDab192640804AC8Cf65CA' const RC0_FROM = '0x469be98FE71AFf8F6e7f64F9b732e28A03596B5C' const BAKLAVA_FROM = '0x0Cc59Ed03B3e763c02d54D695FFE353055f1502D' const BAKLAVASTAGING_FROM = '0x4588ABb84e1BBEFc2BcF4b2296F785fB7AD9F285' const STAGING_FROM = '0x4e3d385ecdee402da395a3b18575b05cc5e8ff21' const CANNOLI_FROM = '0x8C174E896A85E487aa895865657b78Ea64879dC7' // validator zero -const gasLimit = 13000000 +const gasLimit = 20000000 const hostAddress = process.env.CELO_NODE_ADDRESS || '127.0.0.1' const hostPort = parseInt(process.env.CELO_NODE_PORT || '8545') @@ -139,7 +137,11 @@ const networks = { }, }, testnet_prod: defaultConfig, - + anvil: { + ...defaultConfig, + network_id: 31337, + from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + }, // New testnets integration: { ...defaultConfig, @@ -150,9 +152,6 @@ const networks = { from: INTEGRATION_TESTING_FROM, network_id: 1101, }, - argentinastaging: freeGasConfig, - argentinaproduction: freeGasConfig, - alfajoresstaging: { ...defaultConfig, from: ALFAJORESSTAGING_FROM, @@ -170,14 +169,6 @@ const networks = { from: CANNOLI_FROM, }, - pilot: { - ...defaultConfig, - from: PILOT_FROM, - }, - pilotstaging: { - ...defaultConfig, - from: PILOTSTAGING_FROM, - }, baklava: { ...defaultConfig, from: BAKLAVA_FROM, diff --git a/packages/protocol/truffle-config0.8.js b/packages/protocol/truffle-config0.8.js index da81a714ffc..2c40deae146 100644 --- a/packages/protocol/truffle-config0.8.js +++ b/packages/protocol/truffle-config0.8.js @@ -16,6 +16,10 @@ module.exports = { version: SOLC_VERSION, settings: { metadata: { useLiteralContent: true }, + optimizer: { + enabled: true, + runs: 200, + }, }, }, }, diff --git a/remappings.txt b/remappings.txt index e6c8c6ef51b..136032d5144 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,4 +4,5 @@ forge-std-8/=packages/protocol/lib/celo-foundry-8/lib/forge-std/src/ celo-foundry/=packages/protocol/lib/celo-foundry/src/ openzeppelin-solidity/=packages/protocol/lib/openzeppelin-contracts/ solidity-bytes-utils/=packages/protocol/lib/solidity-bytes-utils/ +solidity-bytes-utils-8/=packages/protocol/lib/solidity-bytes-utils-8/ contracts=packages/protocol/contracts/ diff --git a/yarn.lock b/yarn.lock index fbfd28c377e..26e6e685b49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8884,6 +8884,10 @@ drbg.js@^1.0.1: create-hash "^1.1.2" create-hmac "^1.1.4" +"ds-test@github:dapphub/ds-test": + version "1.0.0" + resolved "https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0" + dtrace-provider@~0.8: version "0.8.8" resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" @@ -10715,6 +10719,11 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== +forge-std@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/forge-std/-/forge-std-1.1.2.tgz#f4a0eda103538d56f9c563f3cd1fa2fd01bd9378" + integrity sha512-Wfb0iAS9PcfjMKtGpWQw9mXzJxrWD62kJCUqqLcyuI0+VRtJ3j20XembjF3kS20qELYdXft1vD/SPFVWVKMFOw== + form-data-encoder@1.7.1: version "1.7.1" resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.1.tgz#ac80660e4f87ee0d3d3c3638b7da8278ddb8ec96" @@ -18932,6 +18941,14 @@ solhint@^4.5.4: optionalDependencies: prettier "^2.8.3" +"solidity-bytes-utils-8@npm:solidity-bytes-utils@^0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/solidity-bytes-utils/-/solidity-bytes-utils-0.8.2.tgz#763d6a02fd093e93b3a97b742e97d540e66c29bd" + integrity sha512-cqXPYAV2auhpdKSTPuqji0CwpSceZDu95CzqSM/9tDJ2MoMaMsdHTpOIWtVw31BIqqGPNmIChCswzbw0tHaMTw== + dependencies: + ds-test "github:dapphub/ds-test" + forge-std "^1.1.2" + solidity-bytes-utils@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/solidity-bytes-utils/-/solidity-bytes-utils-0.0.7.tgz#ccc865a6694b4865f2020cee37c15cc26f81cf9b" @@ -19160,7 +19177,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -19186,15 +19203,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -19286,7 +19294,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -19314,13 +19322,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -22287,7 +22288,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22313,15 +22314,6 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"