From 386791a576bae306e6256da219c0491d5073946f Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Thu, 14 Mar 2024 16:43:42 -0400 Subject: [PATCH] feat: angle-core project --- .env.example | 14 + .github/assets/logo.svg | 14 + .github/workflows/ci-deep.yml | 177 +++ .github/workflows/ci.yml | 232 ++++ .gitignore | 38 + .gitmodules | 12 + .prettierignore | 3 + .prettierrc | 12 + .solhint.json | 28 + .solhintignore | 12 + .vscode/settings.json | 7 + .vscode/tasks.json | 20 + CODE_OF_CONDUCT.md | 128 +++ CONTRIBUTING.md | 9 + LICENSE | 674 +++++++++++ README.md | 167 ++- contracts/dao/ANGLE.sol | 60 + contracts/dao/veANGLE.vy | 710 ++++++++++++ contracts/external/AccessControl.sol | 233 ++++ .../external/AccessControlUpgradeable.sol | 241 ++++ contracts/external/FullMath.sol | 112 ++ contracts/external/ProxyAdmin.sol | 81 ++ contracts/external/SmartWalletChecker.sol | 97 ++ .../external/TransparentUpgradeableProxy.sol | 125 +++ contracts/interfaces/IAccessControl.sol | 18 + contracts/interfaces/IAgToken.sol | 27 + contracts/interfaces/IAngleMiddlemanGauge.sol | 10 + contracts/interfaces/IBondingCurve.sol | 23 + contracts/interfaces/ICollateralSettler.sol | 14 + contracts/interfaces/ICore.sol | 26 + contracts/interfaces/ICoreBorrow.sol | 28 + contracts/interfaces/IERC721.sol | 46 + contracts/interfaces/IFeeDistributor.sol | 21 + contracts/interfaces/IFeeManager.sol | 42 + contracts/interfaces/IGaugeController.sol | 14 + contracts/interfaces/IGenericLender.sol | 35 + contracts/interfaces/IKeeperRegistry.sol | 14 + contracts/interfaces/ILiquidityGauge.sol | 24 + contracts/interfaces/IOracle.sol | 23 + contracts/interfaces/IPerpetualManager.sol | 112 ++ contracts/interfaces/IPoolManager.sol | 84 ++ contracts/interfaces/IRewardsDistributor.sol | 52 + contracts/interfaces/ISanToken.sol | 27 + contracts/interfaces/IStableMaster.sol | 168 +++ contracts/interfaces/IStableMasterFront.sol | 40 + contracts/interfaces/IStakingRewards.sol | 27 + contracts/interfaces/IStrategy.sol | 32 + contracts/interfaces/ITimelock.sol | 44 + contracts/interfaces/ITreasury.sol | 16 + contracts/interfaces/IVaultManager.sol | 127 +++ contracts/interfaces/IVeANGLE.sol | 13 + contracts/interfaces/IWETH.sol | 12 + contracts/interfaces/external/IWETH9.sol | 13 + .../interfaces/external/aave/DataTypes.sol | 44 + contracts/interfaces/external/aave/IAave.sol | 124 ++ .../interfaces/external/compound/CErc20I.sol | 15 + .../interfaces/external/compound/CEtherI.sol | 13 + .../interfaces/external/compound/CTokenI.sol | 26 + .../external/compound/IComptroller.sol | 16 + .../external/compound/InterestRateModel.sol | 27 + contracts/interfaces/external/curve/Curve.sol | 60 + contracts/interfaces/external/lido/ISteth.sol | 13 + .../interfaces/external/lido/IWStETH.sol | 16 + .../external/uniswap/IUniswapRouter.sol | 38 + contracts/staking/AngleDistributor.sol | 406 +++++++ contracts/staking/AngleDistributorEvents.sol | 29 + contracts/staking/GaugeController.vy | 605 ++++++++++ contracts/staking/LiquidityGaugeV4.vy | 633 +++++++++++ .../staking/LiquidityGaugeV4UpgradedToken.vy | 670 +++++++++++ contracts/staking/RewardsDistributor.sol | 328 ++++++ .../staking/RewardsDistributorEvents.sol | 34 + contracts/staking/StakingRewards.sol | 284 +++++ contracts/staking/StakingRewardsEvents.sol | 31 + contracts/staking/veBoost.vy | 1000 +++++++++++++++++ contracts/staking/veBoostProxy.vy | 117 ++ contracts/utils/VyperDeployer.sol | 115 ++ foundry.toml | 73 ++ helpers/fork.sh | 37 + lib/forge-std | 1 + lib/openzeppelin-contracts | 1 + lib/openzeppelin-contracts-upgradeable | 1 + lib/utils | 1 + package.json | 42 + remappings.txt | 5 + scripts/BasicScript.s.sol | 20 + scripts/Simulate.s.sol | 22 + scripts/Utils.s.sol | 8 + slither.config.json | 10 + test/fuzz/.gitkeep | 0 test/invariant/.gitkeep | 0 test/unit/.gitkeep | 0 utils/forwardUtils.js | 22 + yarn.lock | 505 +++++++++ 93 files changed, 9729 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .github/assets/logo.svg create mode 100644 .github/workflows/ci-deep.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .solhint.json create mode 100644 .solhintignore create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 contracts/dao/ANGLE.sol create mode 100644 contracts/dao/veANGLE.vy create mode 100644 contracts/external/AccessControl.sol create mode 100644 contracts/external/AccessControlUpgradeable.sol create mode 100644 contracts/external/FullMath.sol create mode 100644 contracts/external/ProxyAdmin.sol create mode 100644 contracts/external/SmartWalletChecker.sol create mode 100644 contracts/external/TransparentUpgradeableProxy.sol create mode 100644 contracts/interfaces/IAccessControl.sol create mode 100644 contracts/interfaces/IAgToken.sol create mode 100644 contracts/interfaces/IAngleMiddlemanGauge.sol create mode 100644 contracts/interfaces/IBondingCurve.sol create mode 100644 contracts/interfaces/ICollateralSettler.sol create mode 100644 contracts/interfaces/ICore.sol create mode 100644 contracts/interfaces/ICoreBorrow.sol create mode 100644 contracts/interfaces/IERC721.sol create mode 100644 contracts/interfaces/IFeeDistributor.sol create mode 100644 contracts/interfaces/IFeeManager.sol create mode 100644 contracts/interfaces/IGaugeController.sol create mode 100644 contracts/interfaces/IGenericLender.sol create mode 100644 contracts/interfaces/IKeeperRegistry.sol create mode 100644 contracts/interfaces/ILiquidityGauge.sol create mode 100644 contracts/interfaces/IOracle.sol create mode 100644 contracts/interfaces/IPerpetualManager.sol create mode 100644 contracts/interfaces/IPoolManager.sol create mode 100644 contracts/interfaces/IRewardsDistributor.sol create mode 100644 contracts/interfaces/ISanToken.sol create mode 100644 contracts/interfaces/IStableMaster.sol create mode 100644 contracts/interfaces/IStableMasterFront.sol create mode 100644 contracts/interfaces/IStakingRewards.sol create mode 100644 contracts/interfaces/IStrategy.sol create mode 100644 contracts/interfaces/ITimelock.sol create mode 100644 contracts/interfaces/ITreasury.sol create mode 100644 contracts/interfaces/IVaultManager.sol create mode 100644 contracts/interfaces/IVeANGLE.sol create mode 100644 contracts/interfaces/IWETH.sol create mode 100644 contracts/interfaces/external/IWETH9.sol create mode 100644 contracts/interfaces/external/aave/DataTypes.sol create mode 100644 contracts/interfaces/external/aave/IAave.sol create mode 100755 contracts/interfaces/external/compound/CErc20I.sol create mode 100644 contracts/interfaces/external/compound/CEtherI.sol create mode 100644 contracts/interfaces/external/compound/CTokenI.sol create mode 100644 contracts/interfaces/external/compound/IComptroller.sol create mode 100755 contracts/interfaces/external/compound/InterestRateModel.sol create mode 100644 contracts/interfaces/external/curve/Curve.sol create mode 100644 contracts/interfaces/external/lido/ISteth.sol create mode 100644 contracts/interfaces/external/lido/IWStETH.sol create mode 100644 contracts/interfaces/external/uniswap/IUniswapRouter.sol create mode 100644 contracts/staking/AngleDistributor.sol create mode 100644 contracts/staking/AngleDistributorEvents.sol create mode 100644 contracts/staking/GaugeController.vy create mode 100644 contracts/staking/LiquidityGaugeV4.vy create mode 100644 contracts/staking/LiquidityGaugeV4UpgradedToken.vy create mode 100644 contracts/staking/RewardsDistributor.sol create mode 100644 contracts/staking/RewardsDistributorEvents.sol create mode 100644 contracts/staking/StakingRewards.sol create mode 100644 contracts/staking/StakingRewardsEvents.sol create mode 100644 contracts/staking/veBoost.vy create mode 100644 contracts/staking/veBoostProxy.vy create mode 100644 contracts/utils/VyperDeployer.sol create mode 100644 foundry.toml create mode 100644 helpers/fork.sh create mode 160000 lib/forge-std create mode 160000 lib/openzeppelin-contracts create mode 160000 lib/openzeppelin-contracts-upgradeable create mode 160000 lib/utils create mode 100644 package.json create mode 100644 remappings.txt create mode 100644 scripts/BasicScript.s.sol create mode 100644 scripts/Simulate.s.sol create mode 100644 scripts/Utils.s.sol create mode 100644 slither.config.json create mode 100644 test/fuzz/.gitkeep create mode 100644 test/invariant/.gitkeep create mode 100644 test/unit/.gitkeep create mode 100644 utils/forwardUtils.js create mode 100644 yarn.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2620949 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +## Add URI and BIP39 mnemonic, and etherscan API key for every network that you plan to use +## These are used in `foundry.toml` (see `nodeUrl` and `accounts` calls in network definitions) + +#ETH_NODE_URI_MAINNET="" +#MNEMONIC_MAINNET="" +#MAINNET_ETHERSCAN_API_KEY="" + +#ETH_NODE_URI_POLYGON="" +#MNEMONIC_POLYGON="" +#POLYGON_ETHERSCAN_API_KEY="" + +#ETH_NODE_URI_FORK=http://localhost:8545 +#MNEMONIC_FORK="" + diff --git a/.github/assets/logo.svg b/.github/assets/logo.svg new file mode 100644 index 0000000..ccd539a --- /dev/null +++ b/.github/assets/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/.github/workflows/ci-deep.yml b/.github/workflows/ci-deep.yml new file mode 100644 index 0000000..a75ce1b --- /dev/null +++ b/.github/workflows/ci-deep.yml @@ -0,0 +1,177 @@ +name: "CI Deep" + +env: + FOUNDRY_PROFILE: "ci" + +on: + schedule: + - cron: "0 3 * * 0" # at 3:00am UTC every Sunday + workflow_dispatch: + inputs: + fuzzRuns: + default: "10000" + description: "Unit: number of fuzz runs." + required: false + invariantRuns: + default: "300" + description: "Unit: number of invariant runs." + required: false + invariantDepth: + default: "50" + description: "Unit: invariant depth." + required: false + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: "yarn" + + - name: Install dependencies + run: yarn install + + - name: Run solhint + run: yarn lint:check + + - name: "Add lint summary" + run: | + echo "## Lint result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Compile foundry + run: yarn compile --sizes + + - name: "Cache the build so that it can be re-used by the other jobs" + uses: "actions/cache/save@v3" + with: + key: "build-${{ github.sha }}" + path: | + cache-forge + out + + - name: "Add build summary" + run: | + echo "## Build result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + test-unit: + needs: ["build", "lint"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - uses: actions/cache/restore@v3 + with: + fail-on-cache-miss: true + path: | + cache-forge + out + key: "build-${{ github.sha }}" + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Foundry tests + run: yarn test:unit + env: + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} + + - name: "Add Unit Test Summary" + run: | + echo "## Unit test result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + test-invariant: + needs: ["build", "lint"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - uses: actions/cache/restore@v3 + with: + fail-on-cache-miss: true + path: | + cache-forge + out + key: "build-${{ github.sha }}" + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Foundry tests + run: yarn test:invariant + env: + FOUNDRY_INVARIANT_RUNS: ${{ inputs.invariantRuns || '300' }} + FOUNDRY_INVARIANT_DEPTH: ${{ inputs.invariantDepth || '50' }} + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} + + - name: "Add Invariant Test Summary" + run: | + echo "## Invariant test result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + test-fuzz: + needs: ["build", "lint"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - uses: actions/cache/restore@v3 + with: + fail-on-cache-miss: true + path: | + cache-forge + out + key: "build-${{ github.sha }}" + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Foundry tests + run: yarn test:fuzz + env: + FOUNDRY_FUZZ_RUNS: ${{ inputs.fuzzRuns || '10000' }} + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} + + - name: "Add Fuzz Test Summary" + run: | + echo "## Fuzz test result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..20fef55 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,232 @@ +name: "CI" + +env: + FOUNDRY_PROFILE: "ci" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - "main" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: "yarn" + + - name: Install dependencies + run: yarn install + + - name: Run solhint + run: yarn lint:check + + - name: "Add lint summary" + run: | + echo "## Lint result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Compile foundry + run: yarn compile --sizes + + - name: "Cache the build so that it can be re-used by the other jobs" + uses: "actions/cache/save@v3" + with: + key: "build-${{ github.sha }}" + path: | + cache-forge + out + + - name: "Add build summary" + run: | + echo "## Build result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + test-unit: + needs: ["build", "lint"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - uses: actions/cache/restore@v3 + with: + fail-on-cache-miss: true + path: | + cache-forge + out + key: "build-${{ github.sha }}" + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Foundry tests + run: yarn test:unit + env: + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} + + - name: "Add Unit Test Summary" + run: | + echo "## Unit test result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + test-invariant: + needs: ["build", "lint"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - uses: actions/cache/restore@v3 + with: + fail-on-cache-miss: true + path: | + cache-forge + out + key: "build-${{ github.sha }}" + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Foundry tests + run: yarn test:invariant + env: + FOUNDRY_INVARIANT_RUNS: "8" + FOUNDRY_INVARIANT_DEPTH: "256" + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} + + - name: "Add Invariant Test Summary" + run: | + echo "## Invariant test result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + test-fuzz: + needs: ["build", "lint"] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - uses: actions/cache/restore@v3 + with: + fail-on-cache-miss: true + path: | + cache-forge + out + key: "build-${{ github.sha }}" + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Foundry tests + run: npm run test:fuzz + env: + FOUNDRY_FUZZ_RUNS: "5000" + ETH_NODE_URI_POLYGON: ${{ secrets.ETH_NODE_URI_POLYGON }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} + + - name: "Add Fuzz Test Summary" + run: | + echo "## Fuzz test result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + coverage: + needs: ["build", "lint"] + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install lcov" + run: "sudo apt-get install lcov" + + - name: "Generate the coverage report using the unit and the integration tests" + run: "yarn ci:coverage" + + - name: "Upload coverage report to Codecov" + uses: "codecov/codecov-action@v3" + with: + files: "./lcov.info" + token: ${{ secrets.CODECOV_TOKEN }} + + - name: "Add coverage summary" + run: | + echo "## Coverage result" >> $GITHUB_STEP_SUMMARY + echo "✅ Uploaded to Codecov" >> $GITHUB_STEP_SUMMARY + + slither-analyze: + needs: ["build", "lint"] + runs-on: "ubuntu-latest" + permissions: + actions: "read" + contents: "read" + security-events: "write" + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Compile foundry + run: forge build --build-info --force + + - name: "Run Slither analysis" + uses: "crytic/slither-action@v0.3.0" + id: slither + with: + fail-on: "none" + sarif: "results.sarif" + ignore-compile: true + + - name: "Upload SARIF file to GitHub code scanning" + uses: "github/codeql-action/upload-sarif@v2" + with: + sarif_file: ${{ steps.slither.outputs.sarif }} + + - name: "Add Slither summary" + run: | + echo "## Slither result" >> $GITHUB_STEP_SUMMARY + echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc31e42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +## Defaults +__pycache__ +.idea +.DS_Store +.deps +.docs +.env +node_modules +venv + +# Build output +slither-audit.txt +slither + +# Test output +coverage +coverage.json +lcov.info + +# Running output +gas-report.txt +gasReporterOutput.json +addresses.json +blockchain_db +yarn-error.log +broadcast + +# deployments +deployments/localhost +deployments/mainnetForkRemote + +# bin +bin + + +# foundry +/out +/cache diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dcf0d19 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/utils"] + path = lib/utils + url = https://github.com/AngleProtocol/utils diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e4006e6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +lib +cache +out \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..295b1da --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "overrides": [ + { + "files": "*.sol", + "options": { + "printWidth": 120, + "singleQuote": false, + "bracketSpacing": true + } + } + ] +} diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..99c3260 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,28 @@ +{ + "extends": "solhint:recommended", + "plugins": ["prettier"], + "rules": { + "max-line-length": ["error", 120], + "avoid-call-value": "warn", + "avoid-low-level-calls": "off", + "avoid-tx-origin": "warn", + "const-name-snakecase": "warn", + "contract-name-camelcase": "warn", + "imports-on-top": "warn", + "prettier/prettier": "error", + "ordering": "off", + "max-states-count": "off", + "mark-callable-contracts": "off", + "no-empty-blocks": "off", + "no-global-import": "off", + "not-rely-on-time": "off", + "compiler-version": "off", + "private-vars-leading-underscore": "warn", + "reentrancy": "warn", + "no-inline-assembly": "off", + "no-complex-fallback": "off", + "reason-string": "off", + "func-visibility": ["warn", { "ignoreConstructors": true }], + "explicit-types": ["error","explicit"] + } +} diff --git a/.solhintignore b/.solhintignore new file mode 100644 index 0000000..6a51c68 --- /dev/null +++ b/.solhintignore @@ -0,0 +1,12 @@ +# Doesn't need to lint dev files +lib +scripts +test + +# Doesn't need to lint build files +node_modules +cache-forge +out + +# Doesn't need to lint utils +external \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e6b354a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "solidity.compileUsingRemoteVersion": "0.8.20", + "[solidity]": { + "editor.defaultFormatter": "JuanBlanco.solidity", + "editor.formatOnSave": true + }, +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..216fc85 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,20 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Generate Header", + "type": "shell", + "command": "headers ${input:header}", + "presentation": { + "reveal": "never" + } + } + ], + "inputs": [ + { + "id": "header", + "description": "Header", + "type": "promptString" + } + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ef8ff11 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +contact@angle.money. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..de7db21 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Submit a change or a new feature + +First of all thank you for your interest in this repository! + +This is only the beginning of the Angle protocol and codebase, and anyone is welcome to improve it. + +To submit some code, please work in a fork, reach out to explain what you've done and open a Pull Request from your fork. + +Feel free to reach out in the [#developers channel](https://discord.gg/HcRB8QMeKU) of our Discord Server if you need a hand! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index bb6a57e..07aa577 100644 --- a/README.md +++ b/README.md @@ -1 +1,166 @@ -# angle-dao \ No newline at end of file +# Angle Angle DAO + +[![CI](https://github.com/AngleProtocol/boilerplate/actions/workflows/ci.yml/badge.svg)](https://github.com/AngleProtocol/boilerplate/actions) +[![Coverage](https://codecov.io/gh/AngleProtocol/boilerplate/branch/main/graph/badge.svg)](https://codecov.io/gh/AngleProtocol/boilerplate) + +This repository proposes a template that is based on foundry frameworks. It also provides templates for EVM compatible smart contracts (in `./contracts/example`), tests and deployment scripts. + +## Starting + +### Install packages + +You can install all dependencies by running + +```bash +yarn +forge i +``` + +### Create `.env` file + +In order to interact with non local networks, you must create an `.env` that has: + +- `PRIVATE_KEY` +- `MNEMONIC` +- network key (eg. `ALCHEMY_NETWORK_KEY`) +- `ETHERSCAN_API_KEY` + +For additional keys, you can check the `.env.example` file. + +Warning: always keep your confidential information safe. + +## Headers + +To automatically create headers, follow: + +## Hardhat Command line completion + +Follow these instructions to have hardhat command line arguments completion: + +## Foundry Installation + +```bash +curl -L https://foundry.paradigm.xyz | bash + +source /root/.zshrc +# or, if you're under bash: source /root/.bashrc + +foundryup +``` + +To install the standard library: + +```bash +forge install foundry-rs/forge-std +``` + +To update libraries: + +```bash +forge update +``` + +### Foundry on Docker 🐳 + +**If you don’t want to install Rust and Foundry on your computer, you can use Docker** +Image is available here [ghcr.io/foundry-rs/foundry](http://ghcr.io/foundry-rs/foundry). + +```bash +docker pull ghcr.io/foundry-rs/foundry +docker tag ghcr.io/foundry-rs/foundry:latest foundry:latest +``` + +To run the container: + +```bash +docker run -it --rm -v $(pwd):/app -w /app foundry sh +``` + +Then you are inside the container and can run Foundry’s commands. + +### Tests + +You can run tests as follows: + +```bash +forge test -vvvv --watch +forge test -vvvv --match-path contracts/forge-tests/KeeperMulticall.t.sol +forge test -vvvv --match-test "testAbc*" +forge test -vvvv --fork-url https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf +``` + +You can also list tests: + +```bash +forge test --list +forge test --list --json --match-test "testXXX*" +``` + +### Deploying + +There is an example script in the `scripts/foundry` folder. Then you can run: + +```bash +yarn foundry:deploy --rpc-url +``` + +Example: + +```bash +yarn foundry:deploy scripts/foundry/DeployMockAgEUR.s.sol --rpc-url goerli +``` + +### Coverage + +We recommend the use of this [vscode extension](ryanluker.vscode-coverage-gutters). + +```bash +yarn hardhat:coverage +yarn foundry:coverage +``` + +### Simulate + +You can simulate your transaction live or in fork mode. For both option you need to +complete the `scripts/foundry/Simulate.s.sol` with your values: address sending the tx, +address caled and the data to give to this address call. + +For live simulation + +```bash +yarn foundry:simulate +``` + +For fork simulation + +```bash +yarn foundry:fork +yarn foundry:simulate:fork +``` + +For fork simulation at a given block + +```bash +yarn foundry:fork:block ${XXXX} +yarn foundry:simulate:fork +``` + +### Gas report + +```bash +yarn foundry:gas +``` + +## Slither + +```bash +pip3 install slither-analyzer +pip3 install solc-select +solc-select install 0.8.11 +solc-select use 0.8.11 +slither . +``` + +## Media + +Don't hesitate to reach out on [Twitter](https://twitter.com/AngleProtocol) 🐦 diff --git a/contracts/dao/ANGLE.sol b/contracts/dao/ANGLE.sol new file mode 100644 index 0000000..6fc747c --- /dev/null +++ b/contracts/dao/ANGLE.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; + +/// @title ANGLE +/// @author Forked but improved from https://github.com/compound-finance/compound-protocol/tree/master/contracts/Governance +/// by Angle Core Team +/// @notice Governance token of Angle's protocol +contract ANGLE is ERC20Votes { + /// @notice An event that is emitted when the minter address is changed + event MinterChanged(address minter, address newMinter); + + /// @notice Minimum time between mints + uint32 public constant MINIMUM_BETWEEN_MINTS = 1 days * 30; + + /// @notice Cap on the percentage of `totalSupply()` that can be minted at each mint + uint8 public constant MAX_MINT = 1; + + /// @notice Address which may mint new tokens + address public minter; + + /// @notice The timestamp after which minting may occur + uint256 public mintingAllowedAfter; + + /// @notice Constructs a new ANGLE token + /// @param account Initial account to grant all the tokens to + /// @param minter_ Account with minting ability + constructor(address account, address minter_) ERC20Permit("ANGLE") ERC20("ANGLE", "ANGLE") { + require(account != address(0) && minter_ != address(0), "0"); + _mint(account, 1_000_000_000e18); // 1 billion ANGLE + minter = minter_; + emit MinterChanged(address(0), minter); + mintingAllowedAfter = block.timestamp; + } + + /// @notice Changes the minter address + /// @param minter_ Address of the new minter + function setMinter(address minter_) external { + require(msg.sender == minter, "67"); + require(minter_ != address(0), "0"); + emit MinterChanged(minter, minter_); + minter = minter_; + } + + /// @notice Mints new tokens + /// @param dst Address of the destination account + /// @param amount Number of tokens to be minted + function mint(address dst, uint256 amount) external { + require(msg.sender == minter, "68"); + require(block.timestamp >= mintingAllowedAfter, "69"); + require(amount <= (totalSupply() * MAX_MINT) / 100, "70"); + // Record the mint + mintingAllowedAfter = block.timestamp + MINIMUM_BETWEEN_MINTS; + + // Mint the amount + _mint(dst, amount); + } +} diff --git a/contracts/dao/veANGLE.vy b/contracts/dao/veANGLE.vy new file mode 100644 index 0000000..674f991 --- /dev/null +++ b/contracts/dao/veANGLE.vy @@ -0,0 +1,710 @@ +# @version 0.2.16 +""" +@title Voting Escrow +@author Angle Protocol +@license MIT +@notice Votes have a weight depending on time, so that users are + committed to the future of (whatever they are voting for) +@dev Vote weight decays linearly over time. Lock time cannot be + more than `MAXTIME` (4 years). +""" + +# Original idea and credit: +# Curve Finance's veCRV +# https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy +# veANGLE is a fork with only one view functions added to it to make veANGLE compatible +# with Compound governance system. The references to the controller have also been removed + +# Voting escrow to have time-weighted votes +# Votes have a weight depending on time, so that users are committed +# to the future of (whatever they are voting for). +# The weight in this implementation is linear, and lock cannot be more than maxtime: +# w ^ +# 1 + / +# | / +# | / +# | / +# |/ +# 0 +--------+------> time +# maxtime (4 years?) + +struct Point: + bias: int128 + slope: int128 # - dweight / dt + ts: uint256 + blk: uint256 # block +# We cannot really do block numbers per se b/c slope is per time, not per block +# and per block could be fairly bad b/c Ethereum changes blocktimes. +# What we can do is to extrapolate ***At functions + +struct LockedBalance: + amount: int128 + end: uint256 + + +interface ERC20: + def decimals() -> uint256: view + def name() -> String[64]: view + def symbol() -> String[32]: view + def transfer(to: address, amount: uint256) -> bool: nonpayable + def transferFrom(spender: address, to: address, amount: uint256) -> bool: nonpayable + + +# Interface for checking whether address belongs to a whitelisted +# type of a smart wallet. +# When new types are added - the whole contract is changed +# The check() method is modifying to be able to use caching +# for individual wallet addresses +interface SmartWalletChecker: + def check(addr: address) -> bool: nonpayable + +DEPOSIT_FOR_TYPE: constant(int128) = 0 +CREATE_LOCK_TYPE: constant(int128) = 1 +INCREASE_LOCK_AMOUNT: constant(int128) = 2 +INCREASE_UNLOCK_TIME: constant(int128) = 3 + + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event Deposit: + provider: indexed(address) + value: uint256 + locktime: indexed(uint256) + type: int128 + ts: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + ts: uint256 + +event Supply: + prevSupply: uint256 + supply: uint256 + + +WEEK: constant(uint256) = 7 * 86400 # all future times are rounded by week +MAXTIME: constant(uint256) = 4 * 365 * 86400 # 4 years +MULTIPLIER: constant(uint256) = 10 ** 18 + +token: public(address) +supply: public(uint256) + +locked: public(HashMap[address, LockedBalance]) + +epoch: public(uint256) +point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point +user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch] +user_point_epoch: public(HashMap[address, uint256]) +slope_changes: public(HashMap[uint256, int128]) # time -> signed slope change + +name: public(String[64]) +symbol: public(String[32]) +decimals: public(uint256) + +# Checker for whitelisted (smart contract) wallets which are allowed to deposit +# The goal is to prevent tokenizing the escrow +future_smart_wallet_checker: public(address) +smart_wallet_checker: public(address) + +admin: public(address) # Can and will be a smart contract +future_admin: public(address) + +initialized: public(bool) + + +@external +def __init__(): + """ + @notice Contract constructor + @dev The contract has an initializer to prevent the take over of the implementation + """ + assert self.initialized == False #dev: contract is already initialized + self.initialized = True + +@external +def initialize(_admin: address, token_addr: address, _smart_wallet_checker: address, _name: String[64], _symbol: String[32]): + """ + @notice Contract initializer + @param _admin Future veANGLE admin + @param token_addr `ERC20ANGLE` token address + @param _smart_wallet_checker Future smart wallet checker contract + @param _name Token name + @param _symbol Token symbol + """ + assert self.initialized == False #dev: contract is already initialized + self.initialized = True + assert _admin!= ZERO_ADDRESS #dev: admin cannot be the 0 address + self.admin = _admin + self.token = token_addr + self.smart_wallet_checker = _smart_wallet_checker + self.point_history[0].blk = block.number + self.point_history[0].ts = block.timestamp + + _decimals: uint256 = ERC20(token_addr).decimals() + assert _decimals <= 255 + self.decimals = _decimals + + self.name = _name + self.symbol = _symbol + + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of VotingEscrow contract to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + assert addr != ZERO_ADDRESS # dev: future admin cannot be the 0 address + self.future_admin = addr + log CommitOwnership(addr) + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + log ApplyOwnership(_admin) + +@external +def apply_transfer_ownership(): + """ + @notice Apply ownership transfer + """ + assert msg.sender == self.admin # dev: admin only + _admin: address = self.future_admin + assert _admin != ZERO_ADDRESS # dev: admin not set + self.admin = _admin + log ApplyOwnership(_admin) + + +@external +def commit_smart_wallet_checker(addr: address): + """ + @notice Set an external contract to check for approved smart contract wallets + @param addr Address of Smart contract checker + """ + assert msg.sender == self.admin + self.future_smart_wallet_checker = addr + + +@external +def apply_smart_wallet_checker(): + """ + @notice Apply setting external contract to check approved smart contract wallets + """ + assert msg.sender == self.admin + self.smart_wallet_checker = self.future_smart_wallet_checker + + +@internal +def assert_not_contract(addr: address): + """ + @notice Check if the call is from a whitelisted smart contract, revert if not + @param addr Address to be checked + """ + if addr != tx.origin: + checker: address = self.smart_wallet_checker + if checker != ZERO_ADDRESS: + if SmartWalletChecker(checker).check(addr): + return + raise "Smart contract depositors not allowed" + + +@external +@view +def get_last_user_slope(addr: address) -> int128: + """ + @notice Get the most recently recorded rate of voting power decrease for `addr` + @param addr Address of the user wallet + @return Value of the slope + """ + uepoch: uint256 = self.user_point_epoch[addr] + return self.user_point_history[addr][uepoch].slope + + +@external +@view +def user_point_history__ts(_addr: address, _idx: uint256) -> uint256: + """ + @notice Get the timestamp for checkpoint `_idx` for `_addr` + @param _addr User wallet address + @param _idx User epoch number + @return Epoch time of the checkpoint + """ + return self.user_point_history[_addr][_idx].ts + + +@external +@view +def locked__end(_addr: address) -> uint256: + """ + @notice Get timestamp when `_addr`'s lock finishes + @param _addr User wallet + @return Epoch time of the lock end + """ + return self.locked[_addr].end + + +@internal +def _checkpoint(addr: address, old_locked: LockedBalance, new_locked: LockedBalance): + """ + @notice Record global and per-user data to checkpoint + @param addr User's wallet address. No user checkpoint if 0x0 + @param old_locked Pevious locked amount / end lock time for the user + @param new_locked New locked amount / end lock time for the user + """ + u_old: Point = empty(Point) + u_new: Point = empty(Point) + old_dslope: int128 = 0 + new_dslope: int128 = 0 + _epoch: uint256 = self.epoch + + if addr != ZERO_ADDRESS: + # Calculate slopes and biases + # Kept at zero when they have to + if old_locked.end > block.timestamp and old_locked.amount > 0: + u_old.slope = old_locked.amount / MAXTIME + u_old.bias = u_old.slope * convert(old_locked.end - block.timestamp, int128) + if new_locked.end > block.timestamp and new_locked.amount > 0: + u_new.slope = new_locked.amount / MAXTIME + u_new.bias = u_new.slope * convert(new_locked.end - block.timestamp, int128) + + # Read values of scheduled changes in the slope + # old_locked.end can be in the past and in the future + # new_locked.end can ONLY by in the FUTURE unless everything expired: than zeros + old_dslope = self.slope_changes[old_locked.end] + if new_locked.end != 0: + if new_locked.end == old_locked.end: + new_dslope = old_dslope + else: + new_dslope = self.slope_changes[new_locked.end] + + last_point: Point = Point({bias: 0, slope: 0, ts: block.timestamp, blk: block.number}) + if _epoch > 0: + last_point = self.point_history[_epoch] + last_checkpoint: uint256 = last_point.ts + # initial_last_point is used for extrapolation to calculate block number + # (approximately, for *At methods) and save them + # as we cannot figure that out exactly from inside the contract + initial_last_point: Point = last_point + block_slope: uint256 = 0 # dblock/dt + if block.timestamp > last_point.ts: + block_slope = MULTIPLIER * (block.number - last_point.blk) / (block.timestamp - last_point.ts) + # If last point is already recorded in this block, slope=0 + # But that's ok b/c we know the block in such case + + # Go over weeks to fill history and calculate what the current point is + t_i: uint256 = (last_checkpoint / WEEK) * WEEK + for i in range(255): + # Hopefully it won't happen that this won't get used in 5 years! + # If it does, users will be able to withdraw but vote weight will be broken + t_i += WEEK + d_slope: int128 = 0 + if t_i > block.timestamp: + t_i = block.timestamp + else: + d_slope = self.slope_changes[t_i] + last_point.bias -= last_point.slope * convert(t_i - last_checkpoint, int128) + last_point.slope += d_slope + if last_point.bias < 0: # This can happen + last_point.bias = 0 + if last_point.slope < 0: # This cannot happen - just in case + last_point.slope = 0 + last_checkpoint = t_i + last_point.ts = t_i + last_point.blk = initial_last_point.blk + block_slope * (t_i - initial_last_point.ts) / MULTIPLIER + _epoch += 1 + if t_i == block.timestamp: + last_point.blk = block.number + break + else: + self.point_history[_epoch] = last_point + + self.epoch = _epoch + # Now point_history is filled until t=now + + if addr != ZERO_ADDRESS: + # If last point was in this block, the slope change has been applied already + # But in such case we have 0 slope(s) + last_point.slope += (u_new.slope - u_old.slope) + last_point.bias += (u_new.bias - u_old.bias) + if last_point.slope < 0: + last_point.slope = 0 + if last_point.bias < 0: + last_point.bias = 0 + + # Record the changed point into history + self.point_history[_epoch] = last_point + + if addr != ZERO_ADDRESS: + # Schedule the slope changes (slope is going down) + # We subtract new_user_slope from [new_locked.end] + # and add old_user_slope to [old_locked.end] + if old_locked.end > block.timestamp: + # old_dslope was - u_old.slope, so we cancel that + old_dslope += u_old.slope + if new_locked.end == old_locked.end: + old_dslope -= u_new.slope # It was a new deposit, not extension + self.slope_changes[old_locked.end] = old_dslope + + if new_locked.end > block.timestamp: + if new_locked.end > old_locked.end: + new_dslope -= u_new.slope # old slope disappeared at this point + self.slope_changes[new_locked.end] = new_dslope + # else: we recorded it already in old_dslope + + # Now handle user history + user_epoch: uint256 = self.user_point_epoch[addr] + 1 + + self.user_point_epoch[addr] = user_epoch + u_new.ts = block.timestamp + u_new.blk = block.number + self.user_point_history[addr][user_epoch] = u_new + + +@internal +def _deposit_for(_addr: address, _value: uint256, unlock_time: uint256, locked_balance: LockedBalance, type: int128, sender: address): + """ + @notice Deposit and lock tokens for a user + @param _addr User's wallet address + @param _value Amount to deposit + @param unlock_time New time when to unlock the tokens, or 0 if unchanged + @param locked_balance Previous locked amount / timestamp + """ + _locked: LockedBalance = locked_balance + supply_before: uint256 = self.supply + + self.supply = supply_before + _value + old_locked: LockedBalance = _locked + # Adding to existing lock, or if a lock is expired - creating a new one + _locked.amount += convert(_value, int128) + if unlock_time != 0: + _locked.end = unlock_time + self.locked[_addr] = _locked + + # Possibilities: + # Both old_locked.end could be current or expired (>/< block.timestamp) + # value == 0 (extend lock) or value > 0 (add to lock or extend lock) + # _locked.end > block.timestamp (always) + self._checkpoint(_addr, old_locked, _locked) + + if _value != 0: + assert ERC20(self.token).transferFrom(sender, self, _value) + + log Deposit(_addr, _value, _locked.end, type, block.timestamp) + log Supply(supply_before, supply_before + _value) + + +@external +def checkpoint(): + """ + @notice Record global data to checkpoint + """ + self._checkpoint(ZERO_ADDRESS, empty(LockedBalance), empty(LockedBalance)) + + +@external +@nonreentrant('lock') +def deposit_for(_addr: address, _value: uint256): + """ + @notice Deposit `_value` tokens for `_addr` and add to the lock + @dev Anyone (even a smart contract) can deposit for someone else, but + cannot extend their locktime and deposit for a brand new user + @param _addr User's wallet address + @param _value Amount to add to user's lock + """ + _locked: LockedBalance = self.locked[_addr] + + assert _value > 0 # dev: need non-zero value + assert _locked.amount > 0, "No existing lock found" + assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw" + + self._deposit_for(_addr, _value, 0, self.locked[_addr], DEPOSIT_FOR_TYPE, msg.sender) + + +@external +@nonreentrant('lock') +def create_lock(_value: uint256, _unlock_time: uint256): + """ + @notice Deposit `_value` tokens for `msg.sender` and lock until `_unlock_time` + @param _value Amount to deposit + @param _unlock_time Epoch time when tokens unlock, rounded down to whole weeks + """ + self.assert_not_contract(msg.sender) + unlock_time: uint256 = (_unlock_time / WEEK) * WEEK # Locktime is rounded down to weeks + _locked: LockedBalance = self.locked[msg.sender] + + assert _value > 0 # dev: need non-zero value + assert _locked.amount == 0, "Withdraw old tokens first" + assert unlock_time > block.timestamp, "Can only lock until time in the future" + assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max" + + self._deposit_for(msg.sender, _value, unlock_time, _locked, CREATE_LOCK_TYPE, msg.sender) + + +@external +@nonreentrant('lock') +def increase_amount(_value: uint256): + """ + @notice Deposit `_value` additional tokens for `msg.sender` + without modifying the unlock time + @param _value Amount of tokens to deposit and add to the lock + """ + self.assert_not_contract(msg.sender) + _locked: LockedBalance = self.locked[msg.sender] + + assert _value > 0 # dev: need non-zero value + assert _locked.amount > 0, "No existing lock found" + assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw" + + self._deposit_for(msg.sender, _value, 0, _locked, INCREASE_LOCK_AMOUNT, msg.sender) + + +@external +@nonreentrant('lock') +def increase_unlock_time(_unlock_time: uint256): + """ + @notice Extend the unlock time for `msg.sender` to `_unlock_time` + @param _unlock_time New epoch time for unlocking + """ + self.assert_not_contract(msg.sender) + _locked: LockedBalance = self.locked[msg.sender] + unlock_time: uint256 = (_unlock_time / WEEK) * WEEK # Locktime is rounded down to weeks + + assert _locked.end > block.timestamp, "Lock expired" + assert _locked.amount > 0, "Nothing is locked" + assert unlock_time > _locked.end, "Can only increase lock duration" + assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max" + + self._deposit_for(msg.sender, 0, unlock_time, _locked, INCREASE_UNLOCK_TIME, msg.sender) + + +@external +@nonreentrant('lock') +def withdraw(): + """ + @notice Withdraw all tokens for `msg.sender` + @dev Only possible if the lock has expired + """ + _locked: LockedBalance = self.locked[msg.sender] + assert block.timestamp >= _locked.end, "The lock didn't expire" + value: uint256 = convert(_locked.amount, uint256) + + old_locked: LockedBalance = _locked + _locked.end = 0 + _locked.amount = 0 + self.locked[msg.sender] = _locked + supply_before: uint256 = self.supply + self.supply = supply_before - value + + # old_locked can have either expired <= timestamp or zero end + # _locked has only 0 end + # Both can have >= 0 amount + self._checkpoint(msg.sender, old_locked, _locked) + + assert ERC20(self.token).transfer(msg.sender, value) + + log Withdraw(msg.sender, value, block.timestamp) + log Supply(supply_before, supply_before - value) + + +# The following ERC20/minime-compatible methods are not real balanceOf and supply! +# They measure the weights for the purpose of voting, so they don't represent +# real coins. + +@internal +@view +def find_block_epoch(_block: uint256, max_epoch: uint256) -> uint256: + """ + @notice Binary search to estimate timestamp for block number + @param _block Block to find + @param max_epoch Don't go beyond this epoch + @return Approximate timestamp for block + """ + # Binary search + _min: uint256 = 0 + _max: uint256 = max_epoch + for i in range(128): # Will be always enough for 128-bit numbers + if _min >= _max: + break + _mid: uint256 = (_min + _max + 1) / 2 + if self.point_history[_mid].blk <= _block: + _min = _mid + else: + _max = _mid - 1 + return _min + + +@external +@view +def balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256: + """ + @notice Get the current voting power for `msg.sender` + @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility + @param addr User wallet address + @param _t Epoch time to return voting power at + @return User voting power + """ + _epoch: uint256 = self.user_point_epoch[addr] + if _epoch == 0: + return 0 + else: + last_point: Point = self.user_point_history[addr][_epoch] + last_point.bias -= last_point.slope * convert(_t - last_point.ts, int128) + if last_point.bias < 0: + last_point.bias = 0 + return convert(last_point.bias, uint256) + + +@internal +@view +def _balanceOfAt(addr: address, _block: uint256) -> uint256: + """ + @notice measure voting power of `addr` at block height `_block` + @param addr User's wallet address + @param _block Block to calculate the voting power at + @return Voting power + """ + # Copying and pasting totalSupply code because Vyper cannot pass by + # reference yet + assert _block <= block.number + + # Binary search + _min: uint256 = 0 + _max: uint256 = self.user_point_epoch[addr] + for i in range(128): # Will be always enough for 128-bit numbers + if _min >= _max: + break + _mid: uint256 = (_min + _max + 1) / 2 + if self.user_point_history[addr][_mid].blk <= _block: + _min = _mid + else: + _max = _mid - 1 + + upoint: Point = self.user_point_history[addr][_min] + + max_epoch: uint256 = self.epoch + _epoch: uint256 = self.find_block_epoch(_block, max_epoch) + point_0: Point = self.point_history[_epoch] + d_block: uint256 = 0 + d_t: uint256 = 0 + if _epoch < max_epoch: + point_1: Point = self.point_history[_epoch + 1] + d_block = point_1.blk - point_0.blk + d_t = point_1.ts - point_0.ts + else: + d_block = block.number - point_0.blk + d_t = block.timestamp - point_0.ts + block_time: uint256 = point_0.ts + if d_block != 0: + block_time += d_t * (_block - point_0.blk) / d_block + + upoint.bias -= upoint.slope * convert(block_time - upoint.ts, int128) + if upoint.bias >= 0: + return convert(upoint.bias, uint256) + else: + return 0 + + +@external +@view +def balanceOfAt(addr: address, _block: uint256) -> uint256: + """ + @notice Measure voting power of `addr` at block height `_block` + @dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime + @param addr User's wallet address + @param _block Block to calculate the voting power at + @return Voting power + """ + return self._balanceOfAt(addr,_block) + + +@external +@view +def getPastVotes(addr: address, _block: uint256) -> uint256: + """ + @notice Measure voting power of `addr` at block height `_block` + @dev Adheres to ERC20Votes `getPastVotes` interface: @openzeppelin-contracts-upgradeable/blob/master/contracts/token/ERC20/extensions/ERC20VotesCompUpgradeable.sol + @param addr User's wallet address + @param _block Block to calculate the voting power at + @return Voting power + """ + return self._balanceOfAt(addr,_block) + + +@internal +@view +def supply_at(point: Point, t: uint256) -> uint256: + """ + @notice Calculate total voting power at some point in the past + @param point The point (bias/slope) to start search from + @param t Time to calculate the total voting power at + @return Total voting power at that time + """ + last_point: Point = point + t_i: uint256 = (last_point.ts / WEEK) * WEEK + for i in range(255): + t_i += WEEK + d_slope: int128 = 0 + if t_i > t: + t_i = t + else: + d_slope = self.slope_changes[t_i] + last_point.bias -= last_point.slope * convert(t_i - last_point.ts, int128) + if t_i == t: + break + last_point.slope += d_slope + last_point.ts = t_i + + if last_point.bias < 0: + last_point.bias = 0 + return convert(last_point.bias, uint256) + + +@external +@view +def totalSupply(t: uint256 = block.timestamp) -> uint256: + """ + @notice Calculate total voting power + @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility + @return Total voting power + """ + _epoch: uint256 = self.epoch + last_point: Point = self.point_history[_epoch] + return self.supply_at(last_point, t) + + +@external +@view +def totalSupplyAt(_block: uint256) -> uint256: + """ + @notice Calculate total voting power at some point in the past + @param _block Block to calculate the total voting power at + @return Total voting power at `_block` + """ + assert _block <= block.number + _epoch: uint256 = self.epoch + target_epoch: uint256 = self.find_block_epoch(_block, _epoch) + + point: Point = self.point_history[target_epoch] + dt: uint256 = 0 + if target_epoch < _epoch: + point_next: Point = self.point_history[target_epoch + 1] + if point.blk != point_next.blk: + dt = (_block - point.blk) * (point_next.ts - point.ts) / (point_next.blk - point.blk) + else: + if point.blk != block.number: + dt = (_block - point.blk) * (block.timestamp - point.ts) / (block.number - point.blk) + # Now dt contains info on how far are we beyond point + + return self.supply_at(point, point.ts + dt) \ No newline at end of file diff --git a/contracts/external/AccessControl.sol b/contracts/external/AccessControl.sol new file mode 100644 index 0000000..bfa8c2a --- /dev/null +++ b/contracts/external/AccessControl.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +import "../interfaces/IAccessControl.sol"; + +/** + * @dev This contract is fully forked from OpenZeppelin `AccessControl`. + * The only difference is the removal of the ERC165 implementation as it's not + * needed in Angle. + * + * Contract module that allows children to implement role-based access + * control mechanisms. This is a lightweight version that doesn't allow enumerating role + * members except through off-chain means by accessing the contract event logs. Some + * applications may benefit from on-chain enumerability, for those cases see + * {AccessControlEnumerable}. + * + * Roles are referred to by their `bytes32` identifier. These should be exposed + * in the external API and be unique. The best way to achieve this is by + * using `public constant` hash digests: + * + * ``` + * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); + * ``` + * + * Roles can be used to represent a set of permissions. To restrict access to a + * function call, use {hasRole}: + * + * ``` + * function foo() public { + * require(hasRole(MY_ROLE, msg.sender)); + * ... + * } + * ``` + * + * Roles can be granted and revoked dynamically via the {grantRole} and + * {revokeRole} functions. Each role has an associated admin role, and only + * accounts that have a role's admin role can call {grantRole} and {revokeRole}. + * + * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means + * that only accounts with this role will be able to grant or revoke other + * roles. More complex role relationships can be created by using + * {_setRoleAdmin}. + * + * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to + * grant and revoke this role. Extra precautions should be taken to secure + * accounts that have been granted it. + */ +abstract contract AccessControl is Context, IAccessControl { + struct RoleData { + mapping(address => bool) members; + bytes32 adminRole; + } + + mapping(bytes32 => RoleData) private _roles; + + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + /** + * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` + * + * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite + * {RoleAdminChanged} not being emitted signaling this. + * + * _Available since v3.1._ + */ + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + + /** + * @dev Emitted when `account` is granted `role`. + * + * `sender` is the account that originated the contract call, an admin role + * bearer except when using {_setupRole}. + */ + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Emitted when `account` is revoked `role`. + * + * `sender` is the account that originated the contract call: + * - if using `revokeRole`, it is the admin role bearer + * - if using `renounceRole`, it is the role bearer (i.e. `account`) + */ + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Modifier that checks that an account has a specific role. Reverts + * with a standardized message including the required role. + * + * The format of the revert reason is given by the following regular expression: + * + * /^AccessControl: account (0x[0-9a-f]{20}) is missing role (0x[0-9a-f]{32})$/ + * + * _Available since v4.1._ + */ + modifier onlyRole(bytes32 role) { + _checkRole(role, _msgSender()); + _; + } + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) public view override returns (bool) { + return _roles[role].members[account]; + } + + /** + * @dev Revert with a standard message if `account` is missing `role`. + * + * The format of the revert reason is given by the following regular expression: + * + * /^AccessControl: account (0x[0-9a-f]{20}) is missing role (0x[0-9a-f]{32})$/ + */ + function _checkRole(bytes32 role, address account) internal view { + if (!hasRole(role, account)) { + revert( + string( + abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(uint160(account), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ) + ); + } + } + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) public view override returns (bytes32) { + return _roles[role].adminRole; + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function grantRole(bytes32 role, address account) external override onlyRole(getRoleAdmin(role)) { + _grantRole(role, account); + } + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function revokeRole(bytes32 role, address account) external override onlyRole(getRoleAdmin(role)) { + _revokeRole(role, account); + } + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been granted `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `account`. + */ + function renounceRole(bytes32 role, address account) external override { + require(account == _msgSender(), "71"); + + _revokeRole(role, account); + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. Note that unlike {grantRole}, this function doesn't perform any + * checks on the calling account. + * + * [WARNING] + * ==== + * This function should only be called from the constructor when setting + * up the initial roles for the system. + * + * Using this function in any other way is effectively circumventing the admin + * system imposed by {AccessControl}. + * ==== + */ + function _setupRole(bytes32 role, address account) internal { + _grantRole(role, account); + } + + /** + * @dev Sets `adminRole` as ``role``'s admin role. + * + * Emits a {RoleAdminChanged} event. + */ + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal { + emit RoleAdminChanged(role, getRoleAdmin(role), adminRole); + _roles[role].adminRole = adminRole; + } + + function _grantRole(bytes32 role, address account) internal { + if (!hasRole(role, account)) { + _roles[role].members[account] = true; + emit RoleGranted(role, account, _msgSender()); + } + } + + function _revokeRole(bytes32 role, address account) internal { + if (hasRole(role, account)) { + _roles[role].members[account] = false; + emit RoleRevoked(role, account, _msgSender()); + } + } +} diff --git a/contracts/external/AccessControlUpgradeable.sol b/contracts/external/AccessControlUpgradeable.sol new file mode 100644 index 0000000..e2ac108 --- /dev/null +++ b/contracts/external/AccessControlUpgradeable.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +import "../interfaces/IAccessControl.sol"; + +/** + * @dev This contract is fully forked from OpenZeppelin `AccessControlUpgradeable`. + * The only difference is the removal of the ERC165 implementation as it's not + * needed in Angle. + * + * Contract module that allows children to implement role-based access + * control mechanisms. This is a lightweight version that doesn't allow enumerating role + * members except through off-chain means by accessing the contract event logs. Some + * applications may benefit from on-chain enumerability, for those cases see + * {AccessControlEnumerable}. + * + * Roles are referred to by their `bytes32` identifier. These should be exposed + * in the external API and be unique. The best way to achieve this is by + * using `public constant` hash digests: + * + * ``` + * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); + * ``` + * + * Roles can be used to represent a set of permissions. To restrict access to a + * function call, use {hasRole}: + * + * ``` + * function foo() public { + * require(hasRole(MY_ROLE, msg.sender)); + * ... + * } + * ``` + * + * Roles can be granted and revoked dynamically via the {grantRole} and + * {revokeRole} functions. Each role has an associated admin role, and only + * accounts that have a role's admin role can call {grantRole} and {revokeRole}. + * + * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means + * that only accounts with this role will be able to grant or revoke other + * roles. More complex role relationships can be created by using + * {_setRoleAdmin}. + * + * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to + * grant and revoke this role. Extra precautions should be taken to secure + * accounts that have been granted it. + */ +abstract contract AccessControlUpgradeable is Initializable, IAccessControl { + function __AccessControl_init() internal initializer { + __AccessControl_init_unchained(); + } + + function __AccessControl_init_unchained() internal initializer {} + + struct RoleData { + mapping(address => bool) members; + bytes32 adminRole; + } + + mapping(bytes32 => RoleData) private _roles; + + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + /** + * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` + * + * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite + * {RoleAdminChanged} not being emitted signaling this. + * + * _Available since v3.1._ + */ + event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); + + /** + * @dev Emitted when `account` is granted `role`. + * + * `sender` is the account that originated the contract call, an admin role + * bearer except when using {_setupRole}. + */ + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Emitted when `account` is revoked `role`. + * + * `sender` is the account that originated the contract call: + * - if using `revokeRole`, it is the admin role bearer + * - if using `renounceRole`, it is the role bearer (i.e. `account`) + */ + event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); + + /** + * @dev Modifier that checks that an account has a specific role. Reverts + * with a standardized message including the required role. + * + * The format of the revert reason is given by the following regular expression: + * + * /^AccessControl: account (0x[0-9a-f]{20}) is missing role (0x[0-9a-f]{32})$/ + * + * _Available since v4.1._ + */ + modifier onlyRole(bytes32 role) { + _checkRole(role, msg.sender); + _; + } + + /** + * @dev Returns `true` if `account` has been granted `role`. + */ + function hasRole(bytes32 role, address account) public view override returns (bool) { + return _roles[role].members[account]; + } + + /** + * @dev Revert with a standard message if `account` is missing `role`. + * + * The format of the revert reason is given by the following regular expression: + * + * /^AccessControl: account (0x[0-9a-f]{20}) is missing role (0x[0-9a-f]{32})$/ + */ + function _checkRole(bytes32 role, address account) internal view { + if (!hasRole(role, account)) { + revert( + string( + abi.encodePacked( + "AccessControl: account ", + StringsUpgradeable.toHexString(uint160(account), 20), + " is missing role ", + StringsUpgradeable.toHexString(uint256(role), 32) + ) + ) + ); + } + } + + /** + * @dev Returns the admin role that controls `role`. See {grantRole} and + * {revokeRole}. + * + * To change a role's admin, use {_setRoleAdmin}. + */ + function getRoleAdmin(bytes32 role) public view override returns (bytes32) { + return _roles[role].adminRole; + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function grantRole(bytes32 role, address account) external override onlyRole(getRoleAdmin(role)) { + _grantRole(role, account); + } + + /** + * @dev Revokes `role` from `account`. + * + * If `account` had been granted `role`, emits a {RoleRevoked} event. + * + * Requirements: + * + * - the caller must have ``role``'s admin role. + */ + function revokeRole(bytes32 role, address account) external override onlyRole(getRoleAdmin(role)) { + _revokeRole(role, account); + } + + /** + * @dev Revokes `role` from the calling account. + * + * Roles are often managed via {grantRole} and {revokeRole}: this function's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * If the calling account had been granted `role`, emits a {RoleRevoked} + * event. + * + * Requirements: + * + * - the caller must be `account`. + */ + function renounceRole(bytes32 role, address account) external override { + require(account == msg.sender, "71"); + + _revokeRole(role, account); + } + + /** + * @dev Grants `role` to `account`. + * + * If `account` had not been already granted `role`, emits a {RoleGranted} + * event. Note that unlike {grantRole}, this function doesn't perform any + * checks on the calling account. + * + * [WARNING] + * ==== + * This function should only be called from the constructor when setting + * up the initial roles for the system. + * + * Using this function in any other way is effectively circumventing the admin + * system imposed by {AccessControl}. + * ==== + */ + function _setupRole(bytes32 role, address account) internal { + _grantRole(role, account); + } + + /** + * @dev Sets `adminRole` as ``role``'s admin role. + * + * Emits a {RoleAdminChanged} event. + */ + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal { + emit RoleAdminChanged(role, getRoleAdmin(role), adminRole); + _roles[role].adminRole = adminRole; + } + + function _grantRole(bytes32 role, address account) internal { + if (!hasRole(role, account)) { + _roles[role].members[account] = true; + emit RoleGranted(role, account, msg.sender); + } + } + + function _revokeRole(bytes32 role, address account) internal { + if (hasRole(role, account)) { + _roles[role].members[account] = false; + emit RoleRevoked(role, account, msg.sender); + } + } + + uint256[49] private __gap; +} diff --git a/contracts/external/FullMath.sol b/contracts/external/FullMath.sol new file mode 100644 index 0000000..e8cec2c --- /dev/null +++ b/contracts/external/FullMath.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.4.0; + +/// @title Contains 512-bit math functions +/// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision +/// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits +/// @dev This contract was forked from Uniswap V3's contract `FullMath.sol` available here +/// https://github.com/Uniswap/uniswap-v3-core/blob/main/contracts/libraries/FullMath.sol +abstract contract FullMath { + /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + /// @param a The multiplicand + /// @param b The multiplier + /// @param denominator The divisor + /// @return result The 256-bit result + /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv + /// @dev Contrary to UniswapV3 implementation, this contract doesn't handle `a*b` overflows + /// and will revert if so + function _mulDiv( + uint256 a, + uint256 b, + uint256 denominator + ) internal pure returns (uint256 result) { + // 512-bit multiply [prod1 prod0] = a * b + // Compute the product mod 2**256 and mod 2**256 - 1 + // then use the Chinese Remainder Theorem to reconstruct + // the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2**256 + prod0 + uint256 prod0; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(a, b, not(0)) + prod0 := mul(a, b) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division + if (prod1 == 0) { + require(denominator > 0); + assembly { + result := div(prod0, denominator) + } + return result; + } + + // Make sure the result is less than 2**256. + // Also prevents denominator == 0 + require(denominator > prod1); + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0] + // Compute remainder using mulmod + uint256 remainder; + assembly { + remainder := mulmod(a, b, denominator) + } + // Subtract 256 bit number from 512 bit number + assembly { + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator + // Compute largest power of two divisor of denominator. + // Always >= 1. + uint256 twos = denominator & (~denominator + 1); + // Divide denominator by power of two + assembly { + denominator := div(denominator, twos) + } + + // Divide [prod1 prod0] by the factors of two + assembly { + prod0 := div(prod0, twos) + } + // Shift in bits from prod1 into prod0. For this we need + // to flip `twos` such that it is 2**256 / twos. + // If twos is zero, then it becomes one + assembly { + twos := add(div(sub(0, twos), twos), 1) + } + prod0 |= prod1 * twos; + + // Invert denominator mod 2**256 + // Now that denominator is an odd number, it has an inverse + // modulo 2**256 such that denominator * inv = 1 mod 2**256. + // Compute the inverse by starting with a seed that is correct + // correct for four bits. That is, denominator * inv = 1 mod 2**4 + uint256 inv = (3 * denominator) ^ 2; + // Now use Newton-Raphson iteration to improve the precision. + // Thanks to Hensel's lifting lemma, this also works in modular + // arithmetic, doubling the correct bits in each step. + inv *= 2 - denominator * inv; // inverse mod 2**8 + inv *= 2 - denominator * inv; // inverse mod 2**16 + inv *= 2 - denominator * inv; // inverse mod 2**32 + inv *= 2 - denominator * inv; // inverse mod 2**64 + inv *= 2 - denominator * inv; // inverse mod 2**128 + inv *= 2 - denominator * inv; // inverse mod 2**256 + + // Because the division is now exact we can divide by multiplying + // with the modular inverse of denominator. This will give us the + // correct result modulo 2**256. Since the precoditions guarantee + // that the outcome is less than 2**256, this is the final result. + // We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inv; + return result; + } +} diff --git a/contracts/external/ProxyAdmin.sol b/contracts/external/ProxyAdmin.sol new file mode 100644 index 0000000..cfaed41 --- /dev/null +++ b/contracts/external/ProxyAdmin.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./TransparentUpgradeableProxy.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an + * explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}. + * This contract was fully forked from OpenZeppelin `ProxyAdmin` + */ +contract ProxyAdmin is Ownable { + /** + * @dev Returns the current implementation of `proxy`. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function getProxyImplementation(TransparentUpgradeableProxy proxy) public view virtual returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("implementation()")) == 0x5c60da1b + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Returns the current admin of `proxy`. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("admin()")) == 0xf851a440 + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Changes the admin of `proxy` to `newAdmin`. + * + * Requirements: + * + * - This contract must be the current admin of `proxy`. + */ + function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner { + proxy.changeAdmin(newAdmin); + } + + /** + * @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function upgrade(TransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner { + proxy.upgradeTo(implementation); + } + + /** + * @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. See + * {TransparentUpgradeableProxy-upgradeToAndCall}. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. + */ + function upgradeAndCall( + TransparentUpgradeableProxy proxy, + address implementation, + bytes memory data + ) public payable virtual onlyOwner { + proxy.upgradeToAndCall{ value: msg.value }(implementation, data); + } +} diff --git a/contracts/external/SmartWalletChecker.sol b/contracts/external/SmartWalletChecker.sol new file mode 100644 index 0000000..5ad1c41 --- /dev/null +++ b/contracts/external/SmartWalletChecker.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.7; + +/// @notice Interface of the `SmartWalletChecker` contracts of the protocol +interface SmartWalletChecker { + function check(address) external view returns (bool); +} + +/// @title SmartWalletWhitelist +/// @author Curve Finance and adapted by Angle Core Team (https://etherscan.io/address/0xca719728ef172d0961768581fdf35cb116e0b7a4#code) +/// @notice Provides functions to check whether a wallet has been verified or not to own veANGLE +contract SmartWalletWhitelist { + /// @notice Mapping between addresses and whether they are whitelisted or not + mapping(address => bool) public wallets; + /// @notice Admin address of the contract + address public admin; + /// @notice Future admin address of the contract + //solhint-disable-next-line + address public future_admin; + /// @notice Contract which works as this contract and that can whitelist addresses + address public checker; + /// @notice Future address to become checker + //solhint-disable-next-line + address public future_checker; + + event ApproveWallet(address); + event RevokeWallet(address); + + /// @notice Constructor of the contract + /// @param _admin Admin address of the contract + constructor(address _admin) { + require(_admin != address(0), "0"); + admin = _admin; + } + + /// @notice Commits to change the admin + /// @param _admin New admin of the contract + function commitAdmin(address _admin) external { + require(msg.sender == admin, "!admin"); + future_admin = _admin; + } + + /// @notice Changes the admin to the admin that has been committed + function applyAdmin() external { + require(msg.sender == admin, "!admin"); + require(future_admin != address(0), "admin not set"); + admin = future_admin; + } + + /// @notice Commits to change the checker address + /// @param _checker New checker address + /// @dev This address can be the zero address in which case there will be no checker + function commitSetChecker(address _checker) external { + require(msg.sender == admin, "!admin"); + future_checker = _checker; + } + + /// @notice Applies the checker previously committed + function applySetChecker() external { + require(msg.sender == admin, "!admin"); + checker = future_checker; + } + + /// @notice Approves a wallet + /// @param _wallet Wallet to approve + function approveWallet(address _wallet) public { + require(msg.sender == admin, "!admin"); + wallets[_wallet] = true; + + emit ApproveWallet(_wallet); + } + + /// @notice Revokes a wallet + /// @param _wallet Wallet to revoke + function revokeWallet(address _wallet) external { + require(msg.sender == admin, "!admin"); + wallets[_wallet] = false; + + emit RevokeWallet(_wallet); + } + + /// @notice Checks whether a wallet is whitelisted + /// @param _wallet Wallet address to check + /// @dev This function can also rely on another SmartWalletChecker (a `checker` to see whether the wallet is whitelisted or not) + function check(address _wallet) external view returns (bool) { + bool _check = wallets[_wallet]; + if (_check) { + return _check; + } else { + if (checker != address(0)) { + return SmartWalletChecker(checker).check(_wallet); + } + } + return false; + } +} diff --git a/contracts/external/TransparentUpgradeableProxy.sol b/contracts/external/TransparentUpgradeableProxy.sol new file mode 100644 index 0000000..2bb6caa --- /dev/null +++ b/contracts/external/TransparentUpgradeableProxy.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +/** + * @dev This contract implements a proxy that is upgradeable by an admin. It is fully forked from OpenZeppelin + * `TransparentUpgradeableProxy` + * + * To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector + * clashing], which can potentially be used in an attack, this contract uses the + * https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two + * things that go hand in hand: + * + * 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if + * that call matches one of the admin functions exposed by the proxy itself. + * 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the + * implementation. If the admin tries to call a function on the implementation it will fail with an error that says + * "admin cannot fallback to proxy target". + * + * These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing + * the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due + * to sudden errors when trying to call a function from the proxy implementation. + * + * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, + * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy. + */ +contract TransparentUpgradeableProxy is ERC1967Proxy { + /** + * @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and + * optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}. + */ + constructor( + address _logic, + address admin_, + bytes memory _data + ) payable ERC1967Proxy(_logic, _data) { + assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)); + _changeAdmin(admin_); + } + + /** + * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. + */ + modifier ifAdmin() { + if (msg.sender == _getAdmin()) { + _; + } else { + _fallback(); + } + } + + /** + * @dev Returns the current admin. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + */ + function admin() external ifAdmin returns (address admin_) { + admin_ = _getAdmin(); + } + + /** + * @dev Returns the current implementation. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` + */ + function implementation() external ifAdmin returns (address implementation_) { + implementation_ = _implementation(); + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {AdminChanged} event. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}. + */ + function changeAdmin(address newAdmin) external virtual ifAdmin { + _changeAdmin(newAdmin); + } + + /** + * @dev Upgrade the implementation of the proxy. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}. + */ + function upgradeTo(address newImplementation) external ifAdmin { + _upgradeToAndCall(newImplementation, bytes(""), false); + } + + /** + * @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified + * by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the + * proxied contract. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}. + */ + function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin { + _upgradeToAndCall(newImplementation, data, true); + } + + /** + * @dev Returns the current admin. + */ + function _admin() internal view virtual returns (address) { + return _getAdmin(); + } + + /** + * @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}. + */ + function _beforeFallback() internal virtual override { + require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); + super._beforeFallback(); + } +} diff --git a/contracts/interfaces/IAccessControl.sol b/contracts/interfaces/IAccessControl.sol new file mode 100644 index 0000000..caf7936 --- /dev/null +++ b/contracts/interfaces/IAccessControl.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +/// @title IAccessControl +/// @author Forked from OpenZeppelin +/// @notice Interface for `AccessControl` contracts +interface IAccessControl { + function hasRole(bytes32 role, address account) external view returns (bool); + + function getRoleAdmin(bytes32 role) external view returns (bytes32); + + function grantRole(bytes32 role, address account) external; + + function revokeRole(bytes32 role, address account) external; + + function renounceRole(bytes32 role, address account) external; +} diff --git a/contracts/interfaces/IAgToken.sol b/contracts/interfaces/IAgToken.sol new file mode 100644 index 0000000..f0d3a18 --- /dev/null +++ b/contracts/interfaces/IAgToken.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +/// @title IAgToken +/// @author Angle Core Team +/// @notice Interface for the stablecoins `AgToken` contracts +/// @dev The only functions that are left in the interface are the functions which are used +/// at another point in the protocol by a different contract +interface IAgToken is IERC20Upgradeable { + // ======================= `StableMaster` functions ============================ + function mint(address account, uint256 amount) external; + + function burnFrom( + uint256 amount, + address burner, + address sender + ) external; + + function burnSelf(uint256 amount, address burner) external; + + // ========================= External function ================================= + + function stableMaster() external view returns (address); +} diff --git a/contracts/interfaces/IAngleMiddlemanGauge.sol b/contracts/interfaces/IAngleMiddlemanGauge.sol new file mode 100644 index 0000000..5907a6c --- /dev/null +++ b/contracts/interfaces/IAngleMiddlemanGauge.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +/// @title IAngleMiddlemanGauge +/// @author Angle Core Team +/// @notice Interface for the `AngleMiddleman` contract +interface IAngleMiddlemanGauge { + function notifyReward(address gauge, uint256 amount) external; +} diff --git a/contracts/interfaces/IBondingCurve.sol b/contracts/interfaces/IBondingCurve.sol new file mode 100644 index 0000000..2662d2f --- /dev/null +++ b/contracts/interfaces/IBondingCurve.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./IAgToken.sol"; +import "./IOracle.sol"; + +/// @title IBondingCurve +/// @author Angle Core Team +/// @notice Interface for the `BondingCurve` contract +interface IBondingCurve { + // ============================ User Functions ================================= + + function buySoldToken( + IAgToken _agToken, + uint256 targetSoldTokenQuantity, + uint256 maxAmountToPayInAgToken + ) external; + + // ========================== Governance Functions ============================= + + function changeOracle(IAgToken _agToken, IOracle _oracle) external; +} diff --git a/contracts/interfaces/ICollateralSettler.sol b/contracts/interfaces/ICollateralSettler.sol new file mode 100644 index 0000000..3293462 --- /dev/null +++ b/contracts/interfaces/ICollateralSettler.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +/// @title ICollateralSettler +/// @author Angle Core Team +/// @notice Interface for the collateral settlement contracts +interface ICollateralSettler { + function triggerSettlement( + uint256 _oracleValue, + uint256 _sanRate, + uint256 _stocksUsers + ) external; +} diff --git a/contracts/interfaces/ICore.sol b/contracts/interfaces/ICore.sol new file mode 100644 index 0000000..0d9fb94 --- /dev/null +++ b/contracts/interfaces/ICore.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./IStableMaster.sol"; + +/// @title ICore +/// @author Angle Core Team +/// @dev Interface for the functions of the `Core` contract +interface ICore { + function revokeStableMaster(address stableMaster) external; + + function addGovernor(address _governor) external; + + function removeGovernor(address _governor) external; + + function setGuardian(address _guardian) external; + + function revokeGuardian() external; + + function governorList() external view returns (address[] memory); + + function stablecoinList() external view returns (address[] memory); + + function guardian() external view returns (address); +} diff --git a/contracts/interfaces/ICoreBorrow.sol b/contracts/interfaces/ICoreBorrow.sol new file mode 100644 index 0000000..3cc5b43 --- /dev/null +++ b/contracts/interfaces/ICoreBorrow.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +/// @title ICoreBorrow +/// @author Angle Core Team +/// @notice Interface for the `CoreBorrow` contract +/// @dev This interface only contains functions of the `CoreBorrow` contract which are called by other contracts +/// of this module +interface ICoreBorrow { + /// @notice Checks if an address corresponds to a treasury of a stablecoin with a flash loan + /// module initialized on it + /// @param treasury Address to check + /// @return Whether the address has the `FLASHLOANER_TREASURY_ROLE` or not + function isFlashLoanerTreasury(address treasury) external view returns (bool); + + /// @notice Checks whether an address is governor of the Angle Protocol or not + /// @param admin Address to check + /// @return Whether the address has the `GOVERNOR_ROLE` or not + function isGovernor(address admin) external view returns (bool); + + /// @notice Checks whether an address is governor or a guardian of the Angle Protocol or not + /// @param admin Address to check + /// @return Whether the address has the `GUARDIAN_ROLE` or not + /// @dev Governance should make sure when adding a governor to also give this governor the guardian + /// role by calling the `addGovernor` function + function isGovernorOrGuardian(address admin) external view returns (bool); +} diff --git a/contracts/interfaces/IERC721.sol b/contracts/interfaces/IERC721.sol new file mode 100644 index 0000000..53d07cf --- /dev/null +++ b/contracts/interfaces/IERC721.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +interface IERC721 is IERC165 { + function balanceOf(address owner) external view returns (uint256 balance); + + function ownerOf(uint256 tokenId) external view returns (address owner); + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + function approve(address to, uint256 tokenId) external; + + function getApproved(uint256 tokenId) external view returns (address operator); + + function setApprovalForAll(address operator, bool _approved) external; + + function isApprovedForAll(address owner, address operator) external view returns (bool); + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external; +} + +interface IERC721Metadata is IERC721 { + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function tokenURI(uint256 tokenId) external view returns (string memory); +} diff --git a/contracts/interfaces/IFeeDistributor.sol b/contracts/interfaces/IFeeDistributor.sol new file mode 100644 index 0000000..8d89070 --- /dev/null +++ b/contracts/interfaces/IFeeDistributor.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +/// @title IFeeDistributor +/// @author Interface of the `FeeDistributor` contract +/// @dev This interface is used by the `SurplusConverter` contract to send funds to the `FeeDistributor` +interface IFeeDistributor { + function burn(address token) external; +} + +/// @title IFeeDistributorFront +/// @author Interface for public use of the `FeeDistributor` contract +/// @dev This interface is used for user related function +interface IFeeDistributorFront { + function token() external returns (address); + + function claim(address _addr) external returns (uint256); + + function claim(address[20] memory _addr) external returns (bool); +} \ No newline at end of file diff --git a/contracts/interfaces/IFeeManager.sol b/contracts/interfaces/IFeeManager.sol new file mode 100644 index 0000000..afe30cc --- /dev/null +++ b/contracts/interfaces/IFeeManager.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./IAccessControl.sol"; + +/// @title IFeeManagerFunctions +/// @author Angle Core Team +/// @dev Interface for the `FeeManager` contract +interface IFeeManagerFunctions is IAccessControl { + // ================================= Keepers =================================== + + function updateUsersSLP() external; + + function updateHA() external; + + // ================================= Governance ================================ + + function deployCollateral( + address[] memory governorList, + address guardian, + address _perpetualManager + ) external; + + function setFees( + uint256[] memory xArray, + uint64[] memory yArray, + uint8 typeChange + ) external; + + function setHAFees(uint64 _haFeeDeposit, uint64 _haFeeWithdraw) external; +} + +/// @title IFeeManager +/// @author Angle Core Team +/// @notice Previous interface with additionnal getters for public variables and mappings +/// @dev We need these getters as they are used in other contracts of the protocol +interface IFeeManager is IFeeManagerFunctions { + function stableMaster() external view returns (address); + + function perpetualManager() external view returns (address); +} diff --git a/contracts/interfaces/IGaugeController.sol b/contracts/interfaces/IGaugeController.sol new file mode 100644 index 0000000..082898d --- /dev/null +++ b/contracts/interfaces/IGaugeController.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +interface IGaugeController { + //solhint-disable-next-line + function gauge_types(address addr) external view returns (int128); + + //solhint-disable-next-line + function gauge_relative_weight_write(address addr, uint256 timestamp) external returns (uint256); + + //solhint-disable-next-line + function gauge_relative_weight(address addr, uint256 timestamp) external view returns (uint256); +} diff --git a/contracts/interfaces/IGenericLender.sol b/contracts/interfaces/IGenericLender.sol new file mode 100644 index 0000000..e875c00 --- /dev/null +++ b/contracts/interfaces/IGenericLender.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./IAccessControl.sol"; + +/// @title IGenericLender +/// @author Yearn with slight modifications from Angle Core Team +/// @dev Interface for the `GenericLender` contract, the base interface for contracts interacting +/// with lending and yield farming platforms +interface IGenericLender is IAccessControl { + function lenderName() external view returns (string memory); + + function nav() external view returns (uint256); + + function strategy() external view returns (address); + + function apr() external view returns (uint256); + + function weightedApr() external view returns (uint256); + + function withdraw(uint256 amount) external returns (uint256); + + function emergencyWithdraw(uint256 amount) external; + + function deposit() external; + + function withdrawAll() external returns (bool); + + function hasAssets() external view returns (bool); + + function aprAfterDeposit(uint256 amount) external view returns (uint256); + + function sweep(address _token, address to) external; +} diff --git a/contracts/interfaces/IKeeperRegistry.sol b/contracts/interfaces/IKeeperRegistry.sol new file mode 100644 index 0000000..452f321 --- /dev/null +++ b/contracts/interfaces/IKeeperRegistry.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +/// @title IKeeperRegistry +/// @author Angle Core Team +interface IKeeperRegistry { + /// @notice Checks whether an address is whitelisted during oracle updates + /// @param caller Address for which the whitelist should be checked + /// @return Whether if the address is trusted + function isTrusted(address caller) external view returns (bool); +} diff --git a/contracts/interfaces/ILiquidityGauge.sol b/contracts/interfaces/ILiquidityGauge.sol new file mode 100644 index 0000000..377d9c2 --- /dev/null +++ b/contracts/interfaces/ILiquidityGauge.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +interface ILiquidityGauge { + // solhint-disable-next-line + function staking_token() external returns (address stakingToken); + + // solhint-disable-next-line + function deposit_reward_token(address _rewardToken, uint256 _amount) external; + + function deposit( + uint256 _value, + address _addr, + // solhint-disable-next-line + bool _claim_rewards + ) external; + + // solhint-disable-next-line + function claim_rewards(address _addr) external; + + // solhint-disable-next-line + function claim_rewards(address _addr, address _receiver) external; +} diff --git a/contracts/interfaces/IOracle.sol b/contracts/interfaces/IOracle.sol new file mode 100644 index 0000000..a95bf60 --- /dev/null +++ b/contracts/interfaces/IOracle.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +/// @title IOracle +/// @author Angle Core Team +/// @notice Interface for Angle's oracle contracts reading oracle rates from both UniswapV3 and Chainlink +/// from just UniswapV3 or from just Chainlink +interface IOracle { + function read() external view returns (uint256); + + function readAll() external view returns (uint256 lowerRate, uint256 upperRate); + + function readLower() external view returns (uint256); + + function readUpper() external view returns (uint256); + + function readQuote(uint256 baseAmount) external view returns (uint256); + + function readQuoteLower(uint256 baseAmount) external view returns (uint256); + + function inBase() external view returns (uint256); +} diff --git a/contracts/interfaces/IPerpetualManager.sol b/contracts/interfaces/IPerpetualManager.sol new file mode 100644 index 0000000..6f3f493 --- /dev/null +++ b/contracts/interfaces/IPerpetualManager.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./IERC721.sol"; +import "./IFeeManager.sol"; +import "./IOracle.sol"; +import "./IAccessControl.sol"; + +/// @title Interface of the contract managing perpetuals +/// @author Angle Core Team +/// @dev Front interface, meaning only user-facing functions +interface IPerpetualManagerFront is IERC721Metadata { + function openPerpetual( + address owner, + uint256 amountBrought, + uint256 amountCommitted, + uint256 maxOracleRate, + uint256 minNetMargin + ) external returns (uint256 perpetualID); + + function closePerpetual( + uint256 perpetualID, + address to, + uint256 minCashOutAmount + ) external; + + function addToPerpetual(uint256 perpetualID, uint256 amount) external; + + function removeFromPerpetual( + uint256 perpetualID, + uint256 amount, + address to + ) external; + + function liquidatePerpetuals(uint256[] memory perpetualIDs) external; + + function forceClosePerpetuals(uint256[] memory perpetualIDs) external; + + // ========================= External View Functions ============================= + + function getCashOutAmount(uint256 perpetualID, uint256 rate) external view returns (uint256, uint256); + + function isApprovedOrOwner(address spender, uint256 perpetualID) external view returns (bool); +} + +/// @title Interface of the contract managing perpetuals +/// @author Angle Core Team +/// @dev This interface does not contain user facing functions, it just has functions that are +/// interacted with in other parts of the protocol +interface IPerpetualManagerFunctions is IAccessControl { + // ================================= Governance ================================ + + function deployCollateral( + address[] memory governorList, + address guardian, + IFeeManager feeManager, + IOracle oracle_ + ) external; + + function setFeeManager(IFeeManager feeManager_) external; + + function setHAFees( + uint64[] memory _xHAFees, + uint64[] memory _yHAFees, + uint8 deposit + ) external; + + function setTargetAndLimitHAHedge(uint64 _targetHAHedge, uint64 _limitHAHedge) external; + + function setKeeperFeesLiquidationRatio(uint64 _keeperFeesLiquidationRatio) external; + + function setKeeperFeesCap(uint256 _keeperFeesLiquidationCap, uint256 _keeperFeesClosingCap) external; + + function setKeeperFeesClosing(uint64[] memory _xKeeperFeesClosing, uint64[] memory _yKeeperFeesClosing) external; + + function setLockTime(uint64 _lockTime) external; + + function setBoundsPerpetual(uint64 _maxLeverage, uint64 _maintenanceMargin) external; + + function pause() external; + + function unpause() external; + + // ==================================== Keepers ================================ + + function setFeeKeeper(uint64 feeDeposit, uint64 feesWithdraw) external; + + // =============================== StableMaster ================================ + + function setOracle(IOracle _oracle) external; +} + +/// @title IPerpetualManager +/// @author Angle Core Team +/// @notice Previous interface with additionnal getters for public variables +interface IPerpetualManager is IPerpetualManagerFunctions { + function poolManager() external view returns (address); + + function oracle() external view returns (address); + + function targetHAHedge() external view returns (uint64); + + function totalHedgeAmount() external view returns (uint256); +} + +/// @title Interface of the contract managing perpetuals with claim function +/// @author Angle Core Team +/// @dev Front interface with rewards function, meaning only user-facing functions +interface IPerpetualManagerFrontWithClaim is IPerpetualManagerFront, IPerpetualManager { + function getReward(uint256 perpetualID) external; +} diff --git a/contracts/interfaces/IPoolManager.sol b/contracts/interfaces/IPoolManager.sol new file mode 100644 index 0000000..2d7757e --- /dev/null +++ b/contracts/interfaces/IPoolManager.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./IFeeManager.sol"; +import "./IPerpetualManager.sol"; +import "./IOracle.sol"; + +// Struct for the parameters associated to a strategy interacting with a collateral `PoolManager` +// contract +struct StrategyParams { + // Timestamp of last report made by this strategy + // It is also used to check if a strategy has been initialized + uint256 lastReport; + // Total amount the strategy is expected to have + uint256 totalStrategyDebt; + // The share of the total assets in the `PoolManager` contract that the `strategy` can access to. + uint256 debtRatio; +} + +/// @title IPoolManagerFunctions +/// @author Angle Core Team +/// @notice Interface for the collateral poolManager contracts handling each one type of collateral for +/// a given stablecoin +/// @dev Only the functions used in other contracts of the protocol are left here +interface IPoolManagerFunctions { + // ============================ Constructor ==================================== + + function deployCollateral( + address[] memory governorList, + address guardian, + IPerpetualManager _perpetualManager, + IFeeManager feeManager, + IOracle oracle + ) external; + + // ============================ Yield Farming ================================== + + function creditAvailable() external view returns (uint256); + + function debtOutstanding() external view returns (uint256); + + function report( + uint256 _gain, + uint256 _loss, + uint256 _debtPayment + ) external; + + // ============================ Governance ===================================== + + function addGovernor(address _governor) external; + + function removeGovernor(address _governor) external; + + function setGuardian(address _guardian, address guardian) external; + + function revokeGuardian(address guardian) external; + + function setFeeManager(IFeeManager _feeManager) external; + + // ============================= Getters ======================================= + + function getBalance() external view returns (uint256); + + function getTotalAsset() external view returns (uint256); +} + +/// @title IPoolManager +/// @author Angle Core Team +/// @notice Previous interface with additionnal getters for public variables and mappings +/// @dev Used in other contracts of the protocol +interface IPoolManager is IPoolManagerFunctions { + function stableMaster() external view returns (address); + + function perpetualManager() external view returns (address); + + function token() external view returns (address); + + function feeManager() external view returns (address); + + function totalDebt() external view returns (uint256); + + function strategies(address _strategy) external view returns (StrategyParams memory); +} diff --git a/contracts/interfaces/IRewardsDistributor.sol b/contracts/interfaces/IRewardsDistributor.sol new file mode 100644 index 0000000..4eb3c62 --- /dev/null +++ b/contracts/interfaces/IRewardsDistributor.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "./IStakingRewards.sol"; + +/// @title IRewardsDistributor +/// @author Angle Core Team, inspired from Fei protocol +/// (https://github.com/fei-protocol/fei-protocol-core/blob/master/contracts/staking/IRewardsDistributor.sol) +/// @notice Rewards Distributor interface +interface IRewardsDistributor { + // ========================= Public Parameter Getter =========================== + + function rewardToken() external view returns (IERC20); + + // ======================== External User Available Function =================== + + function drip(IStakingRewards stakingContract) external returns (uint256); + + // ========================= Governor Functions ================================ + + function governorWithdrawRewardToken(uint256 amount, address governance) external; + + function governorRecover( + address tokenAddress, + address to, + uint256 amount, + IStakingRewards stakingContract + ) external; + + function setUpdateFrequency(uint256 _frequency, IStakingRewards stakingContract) external; + + function setIncentiveAmount(uint256 _incentiveAmount, IStakingRewards stakingContract) external; + + function setAmountToDistribute(uint256 _amountToDistribute, IStakingRewards stakingContract) external; + + function setDuration(uint256 _duration, IStakingRewards stakingContract) external; + + function setStakingContract( + address _stakingContract, + uint256 _duration, + uint256 _incentiveAmount, + uint256 _dripFrequency, + uint256 _amountToDistribute + ) external; + + function setNewRewardsDistributor(address newRewardsDistributor) external; + + function removeStakingContract(IStakingRewards stakingContract) external; +} diff --git a/contracts/interfaces/ISanToken.sol b/contracts/interfaces/ISanToken.sol new file mode 100644 index 0000000..2b85817 --- /dev/null +++ b/contracts/interfaces/ISanToken.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +/// @title ISanToken +/// @author Angle Core Team +/// @notice Interface for Angle's `SanToken` contract that handles sanTokens, tokens that are given to SLPs +/// contributing to a collateral for a given stablecoin +interface ISanToken is IERC20Upgradeable { + // ================================== StableMaster ============================= + + function mint(address account, uint256 amount) external; + + function burnFrom( + uint256 amount, + address burner, + address sender + ) external; + + function burnSelf(uint256 amount, address burner) external; + + function stableMaster() external view returns (address); + + function poolManager() external view returns (address); +} diff --git a/contracts/interfaces/IStableMaster.sol b/contracts/interfaces/IStableMaster.sol new file mode 100644 index 0000000..10616dc --- /dev/null +++ b/contracts/interfaces/IStableMaster.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Normally just importing `IPoolManager` should be sufficient, but for clarity here +// we prefer to import all concerned interfaces +import "./IPoolManager.sol"; +import "./IOracle.sol"; +import "./IPerpetualManager.sol"; +import "./ISanToken.sol"; + +// Struct to handle all the parameters to manage the fees +// related to a given collateral pool (associated to the stablecoin) +struct MintBurnData { + // Values of the thresholds to compute the minting fees + // depending on HA hedge (scaled by `BASE_PARAMS`) + uint64[] xFeeMint; + // Values of the fees at thresholds (scaled by `BASE_PARAMS`) + uint64[] yFeeMint; + // Values of the thresholds to compute the burning fees + // depending on HA hedge (scaled by `BASE_PARAMS`) + uint64[] xFeeBurn; + // Values of the fees at thresholds (scaled by `BASE_PARAMS`) + uint64[] yFeeBurn; + // Max proportion of collateral from users that can be covered by HAs + // It is exactly the same as the parameter of the same name in `PerpetualManager`, whenever one is updated + // the other changes accordingly + uint64 targetHAHedge; + // Minting fees correction set by the `FeeManager` contract: they are going to be multiplied + // to the value of the fees computed using the hedge curve + // Scaled by `BASE_PARAMS` + uint64 bonusMalusMint; + // Burning fees correction set by the `FeeManager` contract: they are going to be multiplied + // to the value of the fees computed using the hedge curve + // Scaled by `BASE_PARAMS` + uint64 bonusMalusBurn; + // Parameter used to limit the number of stablecoins that can be issued using the concerned collateral + uint256 capOnStableMinted; +} + +// Struct to handle all the variables and parameters to handle SLPs in the protocol +// including the fraction of interests they receive or the fees to be distributed to +// them +struct SLPData { + // Last timestamp at which the `sanRate` has been updated for SLPs + uint256 lastBlockUpdated; + // Fees accumulated from previous blocks and to be distributed to SLPs + uint256 lockedInterests; + // Max interests used to update the `sanRate` in a single block + // Should be in collateral token base + uint256 maxInterestsDistributed; + // Amount of fees left aside for SLPs and that will be distributed + // when the protocol is collateralized back again + uint256 feesAside; + // Part of the fees normally going to SLPs that is left aside + // before the protocol is collateralized back again (depends on collateral ratio) + // Updated by keepers and scaled by `BASE_PARAMS` + uint64 slippageFee; + // Portion of the fees from users minting and burning + // that goes to SLPs (the rest goes to surplus) + uint64 feesForSLPs; + // Slippage factor that's applied to SLPs exiting (depends on collateral ratio) + // If `slippage = BASE_PARAMS`, SLPs can get nothing, if `slippage = 0` they get their full claim + // Updated by keepers and scaled by `BASE_PARAMS` + uint64 slippage; + // Portion of the interests from lending + // that goes to SLPs (the rest goes to surplus) + uint64 interestsForSLPs; +} + +/// @title IStableMasterFunctions +/// @author Angle Core Team +/// @notice Interface for the `StableMaster` contract +interface IStableMasterFunctions { + function deploy( + address[] memory _governorList, + address _guardian, + address _agToken + ) external; + + // ============================== Lending ====================================== + + function accumulateInterest(uint256 gain) external; + + function signalLoss(uint256 loss) external; + + // ============================== HAs ========================================== + + function getStocksUsers() external view returns (uint256 maxCAmountInStable); + + function convertToSLP(uint256 amount, address user) external; + + // ============================== Keepers ====================================== + + function getCollateralRatio() external returns (uint256); + + function setFeeKeeper( + uint64 feeMint, + uint64 feeBurn, + uint64 _slippage, + uint64 _slippageFee + ) external; + + // ============================== AgToken ====================================== + + function updateStocksUsers(uint256 amount, address poolManager) external; + + // ============================= Governance ==================================== + + function setCore(address newCore) external; + + function addGovernor(address _governor) external; + + function removeGovernor(address _governor) external; + + function setGuardian(address newGuardian, address oldGuardian) external; + + function revokeGuardian(address oldGuardian) external; + + function setCapOnStableAndMaxInterests( + uint256 _capOnStableMinted, + uint256 _maxInterestsDistributed, + IPoolManager poolManager + ) external; + + function setIncentivesForSLPs( + uint64 _feesForSLPs, + uint64 _interestsForSLPs, + IPoolManager poolManager + ) external; + + function setUserFees( + IPoolManager poolManager, + uint64[] memory _xFee, + uint64[] memory _yFee, + uint8 _mint + ) external; + + function setTargetHAHedge(uint64 _targetHAHedge) external; + + function pause(bytes32 agent, IPoolManager poolManager) external; + + function unpause(bytes32 agent, IPoolManager poolManager) external; +} + +/// @title IStableMaster +/// @author Angle Core Team +/// @notice Previous interface with additionnal getters for public variables and mappings +interface IStableMaster is IStableMasterFunctions { + function agToken() external view returns (address); + + function collateralMap(IPoolManager poolManager) + external + view + returns ( + IERC20 token, + ISanToken sanToken, + IPerpetualManager perpetualManager, + IOracle oracle, + uint256 stocksUsers, + uint256 sanRate, + uint256 collatBase, + SLPData memory slpData, + MintBurnData memory feeData + ); +} diff --git a/contracts/interfaces/IStableMasterFront.sol b/contracts/interfaces/IStableMasterFront.sol new file mode 100644 index 0000000..2728f06 --- /dev/null +++ b/contracts/interfaces/IStableMasterFront.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "../interfaces/IPoolManager.sol"; + +/// @title IStableMasterFront +/// @author Angle Core Team +/// @dev Front interface, meaning only user-facing functions +interface IStableMasterFront { + function mint( + uint256 amount, + address user, + IPoolManager poolManager, + uint256 minStableAmount + ) external; + + function burn( + uint256 amount, + address burner, + address dest, + IPoolManager poolManager, + uint256 minCollatAmount + ) external; + + function deposit( + uint256 amount, + address user, + IPoolManager poolManager + ) external; + + function withdraw( + uint256 amount, + address burner, + address dest, + IPoolManager poolManager + ) external; + + function agToken() external view returns (address); +} diff --git a/contracts/interfaces/IStakingRewards.sol b/contracts/interfaces/IStakingRewards.sol new file mode 100644 index 0000000..135097f --- /dev/null +++ b/contracts/interfaces/IStakingRewards.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title IStakingRewardsFunctions +/// @author Angle Core Team +/// @notice Interface for the staking rewards contract that interact with the `RewardsDistributor` contract +interface IStakingRewardsFunctions { + function notifyRewardAmount(uint256 reward) external; + + function recoverERC20( + address tokenAddress, + address to, + uint256 tokenAmount + ) external; + + function setNewRewardsDistribution(address newRewardsDistribution) external; +} + +/// @title IStakingRewards +/// @author Angle Core Team +/// @notice Previous interface with additionnal getters for public variables +interface IStakingRewards is IStakingRewardsFunctions { + function rewardToken() external view returns (IERC20); +} diff --git a/contracts/interfaces/IStrategy.sol b/contracts/interfaces/IStrategy.sol new file mode 100644 index 0000000..782eaf3 --- /dev/null +++ b/contracts/interfaces/IStrategy.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./IAccessControl.sol"; + +/// @title IStrategy +/// @author Inspired by Yearn with slight changes from Angle Core Team +/// @notice Interface for yield farming strategies +interface IStrategy is IAccessControl { + function estimatedAPR() external view returns (uint256); + + function poolManager() external view returns (address); + + function want() external view returns (address); + + function isActive() external view returns (bool); + + function estimatedTotalAssets() external view returns (uint256); + + function harvestTrigger(uint256 callCost) external view returns (bool); + + function harvest() external; + + function withdraw(uint256 _amountNeeded) external returns (uint256 amountFreed, uint256 _loss); + + function setEmergencyExit() external; + + function addGuardian(address _guardian) external; + + function revokeGuardian(address _guardian) external; +} diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol new file mode 100644 index 0000000..8beb43f --- /dev/null +++ b/contracts/interfaces/ITimelock.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0 + +// Forked from https://github.com/compound-finance/compound-protocol/tree/master/contracts/Governance + +pragma solidity ^0.8.7; + +interface ITimelock { + function delay() external view returns (uint256); + + // solhint-disable-next-line func-name-mixedcase + function GRACE_PERIOD() external view returns (uint256); + + function acceptAdmin() external; + + function queuedTransactions(bytes32 hash) external view returns (bool); + + function queueTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external returns (bytes32); + + function cancelTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external; + + function executeTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external payable returns (bytes memory); +} + +interface ANGLEInterface { + function getPriorVotes(address account, uint256 blockNumber) external view returns (uint96); +} diff --git a/contracts/interfaces/ITreasury.sol b/contracts/interfaces/ITreasury.sol new file mode 100644 index 0000000..3c72db6 --- /dev/null +++ b/contracts/interfaces/ITreasury.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +/// @title ITreasury +/// @author Angle Core Team +/// @notice Interface for the `Treasury` contract +/// @dev This interface only contains functions of the `Treasury` which are called by other contracts +/// of this module +interface ITreasury { + /// @notice Checks whether a given address has well been initialized in this contract + /// as a `VaultManager`` + /// @param _vaultManager Address to check + /// @return Whether the address has been initialized or not + function isVaultManager(address _vaultManager) external view returns (bool); +} diff --git a/contracts/interfaces/IVaultManager.sol b/contracts/interfaces/IVaultManager.sol new file mode 100644 index 0000000..d16dd77 --- /dev/null +++ b/contracts/interfaces/IVaultManager.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./ITreasury.sol"; + +// ========================= Key Structs and Enums ============================= + +/// @notice Data to track during a series of action the amount to give or receive in stablecoins and collateral +/// to the caller or associated addresses +struct PaymentData { + // Stablecoin amount the contract should give + uint256 stablecoinAmountToGive; + // Stablecoin amount owed to the contract + uint256 stablecoinAmountToReceive; + // Collateral amount the contract should give + uint256 collateralAmountToGive; + // Collateral amount owed to the contract + uint256 collateralAmountToReceive; +} + +/// @notice Data stored to track someone's loan (or equivalently called position) +struct Vault { + // Amount of collateral deposited in the vault + uint256 collateralAmount; + // Normalized value of the debt (that is to say of the stablecoins borrowed) + uint256 normalizedDebt; +} + +/// @notice Actions possible when composing calls to the different entry functions proposed +enum ActionBorrowType { + createVault, + closeVault, + addCollateral, + removeCollateral, + repayDebt, + borrow, + getDebtIn, + permit +} + +// ========================= Interfaces ============================= + +/// @title IVaultManagerFunctions +/// @author Angle Core Team +/// @notice Interface for the `VaultManager` contract +/// @dev This interface only contains functions of the contract which are called by other contracts +/// of this module (without getters) +interface IVaultManagerFunctions { + /// @notice Allows composability between calls to the different entry points of this module. Any user calling + /// this function can perform any of the allowed actions in the order of their choice + /// @param actions Set of actions to perform + /// @param datas Data to be decoded for each action: it can include like the `vaultID` or the + /// @param from Address from which stablecoins will be taken if one action includes burning stablecoins. This address + /// should either be the `msg.sender` or be approved by the latter + /// @param to Address to which stablecoins and/or collateral will be sent in case of + /// @return paymentData Struct containing the final transfers executed + /// @dev This function is optimized to reduce gas cost due to payment from or to the user and that expensive calls + /// or computations (like `oracleValue`) are done only once + function angle( + ActionBorrowType[] memory actions, + bytes[] memory datas, + address from, + address to + ) external payable returns (PaymentData memory paymentData); + + /// @notice Allows composability between calls to the different entry points of this module. Any user calling + /// this function can perform any of the allowed actions in the order of their choice + /// @param actions Set of actions to perform + /// @param datas Data to be decoded for each action: it can include like the `vaultID` or the + /// @param from Address from which stablecoins will be taken if one action includes burning stablecoins. This address + /// should either be the `msg.sender` or be approved by the latter + /// @param to Address to which stablecoins and/or collateral will be sent in case of + /// @param who Address of the contract to handle in case of repayment of stablecoins from received collateral + /// @param repayData Data to pass to the repayment contract in case of + /// @return paymentData Struct containing the final transfers executed + /// @dev This function is optimized to reduce gas cost due to payment from or to the user and that expensive calls + /// or computations (like `oracleValue`) are done only once + function angle( + ActionBorrowType[] memory actions, + bytes[] memory datas, + address from, + address to, + address who, + bytes memory repayData + ) external payable returns (PaymentData memory paymentData); + + /// @notice Checks whether a given address is approved for a vault or owns this vault + /// @param spender Address for which vault ownership should be checked + /// @param vaultID ID of the vault to check + /// @return Whether the `spender` address owns or is approved for `vaultID` + function isApprovedOrOwner(address spender, uint256 vaultID) external view returns (bool); + + /// @notice Allows an address to give or revoke approval for all its vaults to another address + /// @param owner Address signing the permit and giving (or revoking) its approval for all the controlled vaults + /// @param spender Address to give approval to + /// @param approved Whether to give or revoke the approval + /// @param deadline Deadline parameter for the signature to be valid + /// @dev The `v`, `r`, and `s` parameters are used as signature data + function permit( + address owner, + address spender, + bool approved, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} + +/// @title IVaultManagerStorage +/// @author Angle Core Team +/// @notice Interface for the `VaultManager` contract +/// @dev This interface contains getters of the contract's public variables used by other contracts +/// of this module +interface IVaultManagerStorage { + /// @notice Reference to the `treasury` contract handling this `VaultManager` + function treasury() external view returns (ITreasury); + + /// @notice Reference to the collateral handled by this `VaultManager` + function collateral() external view returns (IERC20); + + /// @notice ID of the last vault created. The `vaultIDCount` variables serves as a counter to generate a unique + /// `vaultID` for each vault: it is like `tokenID` in basic ERC721 contracts + function vaultIDCount() external view returns (uint256); +} diff --git a/contracts/interfaces/IVeANGLE.sol b/contracts/interfaces/IVeANGLE.sol new file mode 100644 index 0000000..53e254f --- /dev/null +++ b/contracts/interfaces/IVeANGLE.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +/// @title IVeANGLE +/// @author Angle Core Team +/// @notice Interface for the `VeANGLE` contract +interface IVeANGLE { + // solhint-disable-next-line func-name-mixedcase + function deposit_for(address addr, uint256 amount) external; +} diff --git a/contracts/interfaces/IWETH.sol b/contracts/interfaces/IWETH.sol new file mode 100644 index 0000000..1ca3256 --- /dev/null +++ b/contracts/interfaces/IWETH.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.0; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IWETH is IERC20 { + function deposit() external payable; + + function decimals() external view returns (uint256); + + function withdraw(uint256) external; +} diff --git a/contracts/interfaces/external/IWETH9.sol b/contracts/interfaces/external/IWETH9.sol new file mode 100644 index 0000000..443a0a4 --- /dev/null +++ b/contracts/interfaces/external/IWETH9.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Interface for WETH9 +interface IWETH9 is IERC20 { + /// @notice Deposit ether to get wrapped ether + function deposit() external payable; + + /// @notice Withdraw wrapped ether to get ether + function withdraw(uint256) external; +} diff --git a/contracts/interfaces/external/aave/DataTypes.sol b/contracts/interfaces/external/aave/DataTypes.sol new file mode 100644 index 0000000..9d094b1 --- /dev/null +++ b/contracts/interfaces/external/aave/DataTypes.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +library DataTypes { + // refer to the whitepaper, section 1.1 basic concepts for a formal description of these properties. + struct ReserveData { + //stores the reserve configuration + ReserveConfigurationMap configuration; + //the liquidity index. Expressed in ray + uint128 liquidityIndex; + //variable borrow index. Expressed in ray + uint128 variableBorrowIndex; + //the current supply rate. Expressed in ray + uint128 currentLiquidityRate; + //the current variable borrow rate. Expressed in ray + uint128 currentVariableBorrowRate; + //the current stable borrow rate. Expressed in ray + uint128 currentStableBorrowRate; + uint40 lastUpdateTimestamp; + //tokens addresses + address aTokenAddress; + address stableDebtTokenAddress; + address variableDebtTokenAddress; + //address of the interest rate strategy + address interestRateStrategyAddress; + //the id of the reserve. Represents the position in the list of the active reserves + uint8 id; + } + + struct ReserveConfigurationMap { + //bit 0-15: LTV + //bit 16-31: Liq. threshold + //bit 32-47: Liq. bonus + //bit 48-55: Decimals + //bit 56: Reserve is active + //bit 57: reserve is frozen + //bit 58: borrowing is enabled + //bit 59: stable rate borrowing enabled + //bit 60-63: reserved + //bit 64-79: reserve factor + uint256 data; + } +} diff --git a/contracts/interfaces/external/aave/IAave.sol b/contracts/interfaces/external/aave/IAave.sol new file mode 100644 index 0000000..3476753 --- /dev/null +++ b/contracts/interfaces/external/aave/IAave.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import { DataTypes } from "./DataTypes.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IAaveIncentivesController { + function getRewardsBalance(address[] calldata assets, address user) external view returns (uint256); + + function claimRewards( + address[] calldata assets, + uint256 amount, + address to + ) external returns (uint256); + + function getDistributionEnd() external view returns (uint256); + + function getAssetData(address asset) + external + view + returns ( + uint256, + uint256, + uint256 + ); +} + +interface IAToken is IERC20 { + function getIncentivesController() external view returns (IAaveIncentivesController); +} + +interface ILendingPool { + function deposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external; + + function withdraw( + address asset, + uint256 amount, + address to + ) external returns (uint256); + + function getReserveData(address asset) external view returns (DataTypes.ReserveData memory); +} + +interface ILendingPoolAddressesProvider { + function getLendingPool() external view returns (address); +} + +interface IProtocolDataProvider { + function ADDRESSES_PROVIDER() external view returns (ILendingPoolAddressesProvider); + + function getReserveConfigurationData(address asset) + external + view + returns ( + uint256 decimals, + uint256 ltv, + uint256 liquidationThreshold, + uint256 liquidationBonus, + uint256 reserveFactor, + bool usageAsCollateralEnabled, + bool borrowingEnabled, + bool stableBorrowRateEnabled, + bool isActive, + bool isFrozen + ); + + function getReserveData(address asset) + external + view + returns ( + uint256 availableLiquidity, + uint256 totalStableDebt, + uint256 totalVariableDebt, + uint256 liquidityRate, + uint256 variableBorrowRate, + uint256 stableBorrowRate, + uint256 averageStableBorrowRate, + uint256 liquidityIndex, + uint256 variableBorrowIndex, + uint40 lastUpdateTimestamp + ); +} + +interface IReserveInterestRateStrategy { + function calculateInterestRates( + address reserve, + uint256 utilizationRate, + uint256 totalStableDebt, + uint256 totalVariableDebt, + uint256 averageStableBorrowRate, + uint256 reserveFactor + ) + external + view + returns ( + uint256 liquidityRate, + uint256 stableBorrowRate, + uint256 variableBorrowRate + ); +} + +interface IStakedAave { + function stake(address to, uint256 amount) external; + + function redeem(address to, uint256 amount) external; + + function cooldown() external; + + function claimRewards(address to, uint256 amount) external; + + function getTotalRewardsBalance(address) external view returns (uint256); + + function COOLDOWN_SECONDS() external view returns (uint256); + + function stakersCooldowns(address) external view returns (uint256); + + function UNSTAKE_WINDOW() external view returns (uint256); +} diff --git a/contracts/interfaces/external/compound/CErc20I.sol b/contracts/interfaces/external/compound/CErc20I.sol new file mode 100755 index 0000000..03f24ba --- /dev/null +++ b/contracts/interfaces/external/compound/CErc20I.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +import "./CTokenI.sol"; + +interface CErc20I is CTokenI { + function mint(uint256 mintAmount) external returns (uint256); + + function redeemUnderlying(uint256 redeemAmount) external returns (uint256); + + function underlying() external view returns (address); + + function borrow(uint256 borrowAmount) external returns (uint256); +} diff --git a/contracts/interfaces/external/compound/CEtherI.sol b/contracts/interfaces/external/compound/CEtherI.sol new file mode 100644 index 0000000..f85cdcb --- /dev/null +++ b/contracts/interfaces/external/compound/CEtherI.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.5.0; + +import "./CTokenI.sol"; + +interface CEtherI is CTokenI { + function mint() external payable; + + function redeemUnderlying(uint256 redeemAmount) external returns (uint256); + + function borrow(uint256 borrowAmount) external returns (uint256); +} diff --git a/contracts/interfaces/external/compound/CTokenI.sol b/contracts/interfaces/external/compound/CTokenI.sol new file mode 100644 index 0000000..54d1941 --- /dev/null +++ b/contracts/interfaces/external/compound/CTokenI.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; +import "./InterestRateModel.sol"; + +interface CTokenI { + function balanceOf(address owner) external view returns (uint256); + + function balanceOfUnderlying(address owner) external returns (uint256); + + function supplyRatePerBlock() external view returns (uint256); + + function exchangeRateCurrent() external returns (uint256); + + function exchangeRateStored() external view returns (uint256); + + function interestRateModel() external view returns (InterestRateModel); + + function totalReserves() external view returns (uint256); + + function reserveFactorMantissa() external view returns (uint256); + + function totalBorrows() external view returns (uint256); + + function totalSupply() external view returns (uint256); +} diff --git a/contracts/interfaces/external/compound/IComptroller.sol b/contracts/interfaces/external/compound/IComptroller.sol new file mode 100644 index 0000000..1a7e2bb --- /dev/null +++ b/contracts/interfaces/external/compound/IComptroller.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./CTokenI.sol"; + +interface IComptroller { + function compSupplySpeeds(address cToken) external view returns (uint256); + + function claimComp( + address[] memory holders, + CTokenI[] memory cTokens, + bool borrowers, + bool suppliers + ) external; +} diff --git a/contracts/interfaces/external/compound/InterestRateModel.sol b/contracts/interfaces/external/compound/InterestRateModel.sol new file mode 100755 index 0000000..42acca1 --- /dev/null +++ b/contracts/interfaces/external/compound/InterestRateModel.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +interface InterestRateModel { + /** + * @notice Calculates the current supply interest rate per block + * @param cash The total amount of cash the market has + * @param borrows The total amount of borrows the market has outstanding + * @param reserves The total amount of reserves the market has + * @param reserveFactorMantissa The current reserve factor the market has + * @return The supply rate per block (as a percentage, and scaled by 1e18) + */ + function getSupplyRate( + uint256 cash, + uint256 borrows, + uint256 reserves, + uint256 reserveFactorMantissa + ) external view returns (uint256); + + // Rinkeby function + function getBorrowRate( + uint256 cash, + uint256 borrows, + uint256 _reserves + ) external view returns (uint256, uint256); +} diff --git a/contracts/interfaces/external/curve/Curve.sol b/contracts/interfaces/external/curve/Curve.sol new file mode 100644 index 0000000..3b267d5 --- /dev/null +++ b/contracts/interfaces/external/curve/Curve.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.7; + +interface ICurveFi { + function get_virtual_price() external view returns (uint256); + + function add_liquidity( + // sBTC pool + uint256[3] calldata amounts, + uint256 min_mint_amount + ) external; + + function add_liquidity( + // bUSD pool + uint256[4] calldata amounts, + uint256 min_mint_amount + ) external; + + function add_liquidity( + // stETH pool + uint256[2] calldata amounts, + uint256 min_mint_amount + ) external payable; + + function remove_liquidity_imbalance(uint256[4] calldata amounts, uint256 max_burn_amount) external; + + function remove_liquidity(uint256 _amount, uint256[4] calldata amounts) external; + + function remove_liquidity_one_coin( + uint256 _token_amount, + int128 i, + uint256 min_amount + ) external; + + function exchange( + int128 from, + int128 to, + uint256 _from_amount, + uint256 _min_to_amount + ) external payable; + + function balances(int128) external view returns (uint256); + + function get_dy( + int128 from, + int128 to, + uint256 _from_amount + ) external view returns (uint256); + + function calc_token_amount(uint256[2] calldata amounts, bool is_deposit) external view returns (uint256); +} + +interface Zap { + function remove_liquidity_one_coin( + uint256, + int128, + uint256 + ) external; +} diff --git a/contracts/interfaces/external/lido/ISteth.sol b/contracts/interfaces/external/lido/ISteth.sol new file mode 100644 index 0000000..c5e9bde --- /dev/null +++ b/contracts/interfaces/external/lido/ISteth.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.7; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IStETH is IERC20 { + event Submitted(address sender, uint256 amount, address referral); + + function submit(address) external payable returns (uint256); + + function getSharesByPooledEth(uint256 _ethAmount) external view returns (uint256); +} diff --git a/contracts/interfaces/external/lido/IWStETH.sol b/contracts/interfaces/external/lido/IWStETH.sol new file mode 100644 index 0000000..b6f4500 --- /dev/null +++ b/contracts/interfaces/external/lido/IWStETH.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.7; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title IWStETH +/// @author Angle Core Team +/// @notice Interface for the `WStETH` contract +/// @dev This interface only contains functions of the `WStETH` which are called by other contracts +/// of this module +interface IWStETH is IERC20 { + function stETH() external returns (address); + + function wrap(uint256 _stETHAmount) external returns (uint256); +} diff --git a/contracts/interfaces/external/uniswap/IUniswapRouter.sol b/contracts/interfaces/external/uniswap/IUniswapRouter.sol new file mode 100644 index 0000000..c692147 --- /dev/null +++ b/contracts/interfaces/external/uniswap/IUniswapRouter.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; +} + +/// @title Router token swapping functionality +/// @notice Functions for swapping tokens via Uniswap V3 +interface IUniswapV3Router { + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata + /// @return amountOut The amount of the received token + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); +} + +/// @title Router for price estimation functionality +/// @notice Functions for getting the price of one token with respect to another using Uniswap V2 +/// @dev This interface is only used for non critical elements of the protocol +interface IUniswapV2Router { + /// @notice Given an input asset amount, returns the maximum output amount of the + /// other asset (accounting for fees) given reserves. + /// @param path Addresses of the pools used to get prices + function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); + + function swapExactTokensForTokens( + uint256 swapAmount, + uint256 minExpected, + address[] calldata path, + address receiver, + uint256 swapDeadline + ) external; +} diff --git a/contracts/staking/AngleDistributor.sol b/contracts/staking/AngleDistributor.sol new file mode 100644 index 0000000..597befc --- /dev/null +++ b/contracts/staking/AngleDistributor.sol @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./AngleDistributorEvents.sol"; + +/// @title AngleDistributor +/// @author Forked from contracts developed by Curve and Frax and adapted by Angle Core Team +/// - ERC20CRV.vy (https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/ERC20CRV.vy) +/// - FraxGaugeFXSRewardsDistributor.sol (https://github.com/FraxFinance/frax-solidity/blob/master/src/hardhat/contracts/Curve/FraxGaugeFXSRewardsDistributor.sol) +/// @notice All the events used in `AngleDistributor` contract +contract AngleDistributor is AngleDistributorEvents, ReentrancyGuardUpgradeable, AccessControlUpgradeable { + using SafeERC20 for IERC20; + + /// @notice Role for governors only + bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE"); + /// @notice Role for the guardian + bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE"); + + /// @notice Length of a week in seconds + uint256 public constant WEEK = 3600 * 24 * 7; + + /// @notice Time at which the emission rate is updated + uint256 public constant RATE_REDUCTION_TIME = WEEK; + + /// @notice Reduction of the emission rate + uint256 public constant RATE_REDUCTION_COEFFICIENT = 1007827884862117171; // 1.5 ^ (1/52) * 10**18 + + /// @notice Base used for computation + uint256 public constant BASE = 10**18; + + /// @notice Maps the address of a gauge to the last time this gauge received rewards + mapping(address => uint256) public lastTimeGaugePaid; + + /// @notice Maps the address of a gauge to whether it was killed or not + /// A gauge killed in this contract cannot receive any rewards + mapping(address => bool) public killedGauges; + + /// @notice Maps the address of a type >= 2 gauge to a delegate address responsible + /// for giving rewards to the actual gauge + mapping(address => address) public delegateGauges; + + /// @notice Maps the address of a gauge delegate to whether this delegate supports the `notifyReward` interface + /// and is therefore built for automation + mapping(address => bool) public isInterfaceKnown; + + /// @notice Address of the ANGLE token given as a reward + IERC20 public rewardToken; + + /// @notice Address of the `GaugeController` contract + IGaugeController public controller; + + /// @notice Address responsible for pulling rewards of type >= 2 gauges and distributing it to the + /// associated contracts if there is not already an address delegated for this specific contract + address public delegateGauge; + + /// @notice ANGLE current emission rate, it is first defined in the initializer and then updated every week + uint256 public rate; + + /// @notice Timestamp at which the current emission epoch started + uint256 public startEpochTime; + + /// @notice Amount of ANGLE tokens distributed through staking at the start of the epoch + /// This is an informational variable used to track how much has been distributed through liquidity mining + uint256 public startEpochSupply; + + /// @notice Index of the current emission epoch + /// Here also, this variable is not useful per se inside the smart contracts of the protocol, it is + /// just an informational variable + uint256 public miningEpoch; + + /// @notice Whether ANGLE distribution through this contract is on or no + bool public distributionsOn; + + /// @notice Constructor of the contract + /// @param _rewardToken Address of the ANGLE token + /// @param _controller Address of the GaugeController + /// @param _initialRate Initial ANGLE emission rate + /// @param _startEpochSupply Amount of ANGLE tokens already distributed via liquidity mining + /// @param governor Governor address of the contract + /// @param guardian Address of the guardian of this contract + /// @param _delegateGauge Address that will be used to pull rewards for type 2 gauges + /// @dev After this contract is created, the correct amount of ANGLE tokens should be transferred to the contract + /// @dev The `_delegateGauge` can be the zero address + function initialize( + address _rewardToken, + address _controller, + uint256 _initialRate, + uint256 _startEpochSupply, + address governor, + address guardian, + address _delegateGauge + ) external initializer { + require( + _controller != address(0) && _rewardToken != address(0) && guardian != address(0) && governor != address(0), + "0" + ); + rewardToken = IERC20(_rewardToken); + controller = IGaugeController(_controller); + startEpochSupply = _startEpochSupply; + miningEpoch = 0; + // Some ANGLE tokens should be sent to the contract directly after initialization + rate = _initialRate; + delegateGauge = _delegateGauge; + distributionsOn = false; + startEpochTime = block.timestamp; + _setRoleAdmin(GOVERNOR_ROLE, GOVERNOR_ROLE); + _setRoleAdmin(GUARDIAN_ROLE, GOVERNOR_ROLE); + _setupRole(GUARDIAN_ROLE, guardian); + _setupRole(GOVERNOR_ROLE, governor); + _setupRole(GUARDIAN_ROLE, governor); + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() initializer {} + + // ======================== Internal Functions ================================= + + /// @notice Internal function to distribute rewards to a gauge + /// @param gaugeAddr Address of the gauge to distribute rewards to + /// @return weeksElapsed Weeks elapsed since the last call + /// @return rewardTally Amount of rewards distributed to the gauge + /// @dev The reason for having an internal function is that it's called by the `distributeReward` and the + /// `distributeRewardToMultipleGauges` + /// @dev Although they would need to be performed all the time this function is called, this function does not + /// contain checks on whether distribution is on, and on whether rate should be reduced. These are done in each external + /// function calling this function for gas efficiency + function _distributeReward(address gaugeAddr) internal returns (uint256 weeksElapsed, uint256 rewardTally) { + // Checking if the gauge has been added or if it still possible to distribute rewards to this gauge + int128 gaugeType = IGaugeController(controller).gauge_types(gaugeAddr); + require(gaugeType >= 0 && !killedGauges[gaugeAddr], "110"); + + // Calculate the elapsed time in weeks. + uint256 lastTimePaid = lastTimeGaugePaid[gaugeAddr]; + + // Edge case for first reward for this gauge + if (lastTimePaid == 0) { + weeksElapsed = 1; + if (gaugeType == 0) { + // We give a full approval for the gauges with type zero which correspond to the staking + // contracts of the protocol + rewardToken.safeApprove(gaugeAddr, type(uint256).max); + } + } else { + // Truncation desired + weeksElapsed = (block.timestamp - lastTimePaid) / WEEK; + // Return early here for 0 weeks instead of throwing, as it could have bad effects in other contracts + if (weeksElapsed == 0) { + return (0, 0); + } + } + rewardTally = 0; + // We use this variable to keep track of the emission rate across different weeks + uint256 weeklyRate = rate; + for (uint256 i = 0; i < weeksElapsed; i++) { + uint256 relWeightAtWeek; + if (i == 0) { + // Mutative, for the current week: makes sure the weight is checkpointed. Also returns the weight. + relWeightAtWeek = controller.gauge_relative_weight_write(gaugeAddr, block.timestamp); + } else { + // View + relWeightAtWeek = controller.gauge_relative_weight(gaugeAddr, (block.timestamp - WEEK * i)); + } + rewardTally += (weeklyRate * relWeightAtWeek * WEEK) / BASE; + + // To get the rate of the week prior from the current rate we just have to multiply by the weekly division + // factor + // There may be some precisions error: inferred previous values of the rate may be different to what we would + // have had if the rate had been computed correctly in these weeks: we expect from empirical observations + // this `weeklyRate` to be inferior to what the `rate` would have been + weeklyRate = (weeklyRate * RATE_REDUCTION_COEFFICIENT) / BASE; + } + + // Update the last time paid, rounded to the closest week + // in order not to have an ever moving time on when to call this function + lastTimeGaugePaid[gaugeAddr] = (block.timestamp / WEEK) * WEEK; + + // If the `gaugeType >= 2`, this means that the gauge is a gauge on another chain (and corresponds to tokens + // that need to be bridged) or is associated to an external contract of the Angle Protocol + if (gaugeType >= 2) { + // If it is defined, we use the specific delegate attached to the gauge + address delegate = delegateGauges[gaugeAddr]; + if (delegate == address(0)) { + // If not, we check if a delegate common to all gauges with type >= 2 can be used + delegate = delegateGauge; + } + if (delegate != address(0)) { + // In the case where the gauge has a delegate (specific or not), then rewards are transferred to this gauge + rewardToken.safeTransfer(delegate, rewardTally); + // If this delegate supports a specific interface, then rewards sent are notified through this + // interface + if (isInterfaceKnown[delegate]) { + IAngleMiddlemanGauge(delegate).notifyReward(gaugeAddr, rewardTally); + } + } else { + rewardToken.safeTransfer(gaugeAddr, rewardTally); + } + } else if (gaugeType == 1) { + // This is for the case of Perpetual contracts which need to be able to receive their reward tokens + rewardToken.safeTransfer(gaugeAddr, rewardTally); + IStakingRewards(gaugeAddr).notifyRewardAmount(rewardTally); + } else { + // Mainnet: Pay out the rewards directly to the gauge + ILiquidityGauge(gaugeAddr).deposit_reward_token(address(rewardToken), rewardTally); + } + emit RewardDistributed(gaugeAddr, rewardTally); + } + + /// @notice Updates mining rate and supply at the start of the epoch + /// @dev Any modifying mining call must also call this + /// @dev It is possible that more than one week past between two calls of this function, and for this reason + /// this function has been slightly modified from Curve implementation by Angle Team + function _updateMiningParameters() internal { + // When entering this function, we always have: `(block.timestamp - startEpochTime) / RATE_REDUCTION_TIME >= 1` + uint256 epochDelta = (block.timestamp - startEpochTime) / RATE_REDUCTION_TIME; + + // Storing intermediate values for the rate and for the `startEpochSupply` + uint256 _rate = rate; + uint256 _startEpochSupply = startEpochSupply; + + startEpochTime += RATE_REDUCTION_TIME * epochDelta; + miningEpoch += epochDelta; + + for (uint256 i = 0; i < epochDelta; i++) { + // Updating the intermediate values of the `startEpochSupply` + _startEpochSupply += _rate * RATE_REDUCTION_TIME; + _rate = (_rate * BASE) / RATE_REDUCTION_COEFFICIENT; + } + rate = _rate; + startEpochSupply = _startEpochSupply; + emit UpdateMiningParameters(block.timestamp, _rate, _startEpochSupply); + } + + /// @notice Toggles the fact that a gauge delegate can be used for automation or not and therefore supports + /// the `notifyReward` interface + /// @param _delegateGauge Address of the gauge to change + function _toggleInterfaceKnown(address _delegateGauge) internal { + bool isInterfaceKnownMem = isInterfaceKnown[_delegateGauge]; + isInterfaceKnown[_delegateGauge] = !isInterfaceKnownMem; + emit InterfaceKnownToggled(_delegateGauge, !isInterfaceKnownMem); + } + + // ================= Permissionless External Functions ========================= + + /// @notice Distributes rewards to a staking contract (also called gauge) + /// @param gaugeAddr Address of the gauge to send tokens too + /// @return weeksElapsed Number of weeks elapsed since the last time rewards were distributed + /// @return rewardTally Amount of tokens sent to the gauge + /// @dev Anyone can call this function to distribute rewards to the different staking contracts + function distributeReward(address gaugeAddr) external nonReentrant returns (uint256, uint256) { + // Checking if distribution is on + require(distributionsOn == true, "109"); + // Updating rate distribution parameters if need be + if (block.timestamp >= startEpochTime + RATE_REDUCTION_TIME) { + _updateMiningParameters(); + } + return _distributeReward(gaugeAddr); + } + + /// @notice Distributes rewards to multiple staking contracts + /// @param gauges Addresses of the gauge to send tokens too + /// @dev Anyone can call this function to distribute rewards to the different staking contracts + /// @dev Compared with the `distributeReward` function, this function sends rewards to multiple + /// contracts at the same time + function distributeRewardToMultipleGauges(address[] memory gauges) external nonReentrant { + // Checking if distribution is on + require(distributionsOn == true, "109"); + // Updating rate distribution parameters if need be + if (block.timestamp >= startEpochTime + RATE_REDUCTION_TIME) { + _updateMiningParameters(); + } + for (uint256 i = 0; i < gauges.length; i++) { + _distributeReward(gauges[i]); + } + } + + /// @notice Updates mining rate and supply at the start of the epoch + /// @dev Callable by any address, but only once per epoch + function updateMiningParameters() external { + require(block.timestamp >= startEpochTime + RATE_REDUCTION_TIME, "108"); + _updateMiningParameters(); + } + + // ========================= Governor Functions ================================ + + /// @notice Withdraws ERC20 tokens that could accrue on this contract + /// @param tokenAddress Address of the ERC20 token to withdraw + /// @param to Address to transfer to + /// @param amount Amount to transfer + /// @dev Added to support recovering LP Rewards and other mistaken tokens + /// from other systems to be distributed to holders + /// @dev This function could also be used to recover ANGLE tokens in case the rate got smaller + function recoverERC20( + address tokenAddress, + address to, + uint256 amount + ) external onlyRole(GOVERNOR_ROLE) { + // If the token is the ANGLE token, we need to make sure that governance is not going to withdraw + // too many tokens and that it'll be able to sustain the weekly distribution forever + // This check assumes that `distributeReward` has been called for gauges and that there are no gauges + // which have not received their past week's rewards + if (tokenAddress == address(rewardToken)) { + uint256 currentBalance = rewardToken.balanceOf(address(this)); + // The amount distributed till the end is `rate * WEEK / (1 - RATE_REDUCTION_FACTOR)` where + // `RATE_REDUCTION_FACTOR = BASE / RATE_REDUCTION_COEFFICIENT` which translates to: + require( + currentBalance >= + ((rate * RATE_REDUCTION_COEFFICIENT) * WEEK) / (RATE_REDUCTION_COEFFICIENT - BASE) + amount, + "4" + ); + } + IERC20(tokenAddress).safeTransfer(to, amount); + emit Recovered(tokenAddress, to, amount); + } + + /// @notice Sets a new gauge controller + /// @param _controller Address of the new gauge controller + function setGaugeController(address _controller) external onlyRole(GOVERNOR_ROLE) { + require(_controller != address(0), "0"); + controller = IGaugeController(_controller); + emit GaugeControllerUpdated(_controller); + } + + /// @notice Sets a new delegate gauge for pulling rewards of a type >= 2 gauges or of all type >= 2 gauges + /// @param gaugeAddr Gauge to change the delegate of + /// @param _delegateGauge Address of the new gauge delegate related to `gaugeAddr` + /// @param toggleInterface Whether we should toggle the fact that the `_delegateGauge` is built for automation or not + /// @dev This function can be used to remove delegating or introduce the pulling of rewards to a given address + /// @dev If `gaugeAddr` is the zero address, this function updates the delegate gauge common to all gauges with type >= 2 + /// @dev The `toggleInterface` parameter has been added for convenience to save one transaction when adding a gauge delegate + /// which supports the `notifyReward` interface + function setDelegateGauge( + address gaugeAddr, + address _delegateGauge, + bool toggleInterface + ) external onlyRole(GOVERNOR_ROLE) { + if (gaugeAddr != address(0)) { + delegateGauges[gaugeAddr] = _delegateGauge; + } else { + delegateGauge = _delegateGauge; + } + emit DelegateGaugeUpdated(gaugeAddr, _delegateGauge); + + if (toggleInterface) { + _toggleInterfaceKnown(_delegateGauge); + } + } + + /// @notice Changes the ANGLE emission rate + /// @param _newRate New ANGLE emission rate + /// @dev It is important to be super wary when calling this function and to make sure that `distributeReward` + /// has been called for all gauges in the past weeks. If not, gauges may get an incorrect distribution of ANGLE rewards + /// for these past weeks based on the new rate and not on the old rate + /// @dev Governance should thus make sure to call this function rarely and when it does to do it after the weekly `distributeReward` + /// calls for all existing gauges + /// @dev As this function assumes that `distributeReward` has been called during the week, it also assumes that the `startEpochSupply` + /// parameter has been put up to date + function setRate(uint256 _newRate) external onlyRole(GOVERNOR_ROLE) { + // Checking if the new rate is compatible with the amount of ANGLE tokens this contract has in balance + // This check assumes, like this function, that `distributeReward` has correctly been called before + require( + rewardToken.balanceOf(address(this)) >= + ((_newRate * RATE_REDUCTION_COEFFICIENT) * WEEK) / (RATE_REDUCTION_COEFFICIENT - BASE), + "4" + ); + rate = _newRate; + emit RateUpdated(_newRate); + } + + /// @notice Toggles the status of a gauge to either killed or unkilled + /// @param gaugeAddr Gauge to toggle the status of + /// @dev It is impossible to kill a gauge in the `GaugeController` contract, for this reason killing of gauges + /// takes place in the `AngleDistributor` contract + /// @dev This means that people could vote for a gauge in the gauge controller contract but that rewards are not going + /// to be distributed to it in the end: people would need to remove their weights on the gauge killed to end the diminution + /// in rewards + /// @dev In the case of a gauge being killed, this function resets the timestamps at which this gauge has been approved and + /// disapproves the gauge to spend the token + /// @dev It should be cautiously called by governance as it could result in less ANGLE overall rewards than initially planned + /// if people do not remove their voting weights to the killed gauge + function toggleGauge(address gaugeAddr) external onlyRole(GOVERNOR_ROLE) { + bool gaugeKilledMem = killedGauges[gaugeAddr]; + if (!gaugeKilledMem) { + delete lastTimeGaugePaid[gaugeAddr]; + rewardToken.safeApprove(gaugeAddr, 0); + } + killedGauges[gaugeAddr] = !gaugeKilledMem; + emit GaugeToggled(gaugeAddr, !gaugeKilledMem); + } + + // ========================= Guardian Function ================================= + + /// @notice Halts or activates distribution of rewards + function toggleDistributions() external onlyRole(GUARDIAN_ROLE) { + bool distributionsOnMem = distributionsOn; + distributionsOn = !distributionsOnMem; + emit DistributionsToggled(!distributionsOnMem); + } + + /// @notice Notifies that the interface of a gauge delegate is known or has changed + /// @param _delegateGauge Address of the gauge to change + /// @dev Gauge delegates that are built for automation should be toggled + function toggleInterfaceKnown(address _delegateGauge) external onlyRole(GUARDIAN_ROLE) { + _toggleInterfaceKnown(_delegateGauge); + } +} diff --git a/contracts/staking/AngleDistributorEvents.sol b/contracts/staking/AngleDistributorEvents.sol new file mode 100644 index 0000000..8e48844 --- /dev/null +++ b/contracts/staking/AngleDistributorEvents.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +import "../interfaces/IGaugeController.sol"; +import "../interfaces/ILiquidityGauge.sol"; +import "../interfaces/IAngleMiddlemanGauge.sol"; +import "../interfaces/IStakingRewards.sol"; + +import "../external/AccessControlUpgradeable.sol"; + +/// @title AngleDistributorEvents +/// @author Angle Core Team +/// @notice All the events used in `AngleDistributor` contract +contract AngleDistributorEvents { + event DelegateGaugeUpdated(address indexed _gaugeAddr, address indexed _delegateGauge); + event DistributionsToggled(bool _distributionsOn); + event GaugeControllerUpdated(address indexed _controller); + event GaugeToggled(address indexed gaugeAddr, bool newStatus); + event InterfaceKnownToggled(address indexed _delegateGauge, bool _isInterfaceKnown); + event RateUpdated(uint256 _newRate); + event Recovered(address indexed tokenAddress, address indexed to, uint256 amount); + event RewardDistributed(address indexed gaugeAddr, uint256 rewardTally); + event UpdateMiningParameters(uint256 time, uint256 rate, uint256 supply); +} diff --git a/contracts/staking/GaugeController.vy b/contracts/staking/GaugeController.vy new file mode 100644 index 0000000..da35d28 --- /dev/null +++ b/contracts/staking/GaugeController.vy @@ -0,0 +1,605 @@ +# @version 0.2.16 + +""" +@title Gauge Controller +@author Angle Protocol +@license MIT +@notice Controls liquidity gauges and the issuance of coins through the gauges +""" + +# Full fork from: +# Curve Finance's gauge controller +# https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/GaugeController.vy + +# 7 * 86400 seconds - all future times are rounded by week +WEEK: constant(uint256) = 604800 + +# Cannot change weight votes more often than once in 10 days +WEIGHT_VOTE_DELAY: constant(uint256) = 10 * 86400 + + +struct Point: + bias: uint256 + slope: uint256 + +struct VotedSlope: + slope: uint256 + power: uint256 + end: uint256 + + +interface VotingEscrow: + def get_last_user_slope(addr: address) -> int128: view + def locked__end(addr: address) -> uint256: view + + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event AddType: + name: String[64] + type_id: int128 + +event NewTypeWeight: + type_id: int128 + time: uint256 + weight: uint256 + total_weight: uint256 + +event NewGaugeWeight: + gauge_address: address + time: uint256 + weight: uint256 + total_weight: uint256 + +event VoteForGauge: + time: uint256 + user: address + gauge_addr: address + weight: uint256 + +event NewGauge: + addr: address + gauge_type: int128 + weight: uint256 + +event KilledGauge: + addr: address + +MULTIPLIER: constant(uint256) = 10 ** 18 + +admin: public(address) # Can and will be a smart contract +future_admin: public(address) # Can and will be a smart contract + +token: public(address) # ANGLE token +voting_escrow: public(address) # Voting escrow + +# Gauge parameters +# All numbers are "fixed point" on the basis of 1e18 +n_gauge_types: public(int128) +n_gauges: public(int128) +gauge_type_names: public(HashMap[int128, String[64]]) + +# Needed for enumeration +gauges: public(address[1000000000]) + +# we increment values by 1 prior to storing them here so we can rely on a value +# of zero as meaning the gauge has not been set +gauge_types_: HashMap[address, int128] + +vote_user_slopes: public(HashMap[address, HashMap[address, VotedSlope]]) # user -> gauge_addr -> VotedSlope +vote_user_power: public(HashMap[address, uint256]) # Total vote power used by user +last_user_vote: public(HashMap[address, HashMap[address, uint256]]) # Last user vote's timestamp for each gauge address + +# Past and scheduled points for gauge weight, sum of weights per type, total weight +# Point is for bias+slope +# changes_* are for changes in slope +# time_* are for the last change timestamp +# timestamps are rounded to whole weeks + +points_weight: public(HashMap[address, HashMap[uint256, Point]]) # gauge_addr -> time -> Point +changes_weight: HashMap[address, HashMap[uint256, uint256]] # gauge_addr -> time -> slope +time_weight: public(HashMap[address, uint256]) # gauge_addr -> last scheduled time (next week) + +points_sum: public(HashMap[int128, HashMap[uint256, Point]]) # type_id -> time -> Point +changes_sum: HashMap[int128, HashMap[uint256, uint256]] # type_id -> time -> slope +time_sum: public(uint256[1000000000]) # type_id -> last scheduled time (next week) + +points_total: public(HashMap[uint256, uint256]) # time -> total weight +time_total: public(uint256) # last scheduled time + +points_type_weight: public(HashMap[int128, HashMap[uint256, uint256]]) # type_id -> time -> type weight +time_type_weight: public(uint256[1000000000]) # type_id -> last scheduled time (next week) + + +@external +def __init__(_token: address, _voting_escrow: address, _admin: address): + """ + @notice Contract constructor + @param _token `ERC20ANGLE` contract address + @param _voting_escrow `VotingEscrow` contract address + """ + assert _token != ZERO_ADDRESS + assert _voting_escrow != ZERO_ADDRESS + assert _admin != ZERO_ADDRESS + + self.admin = _admin + self.token = _token + self.voting_escrow = _voting_escrow + self.time_total = block.timestamp / WEEK * WEEK + + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of GaugeController to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + assert addr != ZERO_ADDRESS # dev: future admin cannot be the 0 address + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + log ApplyOwnership(_admin) + + +@external +@view +def gauge_types(_addr: address) -> int128: + """ + @notice Get gauge type for address + @param _addr Gauge address + @return Gauge type id + """ + gauge_type: int128 = self.gauge_types_[_addr] + assert gauge_type != 0 + + return gauge_type - 1 + + +@internal +def _get_type_weight(gauge_type: int128) -> uint256: + """ + @notice Fill historic type weights week-over-week for missed checkins + and return the type weight for the future week + @param gauge_type Gauge type id + @return Type weight + """ + t: uint256 = self.time_type_weight[gauge_type] + if t > 0: + w: uint256 = self.points_type_weight[gauge_type][t] + for i in range(500): + if t > block.timestamp: + break + t += WEEK + self.points_type_weight[gauge_type][t] = w + if t > block.timestamp: + self.time_type_weight[gauge_type] = t + return w + else: + return 0 + + +@internal +def _get_sum(gauge_type: int128) -> uint256: + """ + @notice Fill sum of gauge weights for the same type week-over-week for + missed checkins and return the sum for the future week + @param gauge_type Gauge type id + @return Sum of weights + """ + t: uint256 = self.time_sum[gauge_type] + if t > 0: + pt: Point = self.points_sum[gauge_type][t] + for i in range(500): + if t > block.timestamp: + break + t += WEEK + d_bias: uint256 = pt.slope * WEEK + if pt.bias > d_bias: + pt.bias -= d_bias + d_slope: uint256 = self.changes_sum[gauge_type][t] + pt.slope -= d_slope + else: + pt.bias = 0 + pt.slope = 0 + self.points_sum[gauge_type][t] = pt + if t > block.timestamp: + self.time_sum[gauge_type] = t + return pt.bias + else: + return 0 + + +@internal +def _get_total() -> uint256: + """ + @notice Fill historic total weights week-over-week for missed checkins + and return the total for the future week + @return Total weight + """ + t: uint256 = self.time_total + _n_gauge_types: int128 = self.n_gauge_types + if t > block.timestamp: + # If we have already checkpointed - still need to change the value + t -= WEEK + pt: uint256 = self.points_total[t] + + for gauge_type in range(100): + if gauge_type == _n_gauge_types: + break + self._get_sum(gauge_type) + self._get_type_weight(gauge_type) + + for i in range(500): + if t > block.timestamp: + break + t += WEEK + pt = 0 + # Scales as n_types * n_unchecked_weeks (hopefully 1 at most) + for gauge_type in range(100): + if gauge_type == _n_gauge_types: + break + type_sum: uint256 = self.points_sum[gauge_type][t].bias + type_weight: uint256 = self.points_type_weight[gauge_type][t] + pt += type_sum * type_weight + self.points_total[t] = pt + + if t > block.timestamp: + self.time_total = t + return pt + + +@internal +def _get_weight(gauge_addr: address) -> uint256: + """ + @notice Fill historic gauge weights week-over-week for missed checkins + and return the total for the future week + @param gauge_addr Address of the gauge + @return Gauge weight + """ + t: uint256 = self.time_weight[gauge_addr] + if t > 0: + pt: Point = self.points_weight[gauge_addr][t] + for i in range(500): + if t > block.timestamp: + break + t += WEEK + d_bias: uint256 = pt.slope * WEEK + if pt.bias > d_bias: + pt.bias -= d_bias + d_slope: uint256 = self.changes_weight[gauge_addr][t] + pt.slope -= d_slope + else: + pt.bias = 0 + pt.slope = 0 + self.points_weight[gauge_addr][t] = pt + if t > block.timestamp: + self.time_weight[gauge_addr] = t + return pt.bias + else: + return 0 + + +@external +def add_gauge(addr: address, gauge_type: int128, weight: uint256 = 0): + """ + @notice Add gauge `addr` of type `gauge_type` with weight `weight` + @param addr Gauge address + @param gauge_type Gauge type + @param weight Gauge weight + """ + assert msg.sender == self.admin + assert (gauge_type >= 0) and (gauge_type < self.n_gauge_types) + assert self.gauge_types_[addr] == 0 # dev: cannot add the same gauge twice + + n: int128 = self.n_gauges + self.n_gauges = n + 1 + self.gauges[n] = addr + + self.gauge_types_[addr] = gauge_type + 1 + next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK + + if weight > 0: + _type_weight: uint256 = self._get_type_weight(gauge_type) + _old_sum: uint256 = self._get_sum(gauge_type) + _old_total: uint256 = self._get_total() + + self.points_sum[gauge_type][next_time].bias = weight + _old_sum + self.time_sum[gauge_type] = next_time + self.points_total[next_time] = _old_total + _type_weight * weight + self.time_total = next_time + + self.points_weight[addr][next_time].bias = weight + + if self.time_sum[gauge_type] == 0: + self.time_sum[gauge_type] = next_time + self.time_weight[addr] = next_time + + log NewGauge(addr, gauge_type, weight) + + +@external +def checkpoint(): + """ + @notice Checkpoint to fill data common for all gauges + """ + self._get_total() + + +@external +def checkpoint_gauge(addr: address): + """ + @notice Checkpoint to fill data for both a specific gauge and common for all gauges + @param addr Gauge address + """ + self._get_weight(addr) + self._get_total() + + +@internal +@view +def _gauge_relative_weight(addr: address, time: uint256) -> uint256: + """ + @notice Get Gauge relative weight (not more than 1.0) normalized to 1e18 + (e.g. 1.0 == 1e18). Inflation which will be received by it is + inflation_rate * relative_weight / 1e18 + @param addr Gauge address + @param time Relative weight at the specified timestamp in the past or present + @return Value of relative weight normalized to 1e18 + """ + t: uint256 = time / WEEK * WEEK + _total_weight: uint256 = self.points_total[t] + + if _total_weight > 0: + gauge_type: int128 = self.gauge_types_[addr] - 1 + _type_weight: uint256 = self.points_type_weight[gauge_type][t] + _gauge_weight: uint256 = self.points_weight[addr][t].bias + return MULTIPLIER * _type_weight * _gauge_weight / _total_weight + + else: + return 0 + + +@external +@view +def gauge_relative_weight(addr: address, time: uint256 = block.timestamp) -> uint256: + """ + @notice Get Gauge relative weight (not more than 1.0) normalized to 1e18 + (e.g. 1.0 == 1e18). Inflation which will be received by it is + inflation_rate * relative_weight / 1e18 + @param addr Gauge address + @param time Relative weight at the specified timestamp in the past or present + @return Value of relative weight normalized to 1e18 + """ + return self._gauge_relative_weight(addr, time) + + +@external +def gauge_relative_weight_write(addr: address, time: uint256 = block.timestamp) -> uint256: + """ + @notice Get gauge weight normalized to 1e18 and also fill all the unfilled + values for type and gauge records + @dev Any address can call, however nothing is recorded if the values are filled already + @param addr Gauge address + @param time Relative weight at the specified timestamp in the past or present + @return Value of relative weight normalized to 1e18 + """ + self._get_weight(addr) + self._get_total() # Also calculates get_sum + return self._gauge_relative_weight(addr, time) + + + + +@internal +def _change_type_weight(type_id: int128, weight: uint256): + """ + @notice Change type weight + @param type_id Type id + @param weight New type weight + """ + old_weight: uint256 = self._get_type_weight(type_id) + old_sum: uint256 = self._get_sum(type_id) + _total_weight: uint256 = self._get_total() + next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK + + _total_weight = _total_weight + old_sum * weight - old_sum * old_weight + self.points_total[next_time] = _total_weight + self.points_type_weight[type_id][next_time] = weight + self.time_total = next_time + self.time_type_weight[type_id] = next_time + + log NewTypeWeight(type_id, next_time, weight, _total_weight) + + +@external +def add_type(_name: String[64], weight: uint256 = 0): + """ + @notice Add gauge type with name `_name` and weight `weight` + @param _name Name of gauge type + @param weight Weight of gauge type + """ + assert msg.sender == self.admin + type_id: int128 = self.n_gauge_types + self.gauge_type_names[type_id] = _name + self.n_gauge_types = type_id + 1 + if weight != 0: + self._change_type_weight(type_id, weight) + log AddType(_name, type_id) + + +@external +def change_type_weight(type_id: int128, weight: uint256): + """ + @notice Change gauge type `type_id` weight to `weight` + @param type_id Gauge type id + @param weight New Gauge weight + """ + assert msg.sender == self.admin + self._change_type_weight(type_id, weight) + + +@internal +def _change_gauge_weight(addr: address, weight: uint256): + # Change gauge weight + # Only needed when testing in reality + gauge_type: int128 = self.gauge_types_[addr] - 1 + old_gauge_weight: uint256 = self._get_weight(addr) + type_weight: uint256 = self._get_type_weight(gauge_type) + old_sum: uint256 = self._get_sum(gauge_type) + _total_weight: uint256 = self._get_total() + next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK + + self.points_weight[addr][next_time].bias = weight + self.time_weight[addr] = next_time + + new_sum: uint256 = old_sum + weight - old_gauge_weight + self.points_sum[gauge_type][next_time].bias = new_sum + self.time_sum[gauge_type] = next_time + + _total_weight = _total_weight + new_sum * type_weight - old_sum * type_weight + self.points_total[next_time] = _total_weight + self.time_total = next_time + + log NewGaugeWeight(addr, block.timestamp, weight, _total_weight) + + +@external +def change_gauge_weight(addr: address, weight: uint256): + """ + @notice Change weight of gauge `addr` to `weight` + @param addr `GaugeController` contract address + @param weight New Gauge weight + """ + assert msg.sender == self.admin + self._change_gauge_weight(addr, weight) + + +@external +def vote_for_gauge_weights(_gauge_addr: address, _user_weight: uint256): + """ + @notice Allocate voting power for changing pool weights + @param _gauge_addr Gauge which `msg.sender` votes for + @param _user_weight Weight for a gauge in bps (units of 0.01%). Minimal is 0.01%. Ignored if 0 + """ + escrow: address = self.voting_escrow + slope: uint256 = convert(VotingEscrow(escrow).get_last_user_slope(msg.sender), uint256) + lock_end: uint256 = VotingEscrow(escrow).locked__end(msg.sender) + _n_gauges: int128 = self.n_gauges + next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK + assert lock_end > next_time, "Your token lock expires too soon" + assert (_user_weight >= 0) and (_user_weight <= 10000), "You used all your voting power" + assert block.timestamp >= self.last_user_vote[msg.sender][_gauge_addr] + WEIGHT_VOTE_DELAY, "Cannot vote so often" + + gauge_type: int128 = self.gauge_types_[_gauge_addr] - 1 + assert gauge_type >= 0, "Gauge not added" + # Prepare slopes and biases in memory + old_slope: VotedSlope = self.vote_user_slopes[msg.sender][_gauge_addr] + old_dt: uint256 = 0 + if old_slope.end > next_time: + old_dt = old_slope.end - next_time + old_bias: uint256 = old_slope.slope * old_dt + new_slope: VotedSlope = VotedSlope({ + slope: slope * _user_weight / 10000, + end: lock_end, + power: _user_weight + }) + new_dt: uint256 = lock_end - next_time # dev: raises when expired + new_bias: uint256 = new_slope.slope * new_dt + + # Check and update powers (weights) used + power_used: uint256 = self.vote_user_power[msg.sender] + power_used = power_used + new_slope.power - old_slope.power + self.vote_user_power[msg.sender] = power_used + assert (power_used >= 0) and (power_used <= 10000), 'Used too much power' + + ## Remove old and schedule new slope changes + # Remove slope changes for old slopes + # Schedule recording of initial slope for next_time + old_weight_bias: uint256 = self._get_weight(_gauge_addr) + old_weight_slope: uint256 = self.points_weight[_gauge_addr][next_time].slope + old_sum_bias: uint256 = self._get_sum(gauge_type) + old_sum_slope: uint256 = self.points_sum[gauge_type][next_time].slope + + self.points_weight[_gauge_addr][next_time].bias = max(old_weight_bias + new_bias, old_bias) - old_bias + self.points_sum[gauge_type][next_time].bias = max(old_sum_bias + new_bias, old_bias) - old_bias + if old_slope.end > next_time: + self.points_weight[_gauge_addr][next_time].slope = max(old_weight_slope + new_slope.slope, old_slope.slope) - old_slope.slope + self.points_sum[gauge_type][next_time].slope = max(old_sum_slope + new_slope.slope, old_slope.slope) - old_slope.slope + else: + self.points_weight[_gauge_addr][next_time].slope += new_slope.slope + self.points_sum[gauge_type][next_time].slope += new_slope.slope + if old_slope.end > block.timestamp: + # Cancel old slope changes if they still didn't happen + self.changes_weight[_gauge_addr][old_slope.end] -= old_slope.slope + self.changes_sum[gauge_type][old_slope.end] -= old_slope.slope + # Add slope changes for new slopes + self.changes_weight[_gauge_addr][new_slope.end] += new_slope.slope + self.changes_sum[gauge_type][new_slope.end] += new_slope.slope + + self._get_total() + + self.vote_user_slopes[msg.sender][_gauge_addr] = new_slope + + # Record last action time + self.last_user_vote[msg.sender][_gauge_addr] = block.timestamp + + log VoteForGauge(block.timestamp, msg.sender, _gauge_addr, _user_weight) + + +@external +@view +def get_gauge_weight(addr: address) -> uint256: + """ + @notice Get current gauge weight + @param addr Gauge address + @return Gauge weight + """ + return self.points_weight[addr][self.time_weight[addr]].bias + + +@external +@view +def get_type_weight(type_id: int128) -> uint256: + """ + @notice Get current type weight + @param type_id Type id + @return Type weight + """ + return self.points_type_weight[type_id][self.time_type_weight[type_id]] + + +@external +@view +def get_total_weight() -> uint256: + """ + @notice Get current total (type-weighted) weight + @return Total weight + """ + return self.points_total[self.time_total] + + +@external +@view +def get_weights_sum_per_type(type_id: int128) -> uint256: + """ + @notice Get sum of gauge weights per type + @param type_id Type id + @return Sum of gauge weights + """ + return self.points_sum[type_id][self.time_sum[type_id]].bias + \ No newline at end of file diff --git a/contracts/staking/LiquidityGaugeV4.vy b/contracts/staking/LiquidityGaugeV4.vy new file mode 100644 index 0000000..a20241a --- /dev/null +++ b/contracts/staking/LiquidityGaugeV4.vy @@ -0,0 +1,633 @@ +# @version 0.2.16 +""" +@title Liquidity Gauge v4 +@author Angle Protocol +@license MIT +""" + +# Original idea and credit: +# Curve Finance's veCRV +# https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/gauges/LiquidityGaugeV4.vy +# Mostly forked from Curve, except that now there is no direct link between the gauge controller +# and the gauges. In this implementation, ANGLE rewards are like any other token rewards. + +from vyper.interfaces import ERC20 + +implements: ERC20 + +interface VotingEscrow: + def user_point_epoch(addr: address) -> uint256: view + def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view + +interface VotingEscrowBoost: + def adjusted_balance_of(_account: address) -> uint256: view + +interface ERC20Extended: + def symbol() -> String[26]: view + def decimals() -> uint256: view + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: address + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + +event RewardDataUpdate: + _token: indexed(address) + _amount: uint256 + +struct Reward: + token: address + distributor: address + period_finish: uint256 + rate: uint256 + last_update: uint256 + integral: uint256 + + +MAX_REWARDS: constant(uint256) = 8 +TOKENLESS_PRODUCTION: constant(uint256) = 40 +WEEK: constant(uint256) = 604800 + +ANGLE: public(address) +voting_escrow: public(address) +veBoost_proxy: public(address) + +staking_token: public(address) +decimal_staking_token: public(uint256) + +balanceOf: public(HashMap[address, uint256]) +totalSupply: public(uint256) +allowance: public(HashMap[address, HashMap[address, uint256]]) + +name: public(String[64]) +symbol: public(String[32]) + +working_balances: public(HashMap[address, uint256]) +working_supply: public(uint256) + +integrate_checkpoint_of: public(HashMap[address, uint256]) + +# For tracking external rewards +reward_count: public(uint256) +reward_tokens: public(address[MAX_REWARDS]) + +reward_data: public(HashMap[address, Reward]) + +# claimant -> default reward receiver +rewards_receiver: public(HashMap[address, address]) + +# reward token -> claiming address -> integral +reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) + +# user -> [uint128 claimable amount][uint128 claimed amount] +claim_data: HashMap[address, HashMap[address, uint256]] + +admin: public(address) +future_admin: public(address) + +initialized: public(bool) + + +@external +def __init__(): + """ + @notice Contract constructor + @dev The contract has an initializer to prevent the take over of the implementation + """ + assert self.initialized == False #dev: contract is already initialized + self.initialized = True + +@external +def initialize(_staking_token: address, _admin: address, _ANGLE: address, _voting_escrow: address, _veBoost_proxy: address, _distributor: address): + """ + @notice Contract initializer + @param _staking_token Liquidity Pool contract address + @param _admin Admin who can kill the gauge + @param _ANGLE Address of the ANGLE token + @param _voting_escrow Address of the veANGLE contract + @param _veBoost_proxy Address of the proxy contract used to query veANGLE balances and taking into account potential delegations + @param _distributor Address of the contract responsible for distributing ANGLE tokens to this gauge + """ + assert self.initialized == False #dev: contract is already initialized + self.initialized = True + + assert _admin != ZERO_ADDRESS + assert _ANGLE != ZERO_ADDRESS + assert _voting_escrow != ZERO_ADDRESS + assert _veBoost_proxy != ZERO_ADDRESS + assert _distributor != ZERO_ADDRESS + + self.admin = _admin + self.staking_token = _staking_token + self.decimal_staking_token = ERC20Extended(_staking_token).decimals() + + symbol: String[26] = ERC20Extended(_staking_token).symbol() + self.name = concat("Angle ", symbol, " Gauge") + self.symbol = concat(symbol, "-gauge") + self.ANGLE = _ANGLE + self.voting_escrow = _voting_escrow + self.veBoost_proxy = _veBoost_proxy + + # add in all liquidityGauge the ANGLE reward - the distribution could be null though + self.reward_data[_ANGLE].distributor = _distributor + self.reward_tokens[0] = _ANGLE + self.reward_count = 1 + + +@view +@external +def decimals() -> uint256: + """ + @notice Get the number of decimals for this token + @dev Implemented as a view method to reduce gas costs + @return uint256 decimal places + """ + return self.decimal_staking_token + + +@internal +def _update_liquidity_limit(addr: address, l: uint256, L: uint256): + """ + @notice Calculate limits which depend on the amount of ANGLE token per-user. + Effectively it calculates working balances to apply amplification + of ANGLE production by ANGLE + @param addr User address + @param l User's amount of liquidity (LP tokens) + @param L Total amount of liquidity (LP tokens) + """ + # To be called after totalSupply is updated + voting_balance: uint256 = VotingEscrowBoost(self.veBoost_proxy).adjusted_balance_of(addr) + voting_total: uint256 = ERC20(self.voting_escrow).totalSupply() + + lim: uint256 = l * TOKENLESS_PRODUCTION / 100 + if voting_total > 0: + lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 + + lim = min(l, lim) + old_bal: uint256 = self.working_balances[addr] + self.working_balances[addr] = lim + _working_supply: uint256 = self.working_supply + lim - old_bal + self.working_supply = _working_supply + + log UpdateLiquidityLimit(addr, l, L, lim, _working_supply) + + +@internal +def _checkpoint_reward(_user: address, token: address, _total_supply: uint256, _user_balance: uint256, _claim: bool, receiver: address): + """ + @notice Claim pending rewards and checkpoint rewards for a user + """ + total_supply: uint256 = _total_supply + user_balance: uint256 = _user_balance + if token == self.ANGLE : + total_supply = self.working_supply + user_balance = self.working_balances[_user] + + integral: uint256 = self.reward_data[token].integral + last_update: uint256 = min(block.timestamp, self.reward_data[token].period_finish) + duration: uint256 = last_update - self.reward_data[token].last_update + if duration != 0: + self.reward_data[token].last_update = last_update + if total_supply != 0: + integral += duration * self.reward_data[token].rate * 10**18 / total_supply + self.reward_data[token].integral = integral + + if _user != ZERO_ADDRESS: + integral_for: uint256 = self.reward_integral_for[token][_user] + new_claimable: uint256 = 0 + + if integral_for < integral: + self.reward_integral_for[token][_user] = integral + new_claimable = user_balance * (integral - integral_for) / 10**18 + + claim_data: uint256 = self.claim_data[_user][token] + total_claimable: uint256 = shift(claim_data, -128) + new_claimable + if total_claimable > 0: + total_claimed: uint256 = claim_data % 2**128 + if _claim: + response: Bytes[32] = raw_call( + token, + concat( + method_id("transfer(address,uint256)"), + convert(receiver, bytes32), + convert(total_claimable, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + self.claim_data[_user][token] = total_claimed + total_claimable + elif new_claimable > 0: + self.claim_data[_user][token] = total_claimed + shift(total_claimable, 128) + + if token == self.ANGLE : + self.integrate_checkpoint_of[_user] = block.timestamp + +@internal +def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _receiver: address, _only_checkpoint:bool = False): + """ + @notice Claim pending rewards and checkpoint rewards for a user + """ + + receiver: address = _receiver + user_balance: uint256 = 0 + if _user != ZERO_ADDRESS: + user_balance = self.balanceOf[_user] + if _claim and _receiver == ZERO_ADDRESS: + # if receiver is not explicitly declared, check if a default receiver is set + receiver = self.rewards_receiver[_user] + if receiver == ZERO_ADDRESS: + # if no default receiver is set, direct claims to the user + receiver = _user + + if _only_checkpoint: + self._checkpoint_reward(_user, self.ANGLE, _total_supply, user_balance, False, receiver) + else: + reward_count: uint256 = self.reward_count + for i in range(MAX_REWARDS): + if i == reward_count: + break + token: address = self.reward_tokens[i] + self._checkpoint_reward(_user, token, _total_supply, user_balance, _claim, receiver) + +@external +def user_checkpoint(addr: address) -> bool: + """ + @notice Record a checkpoint for `addr` + @param addr User address + @return bool success + """ + assert msg.sender == addr # dev: unauthorized + total_supply: uint256 = self.totalSupply + self._checkpoint_rewards(addr, total_supply, False, ZERO_ADDRESS, True) + self._update_liquidity_limit(addr, self.balanceOf[addr], total_supply) + return True + +@view +@external +def claimed_reward(_addr: address, _token: address) -> uint256: + """ + @notice Get the number of already-claimed reward tokens for a user + @param _addr Account to get reward amount for + @param _token Token to get reward amount for + @return uint256 Total amount of `_token` already claimed by `_addr` + """ + return self.claim_data[_addr][_token] % 2**128 + + +@view +@external +def claimable_reward(_user: address, _reward_token: address) -> uint256: + """ + @notice Get the number of claimable reward tokens for a user + @param _user Account to get reward amount for + @param _reward_token Token to get reward amount for + @return uint256 Claimable reward token amount + """ + integral: uint256 = self.reward_data[_reward_token].integral + total_supply: uint256 = self.totalSupply + user_balance: uint256 = self.balanceOf[_user] + if _reward_token == self.ANGLE : + total_supply = self.working_supply + user_balance = self.working_balances[_user] + + if total_supply != 0: + last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish) + duration: uint256 = last_update - self.reward_data[_reward_token].last_update + integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply) + + integral_for: uint256 = self.reward_integral_for[_reward_token][_user] + new_claimable: uint256 = user_balance * (integral - integral_for) / 10**18 + + return shift(self.claim_data[_user][_reward_token], -128) + new_claimable + + +@external +def set_rewards_receiver(_receiver: address): + """ + @notice Set the default reward receiver for the caller. + @dev When set to ZERO_ADDRESS, rewards are sent to the caller + @param _receiver Receiver address for any rewards claimed via `claim_rewards` + """ + self.rewards_receiver[msg.sender] = _receiver + + +@external +@nonreentrant('lock') +def claim_rewards(_addr: address = msg.sender, _receiver: address = ZERO_ADDRESS): + """ + @notice Claim available reward tokens for `_addr` + @param _addr Address to claim for + @param _receiver Address to transfer rewards to - if set to + ZERO_ADDRESS, uses the default reward receiver + for the caller + """ + if _receiver != ZERO_ADDRESS: + assert _addr == msg.sender # dev: cannot redirect when claiming for another user + self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver) + + +@external +def kick(addr: address): + """ + @notice Kick `addr` for abusing their boost + @dev Only if either they had another voting event, or their voting escrow lock expired + @param addr Address to kick + """ + t_last: uint256 = self.integrate_checkpoint_of[addr] + t_ve: uint256 = VotingEscrow(self.voting_escrow).user_point_history__ts( + addr, VotingEscrow(self.voting_escrow).user_point_epoch(addr) + ) + _balance: uint256 = self.balanceOf[addr] + + assert ERC20(self.voting_escrow).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed + assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed + + total_supply: uint256 = self.totalSupply + self._checkpoint_rewards(addr, total_supply, False, ZERO_ADDRESS, True) + + self._update_liquidity_limit(addr, self.balanceOf[addr], total_supply) + + +@external +@nonreentrant('lock') +def deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = False): + """ + @notice Deposit `_value` LP tokens + @dev Depositting also claims pending reward tokens + @param _value Number of tokens to deposit + @param _addr Address to deposit for + """ + total_supply: uint256 = self.totalSupply + + if _value != 0: + is_rewards: bool = self.reward_count != 0 + if is_rewards: + self._checkpoint_rewards(_addr, total_supply, _claim_rewards, ZERO_ADDRESS) + + total_supply += _value + new_balance: uint256 = self.balanceOf[_addr] + _value + self.balanceOf[_addr] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(_addr, new_balance, total_supply) + + ERC20(self.staking_token).transferFrom(msg.sender, self, _value) + else: + self._checkpoint_rewards(_addr, total_supply, False, ZERO_ADDRESS, True) + + log Deposit(_addr, _value) + log Transfer(ZERO_ADDRESS, _addr, _value) + + +@external +@nonreentrant('lock') +def withdraw(_value: uint256, _claim_rewards: bool = False): + """ + @notice Withdraw `_value` LP tokens + @dev Withdrawing also claims pending reward tokens + @param _value Number of tokens to withdraw + """ + total_supply: uint256 = self.totalSupply + + if _value != 0: + is_rewards: bool = self.reward_count != 0 + if is_rewards: + self._checkpoint_rewards(msg.sender, total_supply, _claim_rewards, ZERO_ADDRESS) + + total_supply -= _value + new_balance: uint256 = self.balanceOf[msg.sender] - _value + self.balanceOf[msg.sender] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(msg.sender, new_balance, total_supply) + + ERC20(self.staking_token).transfer(msg.sender, _value) + else: + self._checkpoint_rewards(msg.sender, total_supply, False, ZERO_ADDRESS, True) + + log Withdraw(msg.sender, _value) + log Transfer(msg.sender, ZERO_ADDRESS, _value) + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + total_supply: uint256 = self.totalSupply + + if _value != 0: + is_rewards: bool = self.reward_count != 0 + if is_rewards: + self._checkpoint_rewards(_from, total_supply, False, ZERO_ADDRESS) + new_balance: uint256 = self.balanceOf[_from] - _value + self.balanceOf[_from] = new_balance + self._update_liquidity_limit(_from, new_balance, total_supply) + + if is_rewards: + self._checkpoint_rewards(_to, total_supply, False, ZERO_ADDRESS) + new_balance = self.balanceOf[_to] + _value + self.balanceOf[_to] = new_balance + self._update_liquidity_limit(_to, new_balance, total_supply) + else: + self._checkpoint_rewards(_from, total_supply, False, ZERO_ADDRESS, True) + self._checkpoint_rewards(_to, total_supply, False, ZERO_ADDRESS, True) + + log Transfer(_from, _to, _value) + + +@external +@nonreentrant('lock') +def transfer(_to : address, _value : uint256) -> bool: + """ + @notice Transfer token for a specified address + @dev Transferring claims pending reward tokens for the sender and receiver + @param _to The address to transfer to. + @param _value The amount to be transferred. + """ + self._transfer(msg.sender, _to, _value) + + return True + + +@external +@nonreentrant('lock') +def transferFrom(_from : address, _to : address, _value : uint256) -> bool: + """ + @notice Transfer tokens from one address to another. + @dev Transferring claims pending reward tokens for the sender and receiver + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + """ + _allowance: uint256 = self.allowance[_from][msg.sender] + if _allowance != MAX_UINT256: + self.allowance[_from][msg.sender] = _allowance - _value + + self._transfer(_from, _to, _value) + + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @notice Approve the passed address to transfer the specified amount of + tokens on behalf of msg.sender + @dev Beware that changing an allowance via this method brings the risk + that someone may use both the old and new allowance by unfortunate + transaction ordering. This may be mitigated with the use of + {incraseAllowance} and {decreaseAllowance}. + https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will transfer the funds + @param _value The amount of tokens that may be transferred + @return bool success + """ + self.allowance[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + + return True + + +@external +def increaseAllowance(_spender: address, _added_value: uint256) -> bool: + """ + @notice Increase the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _added_value The amount of to increase the allowance + @return bool success + """ + allowance: uint256 = self.allowance[msg.sender][_spender] + _added_value + self.allowance[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + + +@external +def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: + """ + @notice Decrease the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _subtracted_value The amount of to decrease the allowance + @return bool success + """ + allowance: uint256 = self.allowance[msg.sender][_spender] - _subtracted_value + self.allowance[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + +@external +def add_reward(_reward_token: address, _distributor: address): + """ + @notice Set the active reward contract + """ + assert msg.sender == self.admin # dev: only owner + + reward_count: uint256 = self.reward_count + assert reward_count < MAX_REWARDS + assert self.reward_data[_reward_token].distributor == ZERO_ADDRESS + + self.reward_data[_reward_token].distributor = _distributor + self.reward_tokens[reward_count] = _reward_token + self.reward_count = reward_count + 1 + +@external +def set_reward_distributor(_reward_token: address, _distributor: address): + current_distributor: address = self.reward_data[_reward_token].distributor + + assert msg.sender == current_distributor or msg.sender == self.admin + assert current_distributor != ZERO_ADDRESS + assert _distributor != ZERO_ADDRESS + + self.reward_data[_reward_token].distributor = _distributor + + +@external +@nonreentrant("lock") +def deposit_reward_token(_reward_token: address, _amount: uint256): + assert msg.sender == self.reward_data[_reward_token].distributor + + self._checkpoint_rewards(ZERO_ADDRESS, self.totalSupply, False, ZERO_ADDRESS) + + response: Bytes[32] = raw_call( + _reward_token, + concat( + method_id("transferFrom(address,address,uint256)"), + convert(msg.sender, bytes32), + convert(self, bytes32), + convert(_amount, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + + period_finish: uint256 = self.reward_data[_reward_token].period_finish + if block.timestamp >= period_finish: + self.reward_data[_reward_token].rate = _amount / WEEK + else: + remaining: uint256 = period_finish - block.timestamp + leftover: uint256 = remaining * self.reward_data[_reward_token].rate + self.reward_data[_reward_token].rate = (_amount + leftover) / WEEK + + self.reward_data[_reward_token].last_update = block.timestamp + self.reward_data[_reward_token].period_finish = block.timestamp + WEEK + + log RewardDataUpdate(_reward_token,_amount) + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of Gauge to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + assert addr != ZERO_ADDRESS # dev: future admin cannot be the 0 address + + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + log ApplyOwnership(_admin) diff --git a/contracts/staking/LiquidityGaugeV4UpgradedToken.vy b/contracts/staking/LiquidityGaugeV4UpgradedToken.vy new file mode 100644 index 0000000..8f0899a --- /dev/null +++ b/contracts/staking/LiquidityGaugeV4UpgradedToken.vy @@ -0,0 +1,670 @@ +# @version 0.2.16 +""" +@title Liquidity Gauge v4 +@author Angle Protocol +@license MIT +""" + +# Original idea and credit: +# Curve Finance's veCRV +# https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/gauges/LiquidityGaugeV4.vy +# Mostly forked from Curve, except that now there is no direct link between the gauge controller +# and the gauges. In this implementation, there is a parameter to scale the value of the staking token +# which can be used if the staking_token and corresponding balances have to be upgraded + +from vyper.interfaces import ERC20 + +implements: ERC20 + +interface VotingEscrow: + def user_point_epoch(addr: address) -> uint256: view + def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view + +interface VotingEscrowBoost: + def adjusted_balance_of(_account: address) -> uint256: view + +interface ERC20Extended: + def symbol() -> String[26]: view + def decimals() -> uint256: view + + +event Deposit: + provider: indexed(address) + value: uint256 + +event Withdraw: + provider: indexed(address) + value: uint256 + +event UpdateLiquidityLimit: + user: address + original_balance: uint256 + original_supply: uint256 + working_balance: uint256 + working_supply: uint256 + +event CommitOwnership: + admin: address + +event ApplyOwnership: + admin: address + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _value: uint256 + +event Approval: + _owner: indexed(address) + _spender: indexed(address) + _value: uint256 + +event RewardDataUpdate: + _token: indexed(address) + _amount: uint256 + +struct Reward: + token: address + distributor: address + period_finish: uint256 + rate: uint256 + last_update: uint256 + integral: uint256 + + +MAX_REWARDS: constant(uint256) = 8 +TOKENLESS_PRODUCTION: constant(uint256) = 40 +WEEK: constant(uint256) = 604800 + +ANGLE: public(address) +voting_escrow: public(address) +veBoost_proxy: public(address) + +staking_token: public(address) +decimal_staking_token: public(uint256) + +balanceOf: public(HashMap[address, uint256]) +totalSupply: public(uint256) +allowance: public(HashMap[address, HashMap[address, uint256]]) + +name: public(String[64]) +symbol: public(String[32]) + +working_balances: public(HashMap[address, uint256]) +working_supply: public(uint256) + +integrate_checkpoint_of: public(HashMap[address, uint256]) + +# For tracking external rewards +reward_count: public(uint256) +reward_tokens: public(address[MAX_REWARDS]) + +reward_data: public(HashMap[address, Reward]) + +# claimant -> default reward receiver +rewards_receiver: public(HashMap[address, address]) + +# reward token -> claiming address -> integral +reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) + +# user -> [uint128 claimable amount][uint128 claimed amount] +claim_data: HashMap[address, HashMap[address, uint256]] + +admin: public(address) +future_admin: public(address) + +initialized: public(bool) + +scaling_factor:public(uint256) + +@external +def __init__(): + """ + @notice Contract constructor + @dev The contract has an initializer to prevent the take over of the implementation + """ + assert self.initialized == False #dev: contract is already initialized + self.initialized = True + +@external +def initialize(_staking_token: address, _admin: address, _ANGLE: address, _voting_escrow: address, _veBoost_proxy: address, _distributor: address): + """ + @notice Contract initializer + @param _staking_token Liquidity Pool contract address + @param _admin Admin who can kill the gauge + @param _ANGLE Address of the ANGLE token + @param _voting_escrow Address of the veANGLE contract + @param _veBoost_proxy Address of the proxy contract used to query veANGLE balances and taking into account potential delegations + @param _distributor Address of the contract responsible for distributing ANGLE tokens to this gauge + """ + assert self.initialized == False #dev: contract is already initialized + self.initialized = True + + assert _admin != ZERO_ADDRESS + assert _ANGLE != ZERO_ADDRESS + assert _voting_escrow != ZERO_ADDRESS + assert _veBoost_proxy != ZERO_ADDRESS + assert _distributor != ZERO_ADDRESS + + self.admin = _admin + self.staking_token = _staking_token + self.decimal_staking_token = ERC20Extended(_staking_token).decimals() + + symbol: String[26] = ERC20Extended(_staking_token).symbol() + self.name = concat("Angle ", symbol, " Gauge") + self.symbol = concat(symbol, "-gauge") + self.ANGLE = _ANGLE + self.voting_escrow = _voting_escrow + self.veBoost_proxy = _veBoost_proxy + + # add in all liquidityGauge the ANGLE reward - the distribution could be null though + self.reward_data[_ANGLE].distributor = _distributor + self.reward_tokens[0] = _ANGLE + self.reward_count = 1 + + +@view +@external +def decimals() -> uint256: + """ + @notice Get the number of decimals for this token + @dev Implemented as a view method to reduce gas costs + @return uint256 decimal places + """ + return self.decimal_staking_token + + +@internal +def _update_liquidity_limit(addr: address, l: uint256, L: uint256): + """ + @notice Calculate limits which depend on the amount of ANGLE token per-user. + Effectively it calculates working balances to apply amplification + of ANGLE production by ANGLE + @param addr User address + @param l User's amount of liquidity (LP tokens) + @param L Total amount of liquidity (LP tokens) + """ + # To be called after totalSupply is updated + voting_balance: uint256 = VotingEscrowBoost(self.veBoost_proxy).adjusted_balance_of(addr) + voting_total: uint256 = ERC20(self.voting_escrow).totalSupply() + + lim: uint256 = l * TOKENLESS_PRODUCTION / 100 + if voting_total > 0: + lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 + + lim = min(l, lim) + old_bal: uint256 = self.working_balances[addr] + self.working_balances[addr] = lim + _working_supply: uint256 = self.working_supply + lim - old_bal + self.working_supply = _working_supply + + log UpdateLiquidityLimit(addr, l, L, lim, _working_supply) + + +@internal +def _checkpoint_reward(_user: address, token: address, _total_supply: uint256, _user_balance: uint256, _claim: bool, receiver: address): + """ + @notice Claim pending rewards and checkpoint rewards for a user + """ + total_supply: uint256 = _total_supply + user_balance: uint256 = _user_balance + if token == self.ANGLE : + total_supply = self.working_supply + user_balance = self.working_balances[_user] + + integral: uint256 = self.reward_data[token].integral + last_update: uint256 = min(block.timestamp, self.reward_data[token].period_finish) + duration: uint256 = last_update - self.reward_data[token].last_update + if duration != 0: + self.reward_data[token].last_update = last_update + if total_supply != 0: + integral += duration * self.reward_data[token].rate * 10**18 / total_supply + self.reward_data[token].integral = integral + + if _user != ZERO_ADDRESS: + integral_for: uint256 = self.reward_integral_for[token][_user] + new_claimable: uint256 = 0 + + if integral_for < integral: + self.reward_integral_for[token][_user] = integral + new_claimable = user_balance * (integral - integral_for) / 10**18 + + claim_data: uint256 = self.claim_data[_user][token] + total_claimable: uint256 = shift(claim_data, -128) + new_claimable + if total_claimable > 0: + total_claimed: uint256 = claim_data % 2**128 + if _claim: + response: Bytes[32] = raw_call( + token, + concat( + method_id("transfer(address,uint256)"), + convert(receiver, bytes32), + convert(total_claimable, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + self.claim_data[_user][token] = total_claimed + total_claimable + elif new_claimable > 0: + self.claim_data[_user][token] = total_claimed + shift(total_claimable, 128) + + if token == self.ANGLE : + self.integrate_checkpoint_of[_user] = block.timestamp + +@internal +def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _receiver: address, _only_checkpoint:bool = False): + """ + @notice Claim pending rewards and checkpoint rewards for a user + """ + + receiver: address = _receiver + user_balance: uint256 = 0 + if _user != ZERO_ADDRESS: + user_balance = self.balanceOf[_user] + if _claim and _receiver == ZERO_ADDRESS: + # if receiver is not explicitly declared, check if a default receiver is set + receiver = self.rewards_receiver[_user] + if receiver == ZERO_ADDRESS: + # if no default receiver is set, direct claims to the user + receiver = _user + + if _only_checkpoint: + self._checkpoint_reward(_user, self.ANGLE, _total_supply, user_balance, False, receiver) + else: + reward_count: uint256 = self.reward_count + for i in range(MAX_REWARDS): + if i == reward_count: + break + token: address = self.reward_tokens[i] + self._checkpoint_reward(_user, token, _total_supply, user_balance, _claim, receiver) + +@external +def user_checkpoint(addr: address) -> bool: + """ + @notice Record a checkpoint for `addr` + @param addr User address + @return bool success + """ + assert msg.sender == addr # dev: unauthorized + total_supply: uint256 = self.totalSupply + self._checkpoint_rewards(addr, total_supply, False, ZERO_ADDRESS, True) + self._update_liquidity_limit(addr, self.balanceOf[addr], total_supply) + return True + +@view +@external +def claimed_reward(_addr: address, _token: address) -> uint256: + """ + @notice Get the number of already-claimed reward tokens for a user + @param _addr Account to get reward amount for + @param _token Token to get reward amount for + @return uint256 Total amount of `_token` already claimed by `_addr` + """ + return self.claim_data[_addr][_token] % 2**128 + + +@view +@external +def claimable_reward(_user: address, _reward_token: address) -> uint256: + """ + @notice Get the number of claimable reward tokens for a user + @param _user Account to get reward amount for + @param _reward_token Token to get reward amount for + @return uint256 Claimable reward token amount + """ + integral: uint256 = self.reward_data[_reward_token].integral + total_supply: uint256 = self.totalSupply + user_balance: uint256 = self.balanceOf[_user] + if _reward_token == self.ANGLE : + total_supply = self.working_supply + user_balance = self.working_balances[_user] + + if total_supply != 0: + last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish) + duration: uint256 = last_update - self.reward_data[_reward_token].last_update + integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply) + + integral_for: uint256 = self.reward_integral_for[_reward_token][_user] + new_claimable: uint256 = user_balance * (integral - integral_for) / 10**18 + + return shift(self.claim_data[_user][_reward_token], -128) + new_claimable + + +@external +def set_rewards_receiver(_receiver: address): + """ + @notice Set the default reward receiver for the caller. + @dev When set to ZERO_ADDRESS, rewards are sent to the caller + @param _receiver Receiver address for any rewards claimed via `claim_rewards` + """ + self.rewards_receiver[msg.sender] = _receiver + + +@external +@nonreentrant('lock') +def claim_rewards(_addr: address = msg.sender, _receiver: address = ZERO_ADDRESS): + """ + @notice Claim available reward tokens for `_addr` + @param _addr Address to claim for + @param _receiver Address to transfer rewards to - if set to + ZERO_ADDRESS, uses the default reward receiver + for the caller + """ + if _receiver != ZERO_ADDRESS: + assert _addr == msg.sender # dev: cannot redirect when claiming for another user + self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver) + + +@external +def kick(addr: address): + """ + @notice Kick `addr` for abusing their boost + @dev Only if either they had another voting event, or their voting escrow lock expired + @param addr Address to kick + """ + t_last: uint256 = self.integrate_checkpoint_of[addr] + t_ve: uint256 = VotingEscrow(self.voting_escrow).user_point_history__ts( + addr, VotingEscrow(self.voting_escrow).user_point_epoch(addr) + ) + _balance: uint256 = self.balanceOf[addr] + + assert ERC20(self.voting_escrow).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed + assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed + + total_supply: uint256 = self.totalSupply + self._checkpoint_rewards(addr, total_supply, False, ZERO_ADDRESS, True) + + self._update_liquidity_limit(addr, self.balanceOf[addr], total_supply) + + +@external +@nonreentrant('lock') +def deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = False): + """ + @notice Deposit `_value` LP tokens + @dev Depositting also claims pending reward tokens + @param _value Number of tokens to deposit + @param _addr Address to deposit for + """ + total_supply: uint256 = self.totalSupply + scaled_value: uint256 = _value + + _scaling_factor: uint256 = self.scaling_factor + if _scaling_factor != 0: + scaled_value = _value * _scaling_factor / 10**18 + + if scaled_value != 0: + is_rewards: bool = self.reward_count != 0 + if is_rewards: + self._checkpoint_rewards(_addr, total_supply, _claim_rewards, ZERO_ADDRESS) + + total_supply += scaled_value + new_balance: uint256 = self.balanceOf[_addr] + scaled_value + self.balanceOf[_addr] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(_addr, new_balance, total_supply) + + ERC20(self.staking_token).transferFrom(msg.sender, self, _value) + else: + self._checkpoint_rewards(_addr, total_supply, False, ZERO_ADDRESS, True) + + log Deposit(_addr, _value) + log Transfer(ZERO_ADDRESS, _addr, scaled_value) + + +@external +@nonreentrant('lock') +def withdraw(_value: uint256, _claim_rewards: bool = False): + """ + @notice Withdraw `_value` LP tokens + @dev Withdrawing also claims pending reward tokens + @param _value Number of tokens to withdraw + """ + total_supply: uint256 = self.totalSupply + _scaling_factor: uint256 = self.scaling_factor + scaled_value: uint256 = _value + + if _scaling_factor != 0: + scaled_value = _value * 10**18 / _scaling_factor + + if _value != 0: + + is_rewards: bool = self.reward_count != 0 + if is_rewards: + self._checkpoint_rewards(msg.sender, total_supply, _claim_rewards, ZERO_ADDRESS) + + total_supply -= _value + new_balance: uint256 = self.balanceOf[msg.sender] - _value + self.balanceOf[msg.sender] = new_balance + self.totalSupply = total_supply + + self._update_liquidity_limit(msg.sender, new_balance, total_supply) + ERC20(self.staking_token).transfer(msg.sender, scaled_value) + else: + self._checkpoint_rewards(msg.sender, total_supply, False, ZERO_ADDRESS, True) + + log Withdraw(msg.sender, scaled_value) + log Transfer(msg.sender, ZERO_ADDRESS, _value) + + +@internal +def _transfer(_from: address, _to: address, _value: uint256): + total_supply: uint256 = self.totalSupply + + if _value != 0: + is_rewards: bool = self.reward_count != 0 + if is_rewards: + self._checkpoint_rewards(_from, total_supply, False, ZERO_ADDRESS) + new_balance: uint256 = self.balanceOf[_from] - _value + self.balanceOf[_from] = new_balance + self._update_liquidity_limit(_from, new_balance, total_supply) + + if is_rewards: + self._checkpoint_rewards(_to, total_supply, False, ZERO_ADDRESS) + new_balance = self.balanceOf[_to] + _value + self.balanceOf[_to] = new_balance + self._update_liquidity_limit(_to, new_balance, total_supply) + else: + self._checkpoint_rewards(_from, total_supply, False, ZERO_ADDRESS, True) + self._checkpoint_rewards(_to, total_supply, False, ZERO_ADDRESS, True) + + log Transfer(_from, _to, _value) + + +@external +@nonreentrant('lock') +def transfer(_to : address, _value : uint256) -> bool: + """ + @notice Transfer token for a specified address + @dev Transferring claims pending reward tokens for the sender and receiver + @param _to The address to transfer to. + @param _value The amount to be transferred. + """ + self._transfer(msg.sender, _to, _value) + + return True + + +@external +@nonreentrant('lock') +def transferFrom(_from : address, _to : address, _value : uint256) -> bool: + """ + @notice Transfer tokens from one address to another. + @dev Transferring claims pending reward tokens for the sender and receiver + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + """ + _allowance: uint256 = self.allowance[_from][msg.sender] + if _allowance != MAX_UINT256: + self.allowance[_from][msg.sender] = _allowance - _value + + self._transfer(_from, _to, _value) + + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @notice Approve the passed address to transfer the specified amount of + tokens on behalf of msg.sender + @dev Beware that changing an allowance via this method brings the risk + that someone may use both the old and new allowance by unfortunate + transaction ordering. This may be mitigated with the use of + {incraseAllowance} and {decreaseAllowance}. + https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will transfer the funds + @param _value The amount of tokens that may be transferred + @return bool success + """ + self.allowance[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + + return True + + +@external +def increaseAllowance(_spender: address, _added_value: uint256) -> bool: + """ + @notice Increase the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _added_value The amount of to increase the allowance + @return bool success + """ + allowance: uint256 = self.allowance[msg.sender][_spender] + _added_value + self.allowance[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + + +@external +def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: + """ + @notice Decrease the allowance granted to `_spender` by the caller + @dev This is alternative to {approve} that can be used as a mitigation for + the potential race condition + @param _spender The address which will transfer the funds + @param _subtracted_value The amount of to decrease the allowance + @return bool success + """ + allowance: uint256 = self.allowance[msg.sender][_spender] - _subtracted_value + self.allowance[msg.sender][_spender] = allowance + + log Approval(msg.sender, _spender, allowance) + + return True + +@external +def add_reward(_reward_token: address, _distributor: address): + """ + @notice Set the active reward contract + """ + assert msg.sender == self.admin # dev: only owner + + reward_count: uint256 = self.reward_count + assert reward_count < MAX_REWARDS + assert self.reward_data[_reward_token].distributor == ZERO_ADDRESS + + self.reward_data[_reward_token].distributor = _distributor + self.reward_tokens[reward_count] = _reward_token + self.reward_count = reward_count + 1 + +@external +def set_reward_distributor(_reward_token: address, _distributor: address): + current_distributor: address = self.reward_data[_reward_token].distributor + + assert msg.sender == current_distributor or msg.sender == self.admin + assert current_distributor != ZERO_ADDRESS + assert _distributor != ZERO_ADDRESS + + self.reward_data[_reward_token].distributor = _distributor + + +@external +@nonreentrant("lock") +def deposit_reward_token(_reward_token: address, _amount: uint256): + assert msg.sender == self.reward_data[_reward_token].distributor + + self._checkpoint_rewards(ZERO_ADDRESS, self.totalSupply, False, ZERO_ADDRESS) + + response: Bytes[32] = raw_call( + _reward_token, + concat( + method_id("transferFrom(address,address,uint256)"), + convert(msg.sender, bytes32), + convert(self, bytes32), + convert(_amount, bytes32), + ), + max_outsize=32, + ) + if len(response) != 0: + assert convert(response, bool) + + period_finish: uint256 = self.reward_data[_reward_token].period_finish + if block.timestamp >= period_finish: + self.reward_data[_reward_token].rate = _amount / WEEK + else: + remaining: uint256 = period_finish - block.timestamp + leftover: uint256 = remaining * self.reward_data[_reward_token].rate + self.reward_data[_reward_token].rate = (_amount + leftover) / WEEK + + self.reward_data[_reward_token].last_update = block.timestamp + self.reward_data[_reward_token].period_finish = block.timestamp + WEEK + + log RewardDataUpdate(_reward_token,_amount) + +@external +def commit_transfer_ownership(addr: address): + """ + @notice Transfer ownership of Gauge to `addr` + @param addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + assert addr != ZERO_ADDRESS # dev: future admin cannot be the 0 address + + self.future_admin = addr + log CommitOwnership(addr) + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + log ApplyOwnership(_admin) + +@external +def recover_erc20(token: address, addr: address, amount: uint256): + """ + @notice Recovers tokens sent or accruing to this contract + @param token Token to be recovered + @param addr Address to send the token to + @param amount Amount of tokens to send + """ + assert msg.sender == self.admin # dev: only owner + if token == self: + self._transfer(self, addr, amount) + else: + ERC20(token).transfer(addr, amount) + + +@external +def set_staking_token_and_scaling_factor(token: address, _value: uint256): + """ + @notice Sets the staking token + """ + assert msg.sender == self.admin # dev: only owner + self.staking_token = token + self.decimal_staking_token = ERC20Extended(token).decimals() + self.scaling_factor = _value diff --git a/contracts/staking/RewardsDistributor.sol b/contracts/staking/RewardsDistributor.sol new file mode 100644 index 0000000..df6b97d --- /dev/null +++ b/contracts/staking/RewardsDistributor.sol @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./RewardsDistributorEvents.sol"; + +/// @notice Distribution parameters for a given contract +struct StakingParameters { + // Amount of rewards distributed since the beginning + uint256 distributedRewards; + // Last time rewards were distributed to the staking contract + uint256 lastDistributionTime; + // Frequency with which rewards should be given to the underlying contract + uint256 updateFrequency; + // Number of tokens distributed for the person calling the update function + uint256 incentiveAmount; + // Time at which reward distribution started for this reward contract + uint256 timeStarted; + // Amount of time during which rewards will be distributed + uint256 duration; + // Amount of tokens to distribute to the concerned contract + uint256 amountToDistribute; +} + +/// @title RewardsDistributor +/// @author Angle Core Team (forked form FEI Protocol) +/// @notice Controls and handles the distribution of governance tokens to the different staking contracts of the protocol +/// @dev Inspired from FEI contract: +/// https://github.com/fei-protocol/fei-protocol-core/blob/master/contracts/staking/FeiRewardsDistributor.sol +contract RewardsDistributor is RewardsDistributorEvents, IRewardsDistributor, AccessControl { + using SafeERC20 for IERC20; + + /// @notice Role for governors only + bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE"); + /// @notice Role for guardians and governors + bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE"); + + // ============================ Reference to a contract ======================== + + /// @notice Token used as a reward + IERC20 public immutable override rewardToken; + + // ============================== Parameters =================================== + + /// @notice Maps a `StakingContract` to its distribution parameters + mapping(IStakingRewards => StakingParameters) public stakingContractsMap; + + /// @notice List of all the staking contracts handled by the rewards distributor + /// Used to be able to change the rewards distributor and propagate a new reference to the underlying + /// staking contract + IStakingRewards[] public stakingContractsList; + + // ============================ Constructor ==================================== + + /// @notice Initializes the distributor contract with a first set of parameters + /// @param governorList List of the governor addresses of the protocol + /// @param guardian The guardian address, optional + /// @param rewardTokenAddress The ERC20 token to distribute + constructor( + address[] memory governorList, + address guardian, + address rewardTokenAddress + ) { + require(rewardTokenAddress != address(0) && guardian != address(0), "0"); + require(governorList.length > 0, "47"); + rewardToken = IERC20(rewardTokenAddress); + // Since this contract is independent from the rest of the protocol + // When updating the governor list, governors should make sure to still update the roles + // in this contract + _setRoleAdmin(GOVERNOR_ROLE, GOVERNOR_ROLE); + _setRoleAdmin(GUARDIAN_ROLE, GOVERNOR_ROLE); + for (uint256 i = 0; i < governorList.length; i++) { + require(governorList[i] != address(0), "0"); + _setupRole(GOVERNOR_ROLE, governorList[i]); + _setupRole(GUARDIAN_ROLE, governorList[i]); + } + _setupRole(GUARDIAN_ROLE, guardian); + } + + // ============================ External Functions ============================= + + /// @notice Sends reward tokens to the staking contract + /// @param stakingContract Reference to the staking contract + /// @dev The way to pause this function is to set `updateFrequency` to infinity, + /// or to completely delete the contract + /// @dev A keeper calling this function could be frontran by a miner seeing the potential profit + /// from calling this function + /// @dev This function automatically computes the amount of reward tokens to send to the staking + /// contract based on the time elapsed since the last drip, on the amount to distribute and on + /// the duration of the distribution + function drip(IStakingRewards stakingContract) external override returns (uint256) { + StakingParameters storage stakingParams = stakingContractsMap[stakingContract]; + require(stakingParams.duration > 0, "80"); + require(_isDripAvailable(stakingParams), "81"); + + uint256 dripAmount = _computeDripAmount(stakingParams); + stakingParams.lastDistributionTime = block.timestamp; + require(dripAmount != 0, "82"); + stakingParams.distributedRewards += dripAmount; + emit Dripped(msg.sender, dripAmount, address(stakingContract)); + + rewardToken.safeTransfer(address(stakingContract), dripAmount); + IStakingRewards(stakingContract).notifyRewardAmount(dripAmount); + _incentivize(stakingParams); + + return dripAmount; + } + + // =========================== Governor Functions ============================== + + /// @notice Sends tokens back to governance treasury or another address + /// @param amount Amount of tokens to send back to treasury + /// @param to Address to send the tokens to + /// @dev Only callable by governance and not by the guardian + function governorWithdrawRewardToken(uint256 amount, address to) external override onlyRole(GOVERNOR_ROLE) { + emit RewardTokenWithdrawn(amount); + rewardToken.safeTransfer(to, amount); + } + + /// @notice Function to withdraw ERC20 tokens that could accrue on a staking contract + /// @param tokenAddress Address of the ERC20 to recover + /// @param to Address to transfer to + /// @param amount Amount to transfer + /// @param stakingContract Reference to the staking contract + /// @dev A use case would be to claim tokens if the staked tokens accumulate rewards or if tokens were + /// mistakenly sent to staking contracts + function governorRecover( + address tokenAddress, + address to, + uint256 amount, + IStakingRewards stakingContract + ) external override onlyRole(GOVERNOR_ROLE) { + stakingContract.recoverERC20(tokenAddress, to, amount); + } + + /// @notice Sets a new rewards distributor contract and automatically makes this contract useless + /// @param newRewardsDistributor Address of the new rewards distributor contract + /// @dev This contract is not upgradeable, setting a new contract could allow for upgrades, which should be + /// propagated across all staking contracts + /// @dev This function transfers all the reward tokens to the new address + /// @dev The new rewards distributor contract should be initialized correctly with all the staking contracts + /// from the staking contract list + function setNewRewardsDistributor(address newRewardsDistributor) external override onlyRole(GOVERNOR_ROLE) { + // Checking the compatibility of the reward tokens. It is checked at the initialization of each staking contract + // in the `setStakingContract` function that reward tokens are compatible with the `rewardsDistributor`. If + // the `newRewardsDistributor` has a compatible rewards token, then all staking contracts will automatically be + // compatible with it + require(address(IRewardsDistributor(newRewardsDistributor).rewardToken()) == address(rewardToken), "83"); + require(newRewardsDistributor != address(this), "84"); + for (uint256 i = 0; i < stakingContractsList.length; i++) { + stakingContractsList[i].setNewRewardsDistribution(newRewardsDistributor); + } + rewardToken.safeTransfer(newRewardsDistributor, rewardToken.balanceOf(address(this))); + // The functions `setStakingContract` should then be called for each staking contract in the `newRewardsDistributor` + emit NewRewardsDistributor(newRewardsDistributor); + } + + /// @notice Deletes a staking contract from the staking contract map and removes it from the + /// `stakingContractsList` + /// @param stakingContract Contract to remove + /// @dev Allows to clean some space and to avoid keeping in memory contracts which became useless + /// @dev It is also a way governance has to completely stop rewards distribution from a contract + function removeStakingContract(IStakingRewards stakingContract) external override onlyRole(GOVERNOR_ROLE) { + uint256 indexMet; + uint256 stakingContractsListLength = stakingContractsList.length; + require(stakingContractsListLength >= 1, "80"); + for (uint256 i = 0; i < stakingContractsListLength - 1; i++) { + if (stakingContractsList[i] == stakingContract) { + indexMet = 1; + stakingContractsList[i] = stakingContractsList[stakingContractsListLength - 1]; + break; + } + } + require(indexMet == 1 || stakingContractsList[stakingContractsListLength - 1] == stakingContract, "80"); + + stakingContractsList.pop(); + + delete stakingContractsMap[stakingContract]; + emit DeletedStakingContract(address(stakingContract)); + } + + // =================== Guardian Functions (for parameters) ===================== + + /// @notice Notifies and initializes a new staking contract + /// @param _stakingContract Address of the staking contract + /// @param _duration Time frame during which tokens will be distributed + /// @param _incentiveAmount Incentive amount given to keepers calling the update function + /// @param _updateFrequency Frequency when it is possible to call the update function and give tokens to the staking contract + /// @param _amountToDistribute Amount of gov tokens to give to the staking contract across all drips + /// @dev Called by governance to activate a contract + /// @dev After setting a new staking contract, everything is as if the contract had already been set for `_updateFrequency` + /// meaning that it is possible to `drip` the staking contract immediately after that + function setStakingContract( + address _stakingContract, + uint256 _duration, + uint256 _incentiveAmount, + uint256 _updateFrequency, + uint256 _amountToDistribute + ) external override onlyRole(GOVERNOR_ROLE) { + require(_duration > 0, "85"); + require(_duration >= _updateFrequency && block.timestamp >= _updateFrequency, "86"); + + IStakingRewards stakingContract = IStakingRewards(_stakingContract); + + require(stakingContract.rewardToken() == rewardToken, "83"); + + StakingParameters storage stakingParams = stakingContractsMap[stakingContract]; + + stakingParams.updateFrequency = _updateFrequency; + stakingParams.incentiveAmount = _incentiveAmount; + stakingParams.lastDistributionTime = block.timestamp - _updateFrequency; + // In order to allow a drip whenever a `stakingContract` is set, we consider that staking has already started + // `_updateFrequency` ago + stakingParams.timeStarted = block.timestamp - _updateFrequency; + stakingParams.duration = _duration; + stakingParams.amountToDistribute = _amountToDistribute; + stakingContractsList.push(stakingContract); + + emit NewStakingContract(_stakingContract); + } + + /// @notice Sets the update frequency + /// @param _updateFrequency New update frequency + /// @param stakingContract Reference to the staking contract + function setUpdateFrequency(uint256 _updateFrequency, IStakingRewards stakingContract) + external + override + onlyRole(GUARDIAN_ROLE) + { + StakingParameters storage stakingParams = stakingContractsMap[stakingContract]; + require(stakingParams.duration > 0, "80"); + require(stakingParams.duration >= _updateFrequency, "87"); + stakingParams.updateFrequency = _updateFrequency; + emit FrequencyUpdated(_updateFrequency, address(stakingContract)); + } + + /// @notice Sets the incentive amount for calling drip + /// @param _incentiveAmount New incentive amount + /// @param stakingContract Reference to the staking contract + function setIncentiveAmount(uint256 _incentiveAmount, IStakingRewards stakingContract) + external + override + onlyRole(GUARDIAN_ROLE) + { + StakingParameters storage stakingParams = stakingContractsMap[stakingContract]; + require(stakingParams.duration > 0, "80"); + stakingParams.incentiveAmount = _incentiveAmount; + emit IncentiveUpdated(_incentiveAmount, address(stakingContract)); + } + + /// @notice Sets the new amount to distribute to a staking contract + /// @param _amountToDistribute New amount to distribute + /// @param stakingContract Reference to the staking contract + function setAmountToDistribute(uint256 _amountToDistribute, IStakingRewards stakingContract) + external + override + onlyRole(GUARDIAN_ROLE) + { + StakingParameters storage stakingParams = stakingContractsMap[stakingContract]; + require(stakingParams.duration > 0, "80"); + require(stakingParams.distributedRewards < _amountToDistribute, "88"); + stakingParams.amountToDistribute = _amountToDistribute; + emit AmountToDistributeUpdated(_amountToDistribute, address(stakingContract)); + } + + /// @notice Sets the new duration with which tokens will be distributed to the staking contract + /// @param _duration New duration + /// @param stakingContract Reference to the staking contract + function setDuration(uint256 _duration, IStakingRewards stakingContract) external override onlyRole(GUARDIAN_ROLE) { + StakingParameters storage stakingParams = stakingContractsMap[stakingContract]; + require(stakingParams.duration > 0, "80"); + require(_duration >= stakingParams.updateFrequency, "87"); + uint256 timeElapsed = _timeSinceStart(stakingParams); + require(timeElapsed < stakingParams.duration && timeElapsed < _duration, "66"); + stakingParams.duration = _duration; + emit DurationUpdated(_duration, address(stakingContract)); + } + + // =========================== Internal Functions ============================== + + /// @notice Gives the next time when `drip` could be called + /// @param stakingParams Parameters of the concerned staking contract + /// @return Block timestamp when `drip` will next be available + function _nextDripAvailable(StakingParameters memory stakingParams) internal pure returns (uint256) { + return stakingParams.lastDistributionTime + stakingParams.updateFrequency; + } + + /// @notice Tells if `drip` can currently be called + /// @param stakingParams Parameters of the concerned staking contract + /// @return If the `updateFrequency` has passed since the last drip + function _isDripAvailable(StakingParameters memory stakingParams) internal view returns (bool) { + return block.timestamp >= _nextDripAvailable(stakingParams); + } + + /// @notice Computes the amount of tokens to give at the current drip + /// @param stakingParams Parameters of the concerned staking contract + /// @dev Constant drip amount across time + function _computeDripAmount(StakingParameters memory stakingParams) internal view returns (uint256) { + if (stakingParams.distributedRewards >= stakingParams.amountToDistribute) { + return 0; + } + uint256 dripAmount = (stakingParams.amountToDistribute * + (block.timestamp - stakingParams.lastDistributionTime)) / stakingParams.duration; + uint256 timeLeft = stakingParams.duration - _timeSinceStart(stakingParams); + uint256 rewardsLeftToDistribute = stakingParams.amountToDistribute - stakingParams.distributedRewards; + if (timeLeft < stakingParams.updateFrequency || rewardsLeftToDistribute < dripAmount || timeLeft == 0) { + return rewardsLeftToDistribute; + } else { + return dripAmount; + } + } + + /// @notice Computes the time since distribution has started for the staking contract + /// @param stakingParams Parameters of the concerned staking contract + /// @return The time since distribution has started for the staking contract + function _timeSinceStart(StakingParameters memory stakingParams) internal view returns (uint256) { + uint256 _duration = stakingParams.duration; + // `block.timestamp` is always greater than `timeStarted` + uint256 timePassed = block.timestamp - stakingParams.timeStarted; + return timePassed > _duration ? _duration : timePassed; + } + + /// @notice Incentivizes the person calling the drip function + /// @param stakingParams Parameters of the concerned staking contract + function _incentivize(StakingParameters memory stakingParams) internal { + rewardToken.safeTransfer(msg.sender, stakingParams.incentiveAmount); + } +} diff --git a/contracts/staking/RewardsDistributorEvents.sol b/contracts/staking/RewardsDistributorEvents.sol new file mode 100644 index 0000000..f2c37db --- /dev/null +++ b/contracts/staking/RewardsDistributorEvents.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../external/AccessControl.sol"; + +import "../interfaces/IRewardsDistributor.sol"; +import "../interfaces/IStakingRewards.sol"; + +/// @title RewardsDistributorEvents +/// @author Angle Core Team +/// @notice All the events used in `RewardsDistributor` contract +contract RewardsDistributorEvents { + event Dripped(address indexed _caller, uint256 _amount, address _stakingContract); + + event RewardTokenWithdrawn(uint256 _amount); + + event FrequencyUpdated(uint256 _frequency, address indexed _stakingContract); + + event IncentiveUpdated(uint256 _incentiveAmount, address indexed _stakingContract); + + event AmountToDistributeUpdated(uint256 _amountToDistribute, address indexed _stakingContract); + + event DurationUpdated(uint256 _duration, address indexed _stakingContract); + + event NewStakingContract(address indexed _stakingContract); + + event DeletedStakingContract(address indexed stakingContract); + + event NewRewardsDistributor(address indexed newRewardsDistributor); +} diff --git a/contracts/staking/StakingRewards.sol b/contracts/staking/StakingRewards.sol new file mode 100644 index 0000000..ee14dee --- /dev/null +++ b/contracts/staking/StakingRewards.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "./StakingRewardsEvents.sol"; + +/// @title StakingRewards +/// @author Forked form SetProtocol +/// https://github.com/SetProtocol/index-coop-contracts/blob/master/contracts/staking/StakingRewards.sol +/// @notice The `StakingRewards` contracts allows to stake an ERC20 token to receive as reward another ERC20 +/// @dev This contracts is managed by the reward distributor and implements the staking interface +contract StakingRewards is StakingRewardsEvents, IStakingRewards, ReentrancyGuard { + using SafeERC20 for IERC20; + + /// @notice Checks to see if it is the `rewardsDistribution` calling this contract + /// @dev There is no Access Control here, because it can be handled cheaply through these modifiers + modifier onlyRewardsDistribution() { + require(msg.sender == rewardsDistribution, "1"); + _; + } + + // ============================ References to contracts ======================== + + /// @notice ERC20 token given as reward + IERC20 public immutable override rewardToken; + + /// @notice ERC20 token used for staking + IERC20 public immutable stakingToken; + + /// @notice Base of the staked token, it is going to be used in the case of sanTokens + /// which are not in base 10**18 + uint256 public immutable stakingBase; + + /// @notice Rewards Distribution contract for this staking contract + address public rewardsDistribution; + + // ============================ Staking parameters ============================= + + /// @notice Time at which distribution ends + uint256 public periodFinish; + + /// @notice Reward per second given to the staking contract, split among the staked tokens + uint256 public rewardRate; + + /// @notice Duration of the reward distribution + uint256 public rewardsDuration; + + /// @notice Last time `rewardPerTokenStored` was updated + uint256 public lastUpdateTime; + + /// @notice Helps to compute the amount earned by someone + /// Cumulates rewards accumulated for one token since the beginning. + /// Stored as a uint so it is actually a float times the base of the reward token + uint256 public rewardPerTokenStored; + + /// @notice Stores for each account the `rewardPerToken`: we do the difference + /// between the current and the old value to compute what has been earned by an account + mapping(address => uint256) public userRewardPerTokenPaid; + + /// @notice Stores for each account the accumulated rewards + mapping(address => uint256) public rewards; + + uint256 private _totalSupply; + + mapping(address => uint256) private _balances; + + // ============================ Constructor ==================================== + + /// @notice Initializes the staking contract with a first set of parameters + /// @param _rewardsDistribution Address owning the rewards token + /// @param _rewardToken ERC20 token given as reward + /// @param _stakingToken ERC20 token used for staking + /// @param _rewardsDuration Duration of the staking contract + constructor( + address _rewardsDistribution, + address _rewardToken, + address _stakingToken, + uint256 _rewardsDuration + ) { + require(_stakingToken != address(0) && _rewardToken != address(0) && _rewardsDistribution != address(0), "0"); + + // We are not checking the compatibility of the reward token between the distributor and this contract here + // because it is checked by the `RewardsDistributor` when activating the staking contract + // Parameters + rewardToken = IERC20(_rewardToken); + stakingToken = IERC20(_stakingToken); + rewardsDuration = _rewardsDuration; + rewardsDistribution = _rewardsDistribution; + + stakingBase = 10**IERC20Metadata(_stakingToken).decimals(); + } + + // ============================ Modifiers ====================================== + + /// @notice Checks to see if the calling address is the zero address + /// @param account Address to check + modifier zeroCheck(address account) { + require(account != address(0), "0"); + _; + } + + /// @notice Called frequently to update the staking parameters associated to an address + /// @param account Address of the account to update + modifier updateReward(address account) { + rewardPerTokenStored = rewardPerToken(); + lastUpdateTime = lastTimeRewardApplicable(); + if (account != address(0)) { + rewards[account] = earned(account); + userRewardPerTokenPaid[account] = rewardPerTokenStored; + } + _; + } + + // ============================ View functions ================================= + + /// @notice Accesses the total supply + /// @dev Used instead of having a public variable to respect the ERC20 standard + function totalSupply() external view returns (uint256) { + return _totalSupply; + } + + /// @notice Accesses the number of token staked by an account + /// @param account Account to query the balance of + /// @dev Used instead of having a public variable to respect the ERC20 standard + function balanceOf(address account) external view returns (uint256) { + return _balances[account]; + } + + /// @notice Queries the last timestamp at which a reward was distributed + /// @dev Returns the current timestamp if a reward is being distributed and the end of the staking + /// period if staking is done + function lastTimeRewardApplicable() public view returns (uint256) { + return Math.min(block.timestamp, periodFinish); + } + + /// @notice Used to actualize the `rewardPerTokenStored` + /// @dev It adds to the reward per token: the time elapsed since the `rewardPerTokenStored` was + /// last updated multiplied by the `rewardRate` divided by the number of tokens + function rewardPerToken() public view returns (uint256) { + if (_totalSupply == 0) { + return rewardPerTokenStored; + } + return + rewardPerTokenStored + + (((lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * stakingBase) / _totalSupply); + } + + /// @notice Returns how much a given account earned rewards + /// @param account Address for which the request is made + /// @return How much a given account earned rewards + /// @dev It adds to the rewards the amount of reward earned since last time that is the difference + /// in reward per token from now and last time multiplied by the number of tokens staked by the person + function earned(address account) public view returns (uint256) { + return + (_balances[account] * (rewardPerToken() - userRewardPerTokenPaid[account])) / + stakingBase + + rewards[account]; + } + + // ======================== Mutative functions forked ========================== + + /// @notice Lets someone stake a given amount of `stakingTokens` + /// @param amount Amount of ERC20 staking token that the `msg.sender` wants to stake + function stake(uint256 amount) external nonReentrant updateReward(msg.sender) { + _stake(amount, msg.sender); + } + + /// @notice Lets a user withdraw a given amount of collateral from the staking contract + /// @param amount Amount of the ERC20 staking token that the `msg.sender` wants to withdraw + function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) { + require(amount > 0, "89"); + _totalSupply = _totalSupply - amount; + _balances[msg.sender] = _balances[msg.sender] - amount; + stakingToken.safeTransfer(msg.sender, amount); + emit Withdrawn(msg.sender, amount); + } + + /// @notice Triggers a payment of the reward earned to the msg.sender + function getReward() public nonReentrant updateReward(msg.sender) { + uint256 reward = rewards[msg.sender]; + if (reward > 0) { + rewards[msg.sender] = 0; + rewardToken.safeTransfer(msg.sender, reward); + emit RewardPaid(msg.sender, reward); + } + } + + /// @notice Exits someone + /// @dev This function lets the caller withdraw its staking and claim rewards + // Attention here, there may be reentrancy attacks because of the following call + // to an external contract done before other things are modified, yet since the `rewardToken` + // is mostly going to be a trusted contract controlled by governance (namely the ANGLE token), + // this is not an issue. If the `rewardToken` changes to an untrusted contract, this need to be updated. + function exit() external { + withdraw(_balances[msg.sender]); + getReward(); + } + + // ====================== Functions added by Angle Core Team =================== + + /// @notice Allows to stake on behalf of another address + /// @param amount Amount to stake + /// @param onBehalf Address to stake onBehalf of + function stakeOnBehalf(uint256 amount, address onBehalf) + external + nonReentrant + zeroCheck(onBehalf) + updateReward(onBehalf) + { + _stake(amount, onBehalf); + } + + /// @notice Internal function to stake called by `stake` and `stakeOnBehalf` + /// @param amount Amount to stake + /// @param onBehalf Address to stake on behalf of + /// @dev Before calling this function, it has already been verified whether this address was a zero address or not + function _stake(uint256 amount, address onBehalf) internal { + require(amount > 0, "90"); + stakingToken.safeTransferFrom(msg.sender, address(this), amount); + _totalSupply = _totalSupply + amount; + _balances[onBehalf] = _balances[onBehalf] + amount; + emit Staked(onBehalf, amount); + } + + // ====================== Restricted Functions ================================= + + /// @notice Adds rewards to be distributed + /// @param reward Amount of reward tokens to distribute + /// @dev This reward will be distributed during `rewardsDuration` set previously + function notifyRewardAmount(uint256 reward) + external + override + onlyRewardsDistribution + nonReentrant + updateReward(address(0)) + { + if (block.timestamp >= periodFinish) { + // If no reward is currently being distributed, the new rate is just `reward / duration` + rewardRate = reward / rewardsDuration; + } else { + // Otherwise, cancel the future reward and add the amount left to distribute to reward + uint256 remaining = periodFinish - block.timestamp; + uint256 leftover = remaining * rewardRate; + rewardRate = (reward + leftover) / rewardsDuration; + } + + // Ensures the provided reward amount is not more than the balance in the contract. + // This keeps the reward rate in the right range, preventing overflows due to + // very high values of `rewardRate` in the earned and `rewardsPerToken` functions; + // Reward + leftover must be less than 2^256 / 10^18 to avoid overflow. + uint256 balance = rewardToken.balanceOf(address(this)); + require(rewardRate <= balance / rewardsDuration, "91"); + + lastUpdateTime = block.timestamp; + periodFinish = block.timestamp + rewardsDuration; // Change the duration + emit RewardAdded(reward); + } + + /// @notice Withdraws ERC20 tokens that could accrue on this contract + /// @param tokenAddress Address of the ERC20 token to withdraw + /// @param to Address to transfer to + /// @param amount Amount to transfer + /// @dev A use case would be to claim tokens if the staked tokens accumulate rewards + function recoverERC20( + address tokenAddress, + address to, + uint256 amount + ) external override onlyRewardsDistribution { + require(tokenAddress != address(stakingToken) && tokenAddress != address(rewardToken), "20"); + + IERC20(tokenAddress).safeTransfer(to, amount); + emit Recovered(tokenAddress, to, amount); + } + + /// @notice Changes the rewards distributor associated to this contract + /// @param _rewardsDistribution Address of the new rewards distributor contract + /// @dev This function was also added by Angle Core Team + /// @dev A compatibility check of the reward token is already performed in the current `RewardsDistributor` implementation + /// which has right to call this function + function setNewRewardsDistribution(address _rewardsDistribution) external override onlyRewardsDistribution { + rewardsDistribution = _rewardsDistribution; + emit RewardsDistributionUpdated(_rewardsDistribution); + } +} diff --git a/contracts/staking/StakingRewardsEvents.sol b/contracts/staking/StakingRewardsEvents.sol new file mode 100644 index 0000000..0841c86 --- /dev/null +++ b/contracts/staking/StakingRewardsEvents.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.7; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +import "../external/AccessControl.sol"; + +import "../interfaces/IRewardsDistributor.sol"; +import "../interfaces/IStakingRewards.sol"; + +/// @title StakingRewardsEvents +/// @author Angle Core Team +/// @notice All the events used in `StakingRewards` contract +contract StakingRewardsEvents { + event RewardAdded(uint256 reward); + + event Staked(address indexed user, uint256 amount); + + event Withdrawn(address indexed user, uint256 amount); + + event RewardPaid(address indexed user, uint256 reward); + + event Recovered(address indexed tokenAddress, address indexed to, uint256 amount); + + event RewardsDistributionUpdated(address indexed _rewardsDistribution); +} diff --git a/contracts/staking/veBoost.vy b/contracts/staking/veBoost.vy new file mode 100644 index 0000000..4b4a1a0 --- /dev/null +++ b/contracts/staking/veBoost.vy @@ -0,0 +1,1000 @@ +# @version 0.2.16 +""" +@title Voting Escrow Delegation +@author Angle Protocol +@license MIT +@dev Provides test functions only available in test mode (`brownie test`) +""" + +# Full fork from: +# Curve Finance's veBoost + +interface ERC721Receiver: + def onERC721Received( + _operator: address, _from: address, _token_id: uint256, _data: Bytes[4096] + ) -> bytes32: + nonpayable + +interface VotingEscrow: + def balanceOf(_account: address) -> int256: view + def locked__end(_addr: address) -> uint256: view + + +event Approval: + _owner: indexed(address) + _approved: indexed(address) + _token_id: indexed(uint256) + +event ApprovalForAll: + _owner: indexed(address) + _operator: indexed(address) + _approved: bool + +event Transfer: + _from: indexed(address) + _to: indexed(address) + _token_id: indexed(uint256) + +event BurnBoost: + _delegator: indexed(address) + _receiver: indexed(address) + _token_id: indexed(uint256) + +event DelegateBoost: + _delegator: indexed(address) + _receiver: indexed(address) + _token_id: indexed(uint256) + _amount: uint256 + _cancel_time: uint256 + _expire_time: uint256 + +event ExtendBoost: + _delegator: indexed(address) + _receiver: indexed(address) + _token_id: indexed(uint256) + _amount: uint256 + _expire_time: uint256 + _cancel_time: uint256 + +event TransferBoost: + _from: indexed(address) + _to: indexed(address) + _token_id: indexed(uint256) + _amount: uint256 + _expire_time: uint256 + +event GreyListUpdated: + _receiver: indexed(address) + _delegator: indexed(address) + _status: bool + + +struct Boost: + # [bias uint128][slope int128] + delegated: uint256 + received: uint256 + # [total active delegations 128][next expiry 128] + expiry_data: uint256 + +struct Token: + # [bias uint128][slope int128] + data: uint256 + # [delegator pos 128][cancel time 128] + dinfo: uint256 + # [global 128][local 128] + position: uint256 + expire_time: uint256 + +struct Point: + bias: int256 + slope: int256 + + +IDENTITY_PRECOMPILE: constant(address) = 0x0000000000000000000000000000000000000004 +MAX_PCT: constant(uint256) = 10_000 +WEEK: constant(uint256) = 86400 * 7 + +voting_escrow: public(address) + +balanceOf: public(HashMap[address, uint256]) +getApproved: public(HashMap[uint256, address]) +isApprovedForAll: public(HashMap[address, HashMap[address, bool]]) +ownerOf: public(HashMap[uint256, address]) + +name: public(String[32]) +symbol: public(String[32]) +base_uri: public(String[128]) + +totalSupply: public(uint256) +# use totalSupply to determine the length +tokenByIndex: public(HashMap[uint256, uint256]) +# use balanceOf to determine the length +tokenOfOwnerByIndex: public(HashMap[address, uint256[MAX_UINT256]]) + +boost: HashMap[address, Boost] +boost_tokens: HashMap[uint256, Token] + +token_of_delegator_by_index: public(HashMap[address, uint256[MAX_UINT256]]) +total_minted: public(HashMap[address, uint256]) +# address => timestamp => # of delegations expiring +account_expiries: public(HashMap[address, HashMap[uint256, uint256]]) + +admin: public(address) # Can and will be a smart contract +future_admin: public(address) + +# The grey list - per-user black and white lists +# users can make this a blacklist or a whitelist - defaults to blacklist +# gray_list[_receiver][_delegator] +# by default is blacklist, with no delegators blacklisted +# if [_receiver][ZERO_ADDRESS] is False = Blacklist, True = Whitelist +# if this is a blacklist, receivers disallow any delegations from _delegator if it is True +# if this is a whitelist, receivers only allow delegations from _delegator if it is True +# Delegation will go through if: not (grey_list[_receiver][ZERO_ADDRESS] ^ grey_list[_receiver][_delegator]) +grey_list: public(HashMap[address, HashMap[address, bool]]) + + +@external +def __init__(_admin: address, _voting_escrow: address, _name: String[32], _symbol: String[32], _base_uri: String[128]): + # These zero checks were added by Angle Core Team + assert _voting_escrow != ZERO_ADDRESS + assert _admin != ZERO_ADDRESS + + self.voting_escrow = _voting_escrow + + self.name = _name + self.symbol = _symbol + self.base_uri = _base_uri + + self.admin = _admin + + +@internal +def _approve(_owner: address, _approved: address, _token_id: uint256): + self.getApproved[_token_id] = _approved + log Approval(_owner, _approved, _token_id) + + +@view +@internal +def _is_approved_or_owner(_spender: address, _token_id: uint256) -> bool: + owner: address = self.ownerOf[_token_id] + return ( + _spender == owner + or _spender == self.getApproved[_token_id] + or self.isApprovedForAll[owner][_spender] + ) + + +@internal +def _update_enumeration_data(_from: address, _to: address, _token_id: uint256): + delegator: address = convert(shift(_token_id, -96), address) + position_data: uint256 = self.boost_tokens[_token_id].position + local_pos: uint256 = position_data % 2 ** 128 + global_pos: uint256 = shift(position_data, -128) + # position in the delegator array of minted tokens + delegator_pos: uint256 = shift(self.boost_tokens[_token_id].dinfo, -128) + + if _from == ZERO_ADDRESS: + # minting - This is called before updates to balance and totalSupply + local_pos = self.balanceOf[_to] + global_pos = self.totalSupply + position_data = shift(global_pos, 128) + local_pos + # this is a new token so we get the index of a new spot + delegator_pos = self.total_minted[delegator] + + self.tokenByIndex[global_pos] = _token_id + self.tokenOfOwnerByIndex[_to][local_pos] = _token_id + self.boost_tokens[_token_id].position = position_data + + # we only mint tokens in the create_boost fn, and this is called + # before we update the cancel_time so we can just set the value + # of dinfo to the shifted position + self.boost_tokens[_token_id].dinfo = shift(delegator_pos, 128) + self.token_of_delegator_by_index[delegator][delegator_pos] = _token_id + self.total_minted[delegator] = delegator_pos + 1 + + elif _to == ZERO_ADDRESS: + # burning - This is called after updates to balance and totalSupply + # we operate on both the global array and local array + last_global_index: uint256 = self.totalSupply + last_local_index: uint256 = self.balanceOf[_from] + last_delegator_pos: uint256 = self.total_minted[delegator] - 1 + + if global_pos != last_global_index: + # swap - set the token we're burnings position to the token in the last index + last_global_token: uint256 = self.tokenByIndex[last_global_index] + last_global_token_pos: uint256 = self.boost_tokens[last_global_token].position + # update the global position of the last global token + self.boost_tokens[last_global_token].position = shift(global_pos, 128) + (last_global_token_pos % 2 ** 128) + self.tokenByIndex[global_pos] = last_global_token + self.tokenByIndex[last_global_index] = 0 + + if local_pos != last_local_index: + # swap - set the token we're burnings position to the token in the last index + last_local_token: uint256 = self.tokenOfOwnerByIndex[_from][last_local_index] + last_local_token_pos: uint256 = self.boost_tokens[last_local_token].position + # update the local position of the last local token + self.boost_tokens[last_local_token].position = shift(last_local_token_pos / 2 ** 128, 128) + local_pos + self.tokenOfOwnerByIndex[_from][local_pos] = last_local_token + self.tokenOfOwnerByIndex[_from][last_local_index] = 0 + self.boost_tokens[_token_id].position = 0 + + if delegator_pos != last_delegator_pos: + last_delegator_token: uint256 = self.token_of_delegator_by_index[delegator][last_delegator_pos] + last_delegator_token_dinfo: uint256 = self.boost_tokens[last_delegator_token].dinfo + # update the last tokens position data and maintain the correct cancel time + self.boost_tokens[last_delegator_token].dinfo = shift(delegator_pos, 128) + (last_delegator_token_dinfo % 2 ** 128) + self.token_of_delegator_by_index[delegator][delegator_pos] = last_delegator_token + self.token_of_delegator_by_index[delegator][last_delegator_pos] = 0 + self.boost_tokens[_token_id].dinfo = 0 # we are burning the token so we can just set to 0 + self.total_minted[delegator] = last_delegator_pos + + else: + # transfering - called between balance updates + from_last_index: uint256 = self.balanceOf[_from] + + if local_pos != from_last_index: + # swap - set the token we're burnings position to the token in the last index + last_local_token: uint256 = self.tokenOfOwnerByIndex[_from][from_last_index] + last_local_token_pos: uint256 = self.boost_tokens[last_local_token].position + # update the local position of the last local token + self.boost_tokens[last_local_token].position = shift(last_local_token_pos / 2 ** 128, 128) + local_pos + self.tokenOfOwnerByIndex[_from][local_pos] = last_local_token + self.tokenOfOwnerByIndex[_from][from_last_index] = 0 + + # to is simple we just add to the end of the list + local_pos = self.balanceOf[_to] + self.tokenOfOwnerByIndex[_to][local_pos] = _token_id + self.boost_tokens[_token_id].position = shift(global_pos, 128) + local_pos + + +@internal +def _burn(_token_id: uint256): + owner: address = self.ownerOf[_token_id] + + self._approve(owner, ZERO_ADDRESS, _token_id) + + self.balanceOf[owner] -= 1 + self.ownerOf[_token_id] = ZERO_ADDRESS + self.totalSupply -= 1 + + self._update_enumeration_data(owner, ZERO_ADDRESS, _token_id) + + log Transfer(owner, ZERO_ADDRESS, _token_id) + + +@internal +def _mint(_to: address, _token_id: uint256): + assert _to != ZERO_ADDRESS # dev: minting to ZERO_ADDRESS disallowed + assert self.ownerOf[_token_id] == ZERO_ADDRESS # dev: token exists + + self._update_enumeration_data(ZERO_ADDRESS, _to, _token_id) + + self.balanceOf[_to] += 1 + self.ownerOf[_token_id] = _to + self.totalSupply += 1 + + log Transfer(ZERO_ADDRESS, _to, _token_id) + + +@internal +def _mint_boost(_token_id: uint256, _delegator: address, _receiver: address, _bias: int256, _slope: int256, _cancel_time: uint256, _expire_time: uint256): + is_whitelist: uint256 = convert(self.grey_list[_receiver][ZERO_ADDRESS], uint256) + delegator_status: uint256 = convert(self.grey_list[_receiver][_delegator], uint256) + assert not convert(bitwise_xor(is_whitelist, delegator_status), bool) # dev: mint boost not allowed + + data: uint256 = shift(convert(_bias, uint256), 128) + convert(abs(_slope), uint256) + self.boost[_delegator].delegated += data + self.boost[_receiver].received += data + + token: Token = self.boost_tokens[_token_id] + token.data = data + token.dinfo = token.dinfo + _cancel_time + token.expire_time = _expire_time + self.boost_tokens[_token_id] = token + + +@internal +def _burn_boost(_token_id: uint256, _delegator: address, _receiver: address, _bias: int256, _slope: int256): + token: Token = self.boost_tokens[_token_id] + expire_time: uint256 = token.expire_time + + if expire_time == 0: + return + + self.boost[_delegator].delegated -= token.data + self.boost[_receiver].received -= token.data + + token.data = 0 + # maintain the same position in the delegator array, but remove the cancel time + token.dinfo = shift(token.dinfo / 2 ** 128, 128) + token.expire_time = 0 + self.boost_tokens[_token_id] = token + + # update the next expiry data + expiry_data: uint256 = self.boost[_delegator].expiry_data + next_expiry: uint256 = expiry_data % 2 ** 128 + active_delegations: uint256 = shift(expiry_data, -128) - 1 + + expiries: uint256 = self.account_expiries[_delegator][expire_time] + + if active_delegations != 0 and expire_time == next_expiry and expiries == 0: + # Will be passed if + # active_delegations == 0, no more active boost tokens + # or + # expire_time != next_expiry, the cancelled boost token isn't the next expiring boost token + # or + # expiries != 0, the cancelled boost token isn't the only one expiring at expire_time + for i in range(1, 513): # ~10 years + # we essentially allow for a boost token be expired for up to 6 years + # 10 yrs - 4 yrs (max vecRV lock time) = ~ 6 yrs + if i == 512: + raise "Failed to find next expiry" + week_ts: uint256 = expire_time + WEEK * (i + 1) + if self.account_expiries[_delegator][week_ts] > 0: + next_expiry = week_ts + break + elif active_delegations == 0: + next_expiry = 0 + + self.boost[_delegator].expiry_data = shift(active_delegations, 128) + next_expiry + self.account_expiries[_delegator][expire_time] = expiries - 1 + + +@internal +def _transfer_boost(_from: address, _to: address, _bias: int256, _slope: int256): + data: uint256 = shift(convert(_bias, uint256), 128) + convert(abs(_slope), uint256) + self.boost[_from].received -= data + self.boost[_to].received += data + + +@pure +@internal +def _deconstruct_bias_slope(_data: uint256) -> Point: + return Point({bias: convert(shift(_data, -128), int256), slope: -convert(_data % 2 ** 128, int256)}) + + +@pure +@internal +def _calc_bias_slope(_x: int256, _y: int256, _expire_time: int256) -> Point: + # SLOPE: (y2 - y1) / (x2 - x1) + # BIAS: y = mx + b -> y - mx = b + slope: int256 = -_y / (_expire_time - _x) + return Point({bias: _y - slope * _x, slope: slope}) + + +@internal +def _transfer(_from: address, _to: address, _token_id: uint256): + assert self.ownerOf[_token_id] == _from # dev: _from is not owner + assert _to != ZERO_ADDRESS # dev: transfers to ZERO_ADDRESS are disallowed + + delegator: address = convert(shift(_token_id, -96), address) + is_whitelist: uint256 = convert(self.grey_list[_to][ZERO_ADDRESS], uint256) + delegator_status: uint256 = convert(self.grey_list[_to][delegator], uint256) + assert not convert(bitwise_xor(is_whitelist, delegator_status), bool) # dev: transfer boost not allowed + + # clear previous token approval + self._approve(_from, ZERO_ADDRESS, _token_id) + + self.balanceOf[_from] -= 1 + self._update_enumeration_data(_from, _to, _token_id) + self.balanceOf[_to] += 1 + self.ownerOf[_token_id] = _to + + tpoint: Point = self._deconstruct_bias_slope(self.boost_tokens[_token_id].data) + tvalue: int256 = tpoint.slope * convert(block.timestamp, int256) + tpoint.bias + + # if the boost value is negative, reset the slope and bias + if tvalue > 0: + self._transfer_boost(_from, _to, tpoint.bias, tpoint.slope) + # y = mx + b -> y - b = mx -> (y - b)/m = x -> -b / m = x (x-intercept) + expiry: uint256 = convert(-tpoint.bias / tpoint.slope, uint256) + log TransferBoost(_from, _to, _token_id, convert(tvalue, uint256), expiry) + else: + self._burn_boost(_token_id, delegator, _from, tpoint.bias, tpoint.slope) + log BurnBoost(delegator, _from, _token_id) + + log Transfer(_from, _to, _token_id) + + +@internal +def _cancel_boost(_token_id: uint256, _caller: address): + receiver: address = self.ownerOf[_token_id] + assert receiver != ZERO_ADDRESS # dev: token does not exist + delegator: address = convert(shift(_token_id, -96), address) + + token: Token = self.boost_tokens[_token_id] + tpoint: Point = self._deconstruct_bias_slope(token.data) + tvalue: int256 = tpoint.slope * convert(block.timestamp, int256) + tpoint.bias + + # if not (the owner or operator or the boost value is negative) + if not (_caller == receiver or self.isApprovedForAll[receiver][_caller] or tvalue <= 0): + if _caller == delegator or self.isApprovedForAll[delegator][_caller]: + # if delegator or operator, wait till after cancel time + assert (token.dinfo % 2 ** 128) <= block.timestamp # dev: must wait for cancel time + else: + # All others are disallowed + raise "Not allowed!" + self._burn_boost(_token_id, delegator, receiver, tpoint.bias, tpoint.slope) + + log BurnBoost(delegator, receiver, _token_id) + + +@internal +def _set_delegation_status(_receiver: address, _delegator: address, _status: bool): + self.grey_list[_receiver][_delegator] = _status + log GreyListUpdated(_receiver, _delegator, _status) + + +@pure +@internal +def _uint_to_string(_value: uint256) -> String[78]: + # NOTE: Odd that this works with a raw_call inside, despite being marked + # a pure function + if _value == 0: + return "0" + + buffer: Bytes[78] = b"" + digits: uint256 = 78 + + for i in range(78): + # go forward to find the # of digits, and set it + # only if we have found the last index + if digits == 78 and _value / 10 ** i == 0: + digits = i + + value: uint256 = ((_value / 10 ** (77 - i)) % 10) + 48 + char: Bytes[1] = slice(convert(value, bytes32), 31, 1) + buffer = raw_call( + IDENTITY_PRECOMPILE, + concat(buffer, char), + max_outsize=78, + is_static_call=True + ) + + return convert(slice(buffer, 78 - digits, digits), String[78]) + + +@external +def approve(_approved: address, _token_id: uint256): + """ + @notice Change or reaffirm the approved address for an NFT. + @dev The zero address indicates there is no approved address. + Throws unless `msg.sender` is the current NFT owner, or an authorized + operator of the current owner. + @param _approved The new approved NFT controller. + @param _token_id The NFT to approve. + """ + owner: address = self.ownerOf[_token_id] + assert ( + msg.sender == owner or self.isApprovedForAll[owner][msg.sender] + ) # dev: must be owner or operator + self._approve(owner, _approved, _token_id) + + +@external +def safeTransferFrom(_from: address, _to: address, _token_id: uint256, _data: Bytes[4096] = b""): + """ + @notice Transfers the ownership of an NFT from one address to another address + @dev Throws unless `msg.sender` is the current owner, an authorized + operator, or the approved address for this NFT. Throws if `_from` is + not the current owner. Throws if `_to` is the zero address. Throws if + `_tokenId` is not a valid NFT. When transfer is complete, this function + checks if `_to` is a smart contract (code size > 0). If so, it calls + `onERC721Received` on `_to` and throws if the return value is not + `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. + @param _from The current owner of the NFT + @param _to The new owner + @param _token_id The NFT to transfer + @param _data Additional data with no specified format, sent in call to `_to`, max length 4096 + """ + assert self._is_approved_or_owner(msg.sender, _token_id) # dev: neither owner nor approved + self._transfer(_from, _to, _token_id) + + if _to.is_contract: + response: bytes32 = ERC721Receiver(_to).onERC721Received( + msg.sender, _from, _token_id, _data + ) + assert slice(response, 0, 4) == method_id( + "onERC721Received(address,address,uint256,bytes)" + ) # dev: invalid response + + +@external +def setApprovalForAll(_operator: address, _approved: bool): + """ + @notice Enable or disable approval for a third party ("operator") to manage + all of `msg.sender`'s assets. + @dev Emits the ApprovalForAll event. Multiple operators per account are allowed. + @param _operator Address to add to the set of authorized operators. + @param _approved True if the operator is approved, false to revoke approval. + """ + self.isApprovedForAll[msg.sender][_operator] = _approved + log ApprovalForAll(msg.sender, _operator, _approved) + + +@external +def transferFrom(_from: address, _to: address, _token_id: uint256): + """ + @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE + THEY MAY BE PERMANENTLY LOST + @dev Throws unless `msg.sender` is the current owner, an authorized + operator, or the approved address for this NFT. Throws if `_from` is + not the current owner. Throws if `_to` is the ZERO_ADDRESS. + @param _from The current owner of the NFT + @param _to The new owner + @param _token_id The NFT to transfer + """ + assert self._is_approved_or_owner(msg.sender, _token_id) # dev: neither owner nor approved + self._transfer(_from, _to, _token_id) + + +@view +@external +def tokenURI(_token_id: uint256) -> String[256]: + return concat(self.base_uri, self._uint_to_string(_token_id)) + + +@external +def burn(_token_id: uint256): + """ + @notice Destroy a token + @dev Only callable by the token owner, their operator, or an approved account. + Burning a token with a currently active boost, burns the boost. + @param _token_id The token to burn + """ + assert self._is_approved_or_owner(msg.sender, _token_id) # dev: neither owner nor approved + + tdata: uint256 = self.boost_tokens[_token_id].data + if tdata != 0: + tpoint: Point = self._deconstruct_bias_slope(tdata) + + delegator: address = convert(shift(_token_id, -96), address) + owner: address = self.ownerOf[_token_id] + + self._burn_boost(_token_id, delegator, owner, tpoint.bias, tpoint.slope) + + log BurnBoost(delegator, owner, _token_id) + + self._burn(_token_id) + + +@external +def create_boost( + _delegator: address, + _receiver: address, + _percentage: int256, + _cancel_time: uint256, + _expire_time: uint256, + _id: uint256, +): + """ + @notice Create a boost and delegate it to another account. + @dev Delegated boost can become negative, and requires active management, else + the adjusted veCRV balance of the delegator's account will decrease until reaching 0 + @param _delegator The account to delegate boost from + @param _receiver The account to receive the delegated boost + @param _percentage Since veCRV is a constantly decreasing asset, we use percentage to determine + the amount of delegator's boost to delegate + @param _cancel_time A point in time before _expire_time in which the delegator or their operator + can cancel the delegated boost + @param _expire_time The point in time, atleast a day in the future, at which the value of the boost + will reach 0. After which the negative value is deducted from the delegator's account (and the + receiver's received boost only) until it is cancelled. This value is rounded down to the nearest + WEEK. + @param _id The token id, within the range of [0, 2 ** 96). Useful for contracts given operator status + to have specific ranges. + """ + assert msg.sender == _delegator or self.isApprovedForAll[_delegator][msg.sender] # dev: only delegator or operator + + expire_time: uint256 = (_expire_time / WEEK) * WEEK + + expiry_data: uint256 = self.boost[_delegator].expiry_data + next_expiry: uint256 = expiry_data % 2 ** 128 + + if next_expiry == 0: + next_expiry = MAX_UINT256 + + assert block.timestamp < next_expiry # dev: negative boost token is in circulation + assert _percentage > 0 # dev: percentage must be greater than 0 bps + assert _percentage <= MAX_PCT # dev: percentage must be less than 10_000 bps + assert _cancel_time <= expire_time # dev: cancel time is after expiry + + assert expire_time >= block.timestamp + WEEK # dev: boost duration must be atleast WEEK + assert expire_time <= VotingEscrow(self.voting_escrow).locked__end(_delegator) # dev: boost expiration is past voting escrow lock expiry + assert _id < 2 ** 96 # dev: id out of bounds + + # [delegator address 160][cancel_time uint40][id uint56] + token_id: uint256 = shift(convert(_delegator, uint256), 96) + _id + # check if the token exists here before we expend more gas by minting it + self._mint(_receiver, token_id) + + # delegated slope and bias + point: Point = self._deconstruct_bias_slope(self.boost[_delegator].delegated) + + time: int256 = convert(block.timestamp, int256) + + # delegated boost will be positive, if any of circulating boosts are negative + # we have already reverted + delegated_boost: int256 = point.slope * time + point.bias + y: int256 = _percentage * (VotingEscrow(self.voting_escrow).balanceOf(_delegator) - delegated_boost) / MAX_PCT + assert y > 0 # dev: no boost + + point = self._calc_bias_slope(time, y, convert(expire_time, int256)) + assert point.slope < 0 # dev: invalid slope + + self._mint_boost(token_id, _delegator, _receiver, point.bias, point.slope, _cancel_time, expire_time) + + # increase the number of expiries for the user + if expire_time < next_expiry: + next_expiry = expire_time + + active_delegations: uint256 = shift(expiry_data, -128) + self.account_expiries[_delegator][expire_time] += 1 + self.boost[_delegator].expiry_data = shift(active_delegations + 1, 128) + next_expiry + + log DelegateBoost(_delegator, _receiver, token_id, convert(y, uint256), _cancel_time, _expire_time) + + +@external +def extend_boost(_token_id: uint256, _percentage: int256, _expire_time: uint256, _cancel_time: uint256): + """ + @notice Extend the boost of an existing boost or expired boost + @dev The extension can not decrease the value of the boost. If there are + any outstanding negative value boosts which cause the delegable boost + of an account to be negative this call will revert + @param _token_id The token to extend the boost of + @param _percentage The percentage of delegable boost to delegate + AFTER burning the token's current boost + @param _expire_time The new time at which the boost value will become + 0, and eventually negative. Must be greater than the previous expiry time, + and atleast a WEEK from now, and less than the veCRV lock expiry of the + delegator's account. This value is rounded down to the nearest WEEK. + """ + delegator: address = convert(shift(_token_id, -96), address) + receiver: address = self.ownerOf[_token_id] + + assert msg.sender == delegator or self.isApprovedForAll[delegator][msg.sender] # dev: only delegator or operator + assert receiver != ZERO_ADDRESS # dev: boost token does not exist + assert _percentage > 0 # dev: percentage must be greater than 0 bps + assert _percentage <= MAX_PCT # dev: percentage must be less than 10_000 bps + + # timestamp when delegating account's voting escrow ends - also our second point (lock_expiry, 0) + token: Token = self.boost_tokens[_token_id] + + expire_time: uint256 = (_expire_time / WEEK) * WEEK + + assert _cancel_time <= expire_time # dev: cancel time is after expiry + assert expire_time >= block.timestamp + WEEK # dev: boost duration must be atleast one day + assert expire_time <= VotingEscrow(self.voting_escrow).locked__end(delegator) # dev: boost expiration is past voting escrow lock expiry + + point: Point = self._deconstruct_bias_slope(token.data) + + time: int256 = convert(block.timestamp, int256) + tvalue: int256 = point.slope * time + point.bias + + # Can extend a token by increasing it's amount but not it's expiry time + assert expire_time >= token.expire_time # dev: new expiration must be greater than old token expiry + + # if we are extending an unexpired boost, the cancel time must the same or greater + # else we can adjust the cancel time to our preference + if _cancel_time < (token.dinfo % 2 ** 128): + assert block.timestamp >= token.expire_time # dev: cancel time reduction disallowed + + # storage variables have been updated: next_expiry + active_delegations + self._burn_boost(_token_id, delegator, receiver, point.bias, point.slope) + + expiry_data: uint256 = self.boost[delegator].expiry_data + next_expiry: uint256 = expiry_data % 2 ** 128 + + if next_expiry == 0: + next_expiry = MAX_UINT256 + + assert block.timestamp < next_expiry # dev: negative outstanding boosts + + # delegated slope and bias + point = self._deconstruct_bias_slope(self.boost[delegator].delegated) + + # verify delegated boost isn't negative, else it'll inflate out vecrv balance + delegated_boost: int256 = point.slope * time + point.bias + y: int256 = _percentage * (VotingEscrow(self.voting_escrow).balanceOf(delegator) - delegated_boost) / MAX_PCT + # a delegator can snipe the exact moment a token expires and create a boost + # with 10_000 or some percentage of their boost, which is perfectly fine. + # this check is here so the user can't extend a boost unless they actually + # have any to give + assert y > 0 # dev: no boost + assert y >= tvalue # dev: cannot reduce value of boost + + point = self._calc_bias_slope(time, y, convert(expire_time, int256)) + assert point.slope < 0 # dev: invalid slope + + self._mint_boost(_token_id, delegator, receiver, point.bias, point.slope, _cancel_time, expire_time) + + # increase the number of expiries for the user + if expire_time < next_expiry: + next_expiry = expire_time + + active_delegations: uint256 = shift(expiry_data, -128) + self.account_expiries[delegator][expire_time] += 1 + self.boost[delegator].expiry_data = shift(active_delegations + 1, 128) + next_expiry + + log ExtendBoost(delegator, receiver, _token_id, convert(y, uint256), expire_time, _cancel_time) + + +@external +def cancel_boost(_token_id: uint256): + """ + @notice Cancel an outstanding boost + @dev This does not burn the token, only the boost it represents. The owner + of the token or their operator can cancel a boost at any time. The + delegator or their operator can only cancel a token after the cancel + time. Anyone can cancel the boost if the value of it is negative. + @param _token_id The token to cancel + """ + self._cancel_boost(_token_id, msg.sender) + + +@external +def batch_cancel_boosts(_token_ids: uint256[256]): + """ + @notice Cancel many outstanding boosts + @dev This does not burn the token, only the boost it represents. The owner + of the token or their operator can cancel a boost at any time. The + delegator or their operator can only cancel a token after the cancel + time. Anyone can cancel the boost if the value of it is negative. + @param _token_ids A list of 256 token ids to nullify. The list must + be padded with 0 values if less than 256 token ids are provided. + """ + + for _token_id in _token_ids: + if _token_id == 0: + break + self._cancel_boost(_token_id, msg.sender) + + +@external +def set_delegation_status(_receiver: address, _delegator: address, _status: bool): + """ + @notice Set or reaffirm the blacklist/whitelist status of a delegator for a receiver. + @dev Setting delegator as the ZERO_ADDRESS enables users to deactivate delegations globally + and enable the white list. The ability of a delegator to delegate to a receiver + is determined by ~(grey_list[_receiver][ZERO_ADDRESS] ^ grey_list[_receiver][_delegator]). + @param _receiver The account which we will be updating it's list + @param _delegator The account to disallow/allow delegations from + @param _status Boolean of the status to set the _delegator account to + """ + assert msg.sender == _receiver or self.isApprovedForAll[_receiver][msg.sender] + self._set_delegation_status(_receiver, _delegator, _status) + + +@external +def batch_set_delegation_status(_receiver: address, _delegators: address[256], _status: uint256[256]): + """ + @notice Set or reaffirm the blacklist/whitelist status of multiple delegators for a receiver. + @dev Setting delegator as the ZERO_ADDRESS enables users to deactivate delegations globally + and enable the white list. The ability of a delegator to delegate to a receiver + is determined by ~(grey_list[_receiver][ZERO_ADDRESS] ^ grey_list[_receiver][_delegator]). + @param _receiver The account which we will be updating it's list + @param _delegators List of 256 accounts to disallow/allow delegations from + @param _status List of 256 0s and 1s (booleans) of the status to set the _delegator_i account to. + if the value is not 0 or 1, execution will break, effectively stopping at the index. + + """ + assert msg.sender == _receiver or self.isApprovedForAll[_receiver][msg.sender] # dev: only receiver or operator + + for i in range(256): + if _status[i] > 1: + break + self._set_delegation_status(_receiver, _delegators[i], convert(_status[i], bool)) + + +@view +@external +def adjusted_balance_of(_account: address) -> uint256: + """ + @notice Adjusted veCRV balance after accounting for delegations and boosts + @dev If boosts/delegations have a negative value, they're effective value is 0 + @param _account The account to query the adjusted balance of + """ + next_expiry: uint256 = self.boost[_account].expiry_data % 2 ** 128 + if next_expiry != 0 and next_expiry < block.timestamp: + # if the account has a negative boost in circulation + # we over penalize by setting their adjusted balance to 0 + # this is because we don't want to iterate to find the real + # value + return 0 + + adjusted_balance: int256 = VotingEscrow(self.voting_escrow).balanceOf(_account) + + boost: Boost = self.boost[_account] + time: int256 = convert(block.timestamp, int256) + + if boost.delegated != 0: + dpoint: Point = self._deconstruct_bias_slope(boost.delegated) + + # we take the absolute value, since delegated boost can be negative + # if any outstanding negative boosts are in circulation + # this can inflate the vecrv balance of a user + # taking the absolute value has the effect that it costs + # a user to negatively impact another's vecrv balance + adjusted_balance -= abs(dpoint.slope * time + dpoint.bias) + + if boost.received != 0: + rpoint: Point = self._deconstruct_bias_slope(boost.received) + + # similar to delegated boost, our received boost can be negative + # if any outstanding negative boosts are in our possession + # However, unlike delegated boost, we do not negatively impact + # our adjusted balance due to negative boosts. Instead we take + # whichever is greater between 0 and the value of our received + # boosts. + adjusted_balance += max(rpoint.slope * time + rpoint.bias, empty(int256)) + + # since we took the absolute value of our delegated boost, it now instead of + # becoming negative is positive, and will continue to increase ... + # meaning if we keep a negative outstanding delegated balance for long + # enought it will not only decrease our vecrv_balance but also our received + # boost, however we return the maximum between our adjusted balance and 0 + # when delegating boost, received boost isn't used for determining how + # much we can delegate. + return convert(max(adjusted_balance, empty(int256)), uint256) + + +@view +@external +def delegated_boost(_account: address) -> uint256: + """ + @notice Query the total effective delegated boost value of an account. + @dev This value can be greater than the veCRV balance of + an account if the account has outstanding negative + value boosts. + @param _account The account to query + """ + dpoint: Point = self._deconstruct_bias_slope(self.boost[_account].delegated) + time: int256 = convert(block.timestamp, int256) + return convert(abs(dpoint.slope * time + dpoint.bias), uint256) + + +@view +@external +def received_boost(_account: address) -> uint256: + """ + @notice Query the total effective received boost value of an account + @dev This value can be 0, even with delegations which have a large value, + if the account has any outstanding negative value boosts. + @param _account The account to query + """ + rpoint: Point = self._deconstruct_bias_slope(self.boost[_account].received) + time: int256 = convert(block.timestamp, int256) + return convert(max(rpoint.slope * time + rpoint.bias, empty(int256)), uint256) + + +@view +@external +def token_boost(_token_id: uint256) -> int256: + """ + @notice Query the effective value of a boost + @dev The effective value of a boost is negative after it's expiration + date. + @param _token_id The token id to query + """ + tpoint: Point = self._deconstruct_bias_slope(self.boost_tokens[_token_id].data) + time: int256 = convert(block.timestamp, int256) + return tpoint.slope * time + tpoint.bias + + +@view +@external +def token_expiry(_token_id: uint256) -> uint256: + """ + @notice Query the timestamp of a boost token's expiry + @dev The effective value of a boost is negative after it's expiration + date. + @param _token_id The token id to query + """ + return self.boost_tokens[_token_id].expire_time + + +@view +@external +def token_cancel_time(_token_id: uint256) -> uint256: + """ + @notice Query the timestamp of a boost token's cancel time. This is + the point at which the delegator can nullify the boost. A receiver + can cancel a token at any point. Anyone can nullify a token's boost + after it's expiration. + @param _token_id The token id to query + """ + return self.boost_tokens[_token_id].dinfo % 2 ** 128 + + +@view +@external +def calc_boost_bias_slope( + _delegator: address, + _percentage: int256, + _expire_time: int256, + _extend_token_id: uint256 = 0 +) -> (int256, int256): + """ + @notice Calculate the bias and slope for a boost. + @param _delegator The account to delegate boost from + @param _percentage The percentage of the _delegator's delegable + veCRV to delegate. + @param _expire_time The time at which the boost value of the token + will reach 0, and subsequently become negative + @param _extend_token_id OPTIONAL token id, which if set will first nullify + the boost of the token, before calculating the bias and slope. Useful + for calculating the new bias and slope when extending a token, or + determining the bias and slope of a subsequent token after cancelling + an existing one. Will have no effect if _delegator is not the delegator + of the token. + """ + time: int256 = convert(block.timestamp, int256) + assert _percentage > 0 # dev: percentage must be greater than 0 + assert _percentage <= MAX_PCT # dev: percentage must be less than or equal to 100% + assert _expire_time > time + WEEK # dev: Invalid min expiry time + + lock_expiry: int256 = convert(VotingEscrow(self.voting_escrow).locked__end(_delegator), int256) + assert _expire_time <= lock_expiry + + ddata: uint256 = self.boost[_delegator].delegated + + if _extend_token_id != 0 and convert(shift(_extend_token_id, -96), address) == _delegator: + # decrease the delegated bias and slope by the token's bias and slope + # only if it is the delegator's and it is within the bounds of existence + ddata -= self.boost_tokens[_extend_token_id].data + + dpoint: Point = self._deconstruct_bias_slope(ddata) + + delegated_boost: int256 = dpoint.slope * time + dpoint.bias + assert delegated_boost >= 0 # dev: outstanding negative boosts + + y: int256 = _percentage * (VotingEscrow(self.voting_escrow).balanceOf(_delegator) - delegated_boost) / MAX_PCT + assert y > 0 # dev: no boost + + slope: int256 = -y / (_expire_time - time) + assert slope < 0 # dev: invalid slope + + bias: int256 = y - slope * time + + return bias, slope + + +@pure +@external +def get_token_id(_delegator: address, _id: uint256) -> uint256: + """ + @notice Simple method to get the token id's mintable by a delegator + @param _delegator The address of the delegator + @param _id The id value, must be less than 2 ** 96 + """ + assert _id < 2 ** 96 # dev: invalid _id + return shift(convert(_delegator, uint256), 96) + _id + + +@external +def commit_transfer_ownership(_addr: address): + """ + @notice Transfer ownership of contract to `addr` + @param _addr Address to have ownership transferred to + """ + assert msg.sender == self.admin # dev: admin only + assert _addr != ZERO_ADDRESS # dev: future admin cannot be the 0 address + self.future_admin = _addr + +@external +def accept_transfer_ownership(): + """ + @notice Accept admin role, only callable by future admin + """ + future_admin: address = self.future_admin + assert msg.sender == future_admin + self.admin = future_admin + +@external +def set_base_uri(_base_uri: String[128]): + assert msg.sender == self.admin + self.base_uri = _base_uri + diff --git a/contracts/staking/veBoostProxy.vy b/contracts/staking/veBoostProxy.vy new file mode 100644 index 0000000..ab28a74 --- /dev/null +++ b/contracts/staking/veBoostProxy.vy @@ -0,0 +1,117 @@ +# @version 0.2.16 +""" +@title Voting Escrow Delegation Proxy +@author Angle Protocol +@license MIT +""" + +# Full fork from: +# Curve Finance's veBoostProxy + +from vyper.interfaces import ERC20 + +interface VeDelegation: + def adjusted_balance_of(_account: address) -> uint256: view + + +event CommitAdmin: + admin: address + +event ApplyAdmin: + admin: address + +event DelegationSet: + delegation: address + + +voting_escrow: public(address) + + +delegation: public(address) + +admin: public(address) +future_admin: public(address) + + +@external +def __init__(_voting_escrow: address, _delegation: address, _admin: address): + + assert _voting_escrow != ZERO_ADDRESS + assert _admin != ZERO_ADDRESS + + self.voting_escrow = _voting_escrow + + self.delegation = _delegation + + self.admin = _admin + + log DelegationSet(_delegation) + + +@view +@external +def adjusted_balance_of(_account: address) -> uint256: + """ + @notice Get the adjusted veCRV balance from the active boost delegation contract + @param _account The account to query the adjusted veCRV balance of + @return veCRV balance + """ + _delegation: address = self.delegation + if _delegation == ZERO_ADDRESS: + return ERC20(self.voting_escrow).balanceOf(_account) + return VeDelegation(_delegation).adjusted_balance_of(_account) + + +@external +def kill_delegation(): + """ + @notice Set delegation contract to 0x00, disabling boost delegation + @dev Callable by the emergency admin in case of an issue with the delegation logic + """ + assert msg.sender == self.admin + + self.delegation = ZERO_ADDRESS + log DelegationSet(ZERO_ADDRESS) + + +@external +def set_delegation(_delegation: address): + """ + @notice Set the delegation contract + @dev Only callable by the ownership admin + @param _delegation `VotingEscrowDelegation` deployment address + """ + assert msg.sender == self.admin + + # call `adjusted_balance_of` to make sure it works + VeDelegation(_delegation).adjusted_balance_of(msg.sender) + + self.delegation = _delegation + log DelegationSet(_delegation) + + +@external +def commit_admin(_admin: address): + """ + @notice Set admin to `_admin` + @param _admin Ownership admin + """ + assert msg.sender == self.admin, "Access denied" + + self.future_admin = _admin + + log CommitAdmin(_admin) + + +@external +def accept_transfer_ownership(): + """ + @notice Accept a pending ownership transfer + """ + _admin: address = self.future_admin + assert msg.sender == _admin # dev: future admin only + + self.admin = _admin + + log ApplyAdmin(_admin) + \ No newline at end of file diff --git a/contracts/utils/VyperDeployer.sol b/contracts/utils/VyperDeployer.sol new file mode 100644 index 0000000..0650a23 --- /dev/null +++ b/contracts/utils/VyperDeployer.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.7; + +///@notice This cheat codes interface is named _CheatCodes so you can use the CheatCodes interface in other testing files without errors +interface _CheatCodes { + function ffi(string[] calldata) external returns (bytes memory); +} + +contract VyperDeployer { + address constant HEVM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); + + /// @notice Initializes cheat codes in order to use ffi to compile Vyper contracts + _CheatCodes cheatCodes = _CheatCodes(HEVM_ADDRESS); + + ///@notice Compiles a Vyper contract and returns the address that the contract was deployeod to + ///@notice If deployment fails, an error will be thrown + ///@param fileName - The file name of the Vyper contract. For example, the file name for "SimpleStore.vy" is "SimpleStore" + ///@return deployedAddress - The address that the contract was deployed to + + function deployContract(string memory fileName) public returns (address) { + ///@notice create a list of strings with the commands necessary to compile Vyper contracts + string[] memory cmds = new string[](2); + cmds[0] = "vyper"; + cmds[1] = string.concat("vyper_contracts/", fileName, ".vy"); + + ///@notice compile the Vyper contract and return the bytecode + bytes memory bytecode = cheatCodes.ffi(cmds); + + ///@notice deploy the bytecode with the create instruction + address deployedAddress; + assembly { + deployedAddress := create(0, add(bytecode, 0x20), mload(bytecode)) + } + + ///@notice check that the deployment was successful + require(deployedAddress != address(0), "VyperDeployer could not deploy contract"); + + ///@notice return the address that the contract was deployed to + return deployedAddress; + } + + ///@notice Compiles a Vyper contract with constructor arguments and returns the address that the contract was deployeod to + ///@notice If deployment fails, an error will be thrown + ///@param fileName - The file name of the Vyper contract. For example, the file name for "SimpleStore.vy" is "SimpleStore" + ///@return deployedAddress - The address that the contract was deployed to + function deployContract(string memory fileName, bytes calldata args) public returns (address) { + ///@notice create a list of strings with the commands necessary to compile Vyper contracts + string[] memory cmds = new string[](2); + cmds[0] = "vyper"; + cmds[1] = string.concat("vyper_contracts/", fileName, ".vy"); + + ///@notice compile the Vyper contract and return the bytecode + bytes memory _bytecode = cheatCodes.ffi(cmds); + + //add args to the deployment bytecode + bytes memory bytecode = abi.encodePacked(_bytecode, args); + + ///@notice deploy the bytecode with the create instruction + address deployedAddress; + assembly { + deployedAddress := create(0, add(bytecode, 0x20), mload(bytecode)) + } + + ///@notice check that the deployment was successful + require(deployedAddress != address(0), "VyperDeployer could not deploy contract"); + + ///@notice return the address that the contract was deployed to + return deployedAddress; + } + + /// @dev Consider listening to the Blueprint if you haven't already + /// @param fileName - The file name of the Blueprint Contract + function deployBlueprint(string memory fileName) public returns (address) { + ///@notice create a list of strings with the commands necessary to compile Vyper contracts + string[] memory cmds = new string[](2); + cmds[0] = "vyper"; + cmds[1] = string.concat("vyper_contracts/", fileName, ".vy"); + + ///@notice compile the Vyper contract and return the bytecode + bytes memory bytecode = cheatCodes.ffi(cmds); + + require(bytecode.length > 0, "Initcodes length must be greater than 0"); + + /// @notice prepend needed items for Blueprint ERC + /// See https://eips.ethereum.org/EIPS/eip-5202 for more details + bytes memory eip_5202_bytecode = bytes.concat( + hex"fe", // EIP_5202_EXECUTION_HALT_BYTE + hex"71", // EIP_5202_BLUEPRINT_IDENTIFIER_BYTE + hex"00", // EIP_5202_VERSION_BYTE + bytecode + ); + + bytes2 len = bytes2(uint16(eip_5202_bytecode.length)); + + /// @notice prepend the deploy preamble + bytes memory deployBytecode = bytes.concat( + hex"61", // DEPLOY_PREAMBLE_INITIAL_BYTE + len, // DEPLOY_PREAMBLE_BYTE_LENGTH + hex"3d81600a3d39f3", // DEPLOY_PREABLE_POST_LENGTH_BYTES + eip_5202_bytecode + ); + + ///@notice check that the deployment was successful + address deployedAddress; + assembly { + deployedAddress := create(0, add(deployBytecode, 0x20), mload(deployBytecode)) + } + + require(deployedAddress != address(0), "VyperDeployer could not deploy contract"); + + ///@notice return the address that the contract was deployed to + return deployedAddress; + } +} diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..e314232 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,73 @@ +[profile.default] +src = "contracts" +out = "out" +test = "test" +libs = ["lib"] +script = "scripts" +cache_path = "cache" +gas_reports = ["*"] +via_ir = true +sizes = true +optimizer = true +optimizer_runs = 1000 +solc_version = "0.8.7" +ffi = true + +[fuzz] +runs = 10000 + +[invariant] +runs = 1000 +depth = 30 + +[rpc_endpoints] +arbitrum = "${ETH_NODE_URI_ARBITRUM}" +gnosis = "${ETH_NODE_URI_GNOSIS}" +mainnet = "${ETH_NODE_URI_MAINNET}" +optimism = "${ETH_NODE_URI_OPTIMISM}" +polygon = "${ETH_NODE_URI_POLYGON}" +fork = "${ETH_NODE_URI_FORK}" +avalanche = "${ETH_NODE_URI_AVALANCHE}" +celo = "${ETH_NODE_URI_CELO}" +polygonzkevm = "${ETH_NODE_URI_POLYGONZKEVM}" +bsc = "${ETH_NODE_URI_BSC}" +base = "${ETH_NODE_URI_BASE}" + +[etherscan] +arbitrum = { key = "${ARBITRUM_ETHERSCAN_API_KEY}" } +gnosis = { key = "${GNOSIS_ETHERSCAN_API_KEY}" , url = "https://api.gnosisscan.io/api"} +mainnet = { key = "${MAINNET_ETHERSCAN_API_KEY}" } +optimism = { key = "${OPTIMISM_ETHERSCAN_API_KEY}" } +polygon = { key = "${POLYGON_ETHERSCAN_API_KEY}" } +avalanche = { key = "${AVALANCHE_ETHERSCAN_API_KEY}" } +celo = { key = "${CELO_ETHERSCAN_API_KEY}", url = "https://api.celoscan.io/api" } +base = { key = "${BASE_ETHERSCAN_API_KEY}", url = "https://api.basescan.org/api" } +polygonzkevm = { key = "${POLYGONZKEVM_ETHERSCAN_API_KEY}", url = "https://api-zkevm.polygonscan.com/api" } +bsc = { key = "${BSC_ETHERSCAN_API_KEY}"} + +[profile.dev] +optimizer = true +via_ir = false +src = "test" +gas_reports = ["*"] + +[profile.dev.fuzz] +runs = 2000 + +[profile.dev.invariant] +runs = 10 +depth = 1 +fail_on_revert = false + +[profile.ci] +src = "test" +via_ir = false +gas_reports = ["*"] + +[profile.ci.fuzz] +runs = 100 + +[profile.ci.invariant] +runs = 10 +depth = 30 +fail_on_revert = false diff --git a/helpers/fork.sh b/helpers/fork.sh new file mode 100644 index 0000000..ea784a1 --- /dev/null +++ b/helpers/fork.sh @@ -0,0 +1,37 @@ +#! /bin/bash + +source lib/utils/helpers/common.sh + +function main { + if [ ! -f .env ]; then + echo ".env not found!" + exit 1 + fi + source .env + + echo "Which chain would you like to fork ?" + echo "- 1: Ethereum Mainnet" + echo "- 2: Arbitrum" + echo "- 3: Polygon" + echo "- 4: Gnosis" + echo "- 5: Avalanche" + echo "- 6: Base" + echo "- 7: Binance Smart Chain" + echo "- 8: Celo" + echo "- 9: Polygon ZkEvm" + echo "- 10: Optimism" + echo "- 11: Linea" + + read option + + uri=$(chain_to_uri $option) + if [ -z "$uri" ]; then + echo "Unknown network" + exit 1 + fi + + echo "Forking $uri" + anvil --fork-url $uri +} + +main diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..b6a506d --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit b6a506db2262cad5ff982a87789ee6d1558ec861 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..8d7a871 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 8d7a871609117b2d95074f5f5e92e4c0506584b7 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..ebf264c --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit ebf264cc4b812e6557ed7d539e947211acd5670c diff --git a/lib/utils b/lib/utils new file mode 160000 index 0000000..03a279e --- /dev/null +++ b/lib/utils @@ -0,0 +1 @@ +Subproject commit 03a279efc8569b5b6f1a2925effeb5e966a02f03 diff --git a/package.json b/package.json new file mode 100644 index 0000000..4b559c4 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "angle-boilerplate", + "version": "1.0.0", + "description": "", + "scripts": { + "ci:coverage": "forge coverage --report lcov && yarn lcov:clean", + "coverage": "FOUNDRY_PROFILE=dev forge coverage --report lcov && yarn lcov:clean && yarn lcov:generate-html", + "compile": "forge build", + "compile:dev": "FOUNDRY_PROFILE=dev forge build", + "deploy": "forge script --skip test --broadcast --verify --slow -vvvv --rpc-url mainnet scripts/DeployMockAgEUR.s.sol", + "deploy:fork": "source .env && forge script --skip test --slow --fork-url fork --broadcast scripts/DeployMockAgEUR.s.sol -vvvv", + "gas": "yarn test --gas-report", + "fork": "bash helpers/fork.sh", + "run": "docker run -it --rm -v $(pwd):/app -w /app ghcr.io/foundry-rs/foundry sh", + "script:fork": "source .env && forge script --skip test --fork-url fork --broadcast -vvvv", + "test:unit": "forge test -vvv --gas-report --match-path \"test/unit/**/*.sol\"", + "test:invariant": "forge test -vvv --gas-report --match-path \"test/invariant/**/*.sol\"", + "test:fuzz": "forge test -vvv --gas-report --match-path \"test/fuzz/**/*.sol\"", + "test": "FOUNDRY_PROFILE=dev forge test -vvv", + "slither": "slither .", + "lcov:clean": "lcov --remove lcov.info -o lcov.info 'test/**' 'scripts/**' 'contracts/transmuter/configs/**' 'contracts/utils/**'", + "lcov:generate-html": "genhtml lcov.info --output=coverage", + "size": "forge build --skip test --sizes", + "size:dev": "FOUNDRY_PROFILE=dev forge build --skip test --sizes", + "prettier": "prettier --write '**/*.sol'", + "lint": "yarn lint:check --fix", + "lint:check": "solhint --max-warnings 20 \"**/*.sol\"" + }, + "keywords": [], + "author": "Angle Core Team", + "license": "GPL-3.0", + "bugs": { + "url": "https://github.com/AngleProtocol/boilerplate/issues" + }, + "devDependencies": { + "prettier": "^2.0.0", + "prettier-plugin-solidity": "^1.1.3", + "solhint": "^3.5.1", + "solhint-plugin-prettier": "^0.0.5" + }, + "dependencies": {} +} diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..20ce6b6 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,5 @@ +ds-test/=lib/forge-std/lib/ds-test/src/ +forge-std/=lib/forge-std/src/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts +utils/=lib/utils/ \ No newline at end of file diff --git a/scripts/BasicScript.s.sol b/scripts/BasicScript.s.sol new file mode 100644 index 0000000..8c1e77d --- /dev/null +++ b/scripts/BasicScript.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import "forge-std/Script.sol"; +import { console } from "forge-std/console.sol"; + +contract MyScript is Script { + function test() external { + vm.startBroadcast(); + + address _sender = address(uint160(uint256(keccak256(abi.encodePacked("sender"))))); + address _receiver = address(uint160(uint256(keccak256(abi.encodePacked("receiver"))))); + + // deal(address(token), _sender, 1 ether); + // vm.prank(_sender); + // token.transfer(_receiver, 1 ether); + + vm.stopBroadcast(); + } +} diff --git a/scripts/Simulate.s.sol b/scripts/Simulate.s.sol new file mode 100644 index 0000000..d922378 --- /dev/null +++ b/scripts/Simulate.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import "forge-std/Script.sol"; +import { console } from "forge-std/console.sol"; + +contract Simulate is Script { + error WrongCall(); + + function run() external { + // TODO replace with your inputs + address sender = address(0x0274a704a6D9129F90A62dDC6f6024b33EcDad36); + address contractAddress = address(0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae); + // remove the 0x + bytes + memory data = hex"71ee95c0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000274a704a6d9129f90a62ddc6f6024b33ecdad3600000000000000000000000000000000000000000000000000000000000000010000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f984000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000427abf85e5d121ac000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000811e1782af6373843046497b3be2c5b25f13037b02c218c5a1a5be24666434d169ca949b7c3d8c763a566075f024a19b4565aba390e00f197fff97adb2f9ef8b0eee43dfe473f564bbd71b106e37928d4f052afe2abf8a42e5f07aded57af2766943bc7bedecf531200d4ca454185d135e6933aa3cff5c53a55f45033135d3a01aae71a2ff3d1e34342bdb86d6348a9da5c8384085f743bb2451aa846b5f667690ccb7b7b1fe363d3e286addaaff93de4a258308fae35fb008580bc25873284f63b37592378acc9f27d211017550c57ae97d0e9cc944d3f90bf54147a69fedeed81940d2088ad7b84767ae8569a9202e23aa0f8204a30b365916d5cefb60c1dfe"; + + vm.prank(sender); + (bool success, ) = contractAddress.call(data); + if (!success) revert WrongCall(); + } +} diff --git a/scripts/Utils.s.sol b/scripts/Utils.s.sol new file mode 100644 index 0000000..e377053 --- /dev/null +++ b/scripts/Utils.s.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import "utils/src/CommonUtils.sol"; +import { console } from "forge-std/console.sol"; +import "forge-std/Script.sol"; + +contract Utils is Script, CommonUtils {} diff --git a/slither.config.json b/slither.config.json new file mode 100644 index 0000000..81e2b31 --- /dev/null +++ b/slither.config.json @@ -0,0 +1,10 @@ +{ + "detectors_to_exclude": "naming-convention,solc-version", + "filter_paths": "(lib|test|external|scripts)", + "solc_remaps": [ + "ds-test/=lib/ds-test/src/", + "forge-std/=lib/forge-std/src/", + "oz/=lib/openzeppelin-contracts/contracts/", + "oz-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/" + ] +} diff --git a/test/fuzz/.gitkeep b/test/fuzz/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/invariant/.gitkeep b/test/invariant/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/.gitkeep b/test/unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/utils/forwardUtils.js b/utils/forwardUtils.js new file mode 100644 index 0000000..237c7e0 --- /dev/null +++ b/utils/forwardUtils.js @@ -0,0 +1,22 @@ +const { exec } = require("child_process"); + +if (process.argv.length < 3) { + console.error('Please provide a chain input as an argument.'); + process.exit(1); +} + +const command = process.argv[2]; +const extraArgs = process.argv.slice(3).join(' '); + + +exec(`node lib/utils/utils/${command}.js ${extraArgs}`, (error, stdout, stderr) => { + if (error) { + console.log(error); + return; + } + if (stderr) { + console.log(stderr); + return; + } + console.log(stdout); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..54f44d1 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,505 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@solidity-parser/parser@^0.16.0": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.1.tgz#f7c8a686974e1536da0105466c4db6727311253c" + integrity sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw== + dependencies: + antlr4ts "^0.5.0-alpha.4" + +ajv@^6.12.6: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.1: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +antlr4@^4.11.0: + version "4.13.1" + resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.1.tgz#1e0a1830a08faeb86217cb2e6c34716004e4253d" + integrity sha512-kiXTspaRYvnIArgE97z5YVVf/cDVQABr3abFRR6mE7yesLMkgu4ujuyV/sgxafQ8wgve0DJQUJ38Z8tkgA2izA== + +antlr4ts@^0.5.0-alpha.4: + version "0.5.0-alpha.4" + resolved "https://registry.yarnpkg.com/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz#71702865a87478ed0b40c0709f422cf14d51652a" + integrity sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +ast-parents@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/ast-parents/-/ast-parents-0.0.1.tgz#508fd0f05d0c48775d9eccda2e174423261e8dd3" + integrity sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +cosmiconfig@^8.0.0: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2, fast-diff@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +glob@^8.0.3: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +ignore@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier-plugin-solidity@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.3.tgz#9a35124f578404caf617634a8cab80862d726cba" + integrity sha512-fQ9yucPi2sBbA2U2Xjh6m4isUTJ7S7QLc/XDDsktqqxYfTwdYKJ0EnnywXHwCGAaYbQNK+HIYPL1OemxuMsgeg== + dependencies: + "@solidity-parser/parser" "^0.16.0" + semver "^7.3.8" + solidity-comments-extractor "^0.0.7" + +prettier@^2.0.0, prettier@^2.8.3: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +semver@^7.3.8, semver@^7.5.2: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +solhint-plugin-prettier@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/solhint-plugin-prettier/-/solhint-plugin-prettier-0.0.5.tgz#e3b22800ba435cd640a9eca805a7f8bc3e3e6a6b" + integrity sha512-7jmWcnVshIrO2FFinIvDQmhQpfpS2rRRn3RejiYgnjIE68xO2bvrYvjqVNfrio4xH9ghOqn83tKuTzLjEbmGIA== + dependencies: + prettier-linter-helpers "^1.0.0" + +solhint@^3.5.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-3.6.2.tgz#2b2acbec8fdc37b2c68206a71ba89c7f519943fe" + integrity sha512-85EeLbmkcPwD+3JR7aEMKsVC9YrRSxd4qkXuMzrlf7+z2Eqdfm1wHWq1ffTuo5aDhoZxp2I9yF3QkxZOxOL7aQ== + dependencies: + "@solidity-parser/parser" "^0.16.0" + ajv "^6.12.6" + antlr4 "^4.11.0" + ast-parents "^0.0.1" + chalk "^4.1.2" + commander "^10.0.0" + cosmiconfig "^8.0.0" + fast-diff "^1.2.0" + glob "^8.0.3" + ignore "^5.2.4" + js-yaml "^4.1.0" + lodash "^4.17.21" + pluralize "^8.0.0" + semver "^7.5.2" + strip-ansi "^6.0.1" + table "^6.8.1" + text-table "^0.2.0" + optionalDependencies: + prettier "^2.8.3" + +solidity-comments-extractor@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz#99d8f1361438f84019795d928b931f4e5c39ca19" + integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw== + +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" + +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" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +table@^6.8.1: + version "6.8.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" + integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==