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 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==