├── .env.example ├── .gitattributes ├── .github └── workflows │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .mocharc.js ├── .prettierignore ├── .prettierrc ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── BUG_BOUNTY.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── deploy ├── 001_stateful_chainlink_oracle.ts ├── 002_uniswap_v3_adapter.ts ├── 003_identity_oracle.ts ├── 004_oracle_aggregator.ts ├── 005_transformer_oracle.ts ├── 006_api3_chainlink_adapter_factory.ts └── 007_dia_chainlink_adapter_factory.ts ├── hardhat.config.ts ├── package.json ├── scripts └── verify-contracts.ts ├── slither.config.json ├── solidity ├── contracts │ ├── IdentityOracle.sol │ ├── OracleAggregator.sol │ ├── StatefulChainlinkOracle.sol │ ├── TransformerOracle.sol │ ├── adapters │ │ ├── UniswapV3Adapter.sol │ │ ├── api3-chainlink-adapter │ │ │ ├── API3ChainlinkAdapter.sol │ │ │ └── API3ChainlinkAdapterFactory.sol │ │ └── dia-chainlink-adapter │ │ │ ├── DIAChainlinkAdapter.sol │ │ │ └── DIAChainlinkAdapterFactory.sol │ ├── base │ │ ├── BaseOracle.sol │ │ └── SimpleOracle.sol │ ├── libraries │ │ └── TokenSorting.sol │ └── test │ │ ├── OracleAggregator.sol │ │ ├── StatefulChainlinkOracle.sol │ │ ├── TransformerOracle.sol │ │ ├── UniswapV3Pool.sol │ │ ├── adapters │ │ └── UniswapV3Adapter.sol │ │ └── base │ │ └── SimpleOracle.sol └── interfaces │ ├── IOracleAggregator.sol │ ├── IStatefulChainlinkOracle.sol │ ├── ITokenPriceOracle.sol │ ├── ITransformerOracle.sol │ └── adapters │ └── IUniswapV3Adapter.sol ├── tasks └── npm-publish-clean-typechain.ts ├── test ├── e2e │ └── oracle-aggregator.spec.ts ├── integration │ ├── api3_chainlink_adapter.spec.ts │ ├── comprehensive-oracle-test.spec.ts │ ├── dia_chainlink_adapter.spec.ts │ ├── stateful-chainlink-oracle.spec.ts │ ├── transformer-oracle.spec.ts │ └── uniswap-v3-add-support-gas.spec.ts ├── unit │ ├── adapters │ │ ├── api3-chainlink-adapter │ │ │ ├── api3-chainlink-adapter-factory.spec.ts │ │ │ └── api3-chainlink-adapter.spec.ts │ │ ├── dia-chainlink-adapter │ │ │ ├── dia-chainlink-adapter-factory.spec.ts │ │ │ └── dia-chainlink-adapter.spec.ts │ │ └── uniswap-v3-adapter.spec.ts │ ├── base │ │ └── simple-oracle.spec.ts │ ├── identity-oracle.spec.ts │ ├── oracle-aggregator.spec.ts │ ├── stateful-chainlink-oracle.spec.ts │ └── transformer-oracle.spec.ts └── utils │ ├── bdd.ts │ ├── behaviours.ts │ ├── bn.ts │ ├── contracts.ts │ ├── defillama.ts │ ├── erc165.ts │ ├── event-utils.ts │ ├── evm.ts │ ├── index.ts │ ├── uniswap.ts │ └── wallet.ts ├── tsconfig.json ├── tsconfig.publish.json ├── utils ├── deploy.ts └── env.ts ├── workspace.code-workspace └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # Enable imports 2 | TS_NODE_SKIP_IGNORE=true 3 | 4 | # network specific node uri: `"NODE_URI_" + networkName.toUpperCase()` 5 | NODE_URI_ETHEREUM=https://eth-mainnet.alchemyapi.io/v2/ 6 | 7 | # network specific account type: `${networkName.toUpperCase()}_ACCOUNTS_TYPE`, can either be MNEMONIC, or PRIVATE_KEYS. 8 | ETHEREUM_ACCOUNTS_TYPE=PRIVATE_KEYS 9 | 10 | # network specific mnemonic: `${networkName.toUpperCase()}_MNEMONIC` 11 | ETHEREUM_MNEMONIC= 12 | 13 | # network specific private keys: `${networkName.toUpperCase()}_{INDEX_OF_ACCOUNT}_PRIVATE_KEY` 14 | ETHEREUM_1_PRIVATE_KEY= 15 | 16 | # Mocha (10 minutes) 17 | MOCHA_TIMEOUT=600000 18 | 19 | # Coinmarketcap (optional, only for gas reporting) 20 | COINMARKETCAP_API_KEY= 21 | COINMARKETCAP_DEFAULT_CURRENCY=USD 22 | 23 | # Etherscan (optional, only for verifying smart contracts) 24 | # network specific api key: `${networkName.toUpperCase()}_{INDEX_OF_ACCOUNT}_PRIVATE_KEY` 25 | ETHEREUM_ETHERSCAN_API_KEY= 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | files: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Check out github repository 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 1 14 | 15 | - name: Cache node modules 16 | uses: actions/cache@v3 17 | env: 18 | cache-name: cache-node-modules 19 | with: 20 | path: "**/node_modules" 21 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} 22 | 23 | - name: Install node 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: "18.x" 27 | 28 | - name: Install dependencies 29 | run: yarn --frozen-lockfile 30 | 31 | - name: Run linter 32 | run: yarn run lint:check 33 | 34 | commits: 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - name: Check out github repository 39 | uses: actions/checkout@v2 40 | with: 41 | fetch-depth: 0 42 | 43 | - name: Run commitlint 44 | uses: wagoid/commitlint-github-action@v2 45 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | unit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out github repository 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 1 17 | 18 | - name: Cache node modules 19 | uses: actions/cache@v3 20 | env: 21 | cache-name: cache-node-modules 22 | with: 23 | path: "**/node_modules" 24 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} 25 | 26 | - name: Install node 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: "18.x" 30 | 31 | - name: Install dependencies 32 | run: yarn --frozen-lockfile 33 | 34 | - name: Run unit tests 35 | run: yarn test:unit 36 | env: 37 | TS_NODE_SKIP_IGNORE: true 38 | e2e: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Check out github repository 42 | uses: actions/checkout@v2 43 | with: 44 | fetch-depth: 1 45 | 46 | - name: Cache node modules 47 | uses: actions/cache@v3 48 | env: 49 | cache-name: cache-node-modules 50 | with: 51 | path: "**/node_modules" 52 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} 53 | 54 | - name: Install node 55 | uses: actions/setup-node@v1 56 | with: 57 | node-version: "18.x" 58 | 59 | - name: Install dependencies 60 | run: yarn --frozen-lockfile 61 | 62 | - name: Run e2e tests 63 | run: yarn test:e2e 64 | env: 65 | TS_NODE_SKIP_IGNORE: true 66 | integration: 67 | needs: ["unit", "e2e"] 68 | runs-on: ubuntu-latest 69 | steps: 70 | - name: Check out github repository 71 | uses: actions/checkout@v2 72 | with: 73 | fetch-depth: 1 74 | 75 | - name: Cache node modules 76 | uses: actions/cache@v3 77 | env: 78 | cache-name: cache-node-modules 79 | with: 80 | path: "**/node_modules" 81 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} 82 | 83 | - name: Install node 84 | uses: actions/setup-node@v1 85 | with: 86 | node-version: "18.x" 87 | 88 | - name: Install dependencies 89 | run: yarn --frozen-lockfile 90 | 91 | - name: Run integration tests 92 | run: yarn test:integration 93 | env: 94 | TS_NODE_SKIP_IGNORE: true 95 | NODE_URI_ETHEREUM: https://eth-mainnet.alchemyapi.io/v2/${{ secrets.ALCHEMYKEY }} 96 | NODE_URI_OPTIMISM: https://opt-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMYKEY }} 97 | NODE_URI_ARBITRUM: https://arb-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMYKEY }} 98 | NODE_URI_POLYGON: https://polygon-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMYKEY }} 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | dist 3 | node_modules 4 | .DS_STORE 5 | 6 | # Coverage 7 | coverage.json 8 | coverage 9 | 10 | # ETH-SDK 11 | eth-sdk/abis 12 | 13 | # Hardhat files 14 | cache 15 | artifacts 16 | typechained 17 | deployments/hardhat 18 | deployments/localhost 19 | 20 | # Config files 21 | .env 22 | 23 | # Not ignore gitkeep 24 | !/**/.gitkeep 25 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | dotenv.config(); 3 | 4 | module.exports = { 5 | require: ['hardhat/register'], 6 | extension: ['.ts'], 7 | ignore: ['./test/utils/**'], 8 | recursive: true, 9 | timeout: process.env.MOCHA_TIMEOUT || 300000, 10 | }; 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # General 2 | dist 3 | .prettierignore 4 | .solhintignore 5 | .husky 6 | .gitignore 7 | .gitattributes 8 | .env.example 9 | .env 10 | workspace.code-workspace 11 | .DS_STORE 12 | LICENSE 13 | codechecks.yml 14 | 15 | # Hardhat 16 | coverage 17 | coverage.json 18 | artifacts 19 | cache 20 | typechained 21 | deployments 22 | 23 | # JS 24 | node_modules 25 | package-lock.json 26 | yarn.lock 27 | 28 | # Solidity 29 | contracts/mock 30 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": "**.sol", 5 | "options": { 6 | "printWidth": 145, 7 | "tabWidth": 2, 8 | "useTabs": false, 9 | "singleQuote": true, 10 | "bracketSpacing": false 11 | } 12 | }, 13 | { 14 | "files": ["**.ts", "**.js"], 15 | "options": { 16 | "printWidth": 145, 17 | "tabWidth": 2, 18 | "semi": true, 19 | "singleQuote": true, 20 | "useTabs": false, 21 | "endOfLine": "auto" 22 | } 23 | }, 24 | { 25 | "files": "**.json", 26 | "options": { 27 | "tabWidth": 2, 28 | "printWidth": 200 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: ['test', 'interfaces', 'external', 'mocks'], 3 | mocha: { 4 | forbidOnly: true, 5 | grep: '@skip-on-coverage', 6 | invert: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "warn", 6 | "compiler-version": ["off"], 7 | "constructor-syntax": "warn", 8 | "quotes": ["error", "single"], 9 | "func-visibility": ["warn", { "ignoreConstructors": true }], 10 | "not-rely-on-time": "off", 11 | "private-vars-leading-underscore": ["warn", { "strict": false }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | contracts/mock 3 | -------------------------------------------------------------------------------- /BUG_BOUNTY.md: -------------------------------------------------------------------------------- 1 | # Balmy Bug Bounty 2 | 3 | Balmy ImmuneFi bug bounty program is focused on the prevention of negative impacts to the Balmy ecosystem, which currently covers our smart contracts and integrations. As such, all bug disclosures must be done through ImmuneFi's platform. 4 | 5 | **Further details and bounty values can be found here**: https://immunefi.com/bounty/balmy/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Lint](https://github.com/Balmy-Protocol/oracles/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/Balmy-Protocol/oracles/actions/workflows/lint.yml) 2 | [![Tests](https://github.com/Balmy-Protocol/oracles/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/Balmy-Protocol/oracles/actions/workflows/tests.yml) 3 | [![Slither Analysis](https://github.com/Balmy-Protocol/oracles/actions/workflows/slither.yml/badge.svg?branch=main)](https://github.com/Balmy-Protocol/oracles/actions/workflows/slither.yml) 4 | 5 | # Balmy Oracles 6 | 7 | This repository will hold all Balmy's oracle infrastructure. It aims to have a sufficiently flexible architecture as to support a wide amount of tokens composition, and therefore enabling quoting pairs that couldn't be done before. 8 | 9 | Some of this is achieved by leveraging already existing oracles like [Uniswap V3 Static Oracle](https://github.com/Balmy-Protocol/uniswap-v3-oracle). 10 | 11 | ## 🔒 Audits 12 | 13 | Oracles has been audited by [Omniscia](https://omniscia.io/) and can be find [here](https://omniscia.io/reports/mean-finance-oracle-module/). 14 | 15 | ## 📦 NPM/YARN Package 16 | 17 | The package will contain: 18 | 19 | - Artifacts can be found under `@balmy/oracles/artifacts` 20 | - Typescript smart contract typings under `@balmy/oracles/typechained` 21 | 22 | ## 📚 Documentation 23 | 24 | Everything that you need to know as a developer on how to use all repository smart contracts can be found in the [documented interfaces](./solidity/interfaces/). 25 | 26 | ## 🛠 Installation 27 | 28 | To install with [**Hardhat**](https://github.com/nomiclabs/hardhat) or [**Truffle**](https://github.com/trufflesuite/truffle): 29 | 30 | #### YARN 31 | 32 | ```sh 33 | yarn add @balmy/oracles 34 | ``` 35 | 36 | ### NPM 37 | 38 | ```sh 39 | npm install @balmy/oracles 40 | ``` 41 | 42 | ## 📖 Deployment Registry 43 | 44 | Contracts are deployed at the same address on all available networks via the [deterministic contract factory](https://github.com/Balmy-Protocol/deterministic-factory) 45 | 46 | > Available networks: Optimism, Arbitrum One, Polygon. 47 | 48 | - Identity Oracle: `0x0171C3D8315159d771f4A4e09840b1747b7f7364` 49 | - OracleAggregator: `0x9e1ca4Cd00ED059C5d34204DCe622549583545d9` 50 | - StatefulChainlinkOracle: `0x5587d300d41E418B3F4DC7c273351748a116d78B` 51 | - UniswapV3Adapter: `0xD741623299413d02256aAC2101f8B30873fED1d2` 52 | - TransformerOracle: `0xEB8615cF5bf0f851aEFa894307aAe2b595628148` 53 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /deploy/001_stateful_chainlink_oracle.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { DeployFunction } from '@0xged/hardhat-deploy/types'; 3 | import { bytecode } from '../artifacts/solidity/contracts/StatefulChainlinkOracle.sol/StatefulChainlinkOracle.json'; 4 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer, msig } = await hre.getNamedAccounts(); 8 | const registry = await hre.deployments.get('ChainlinkFeedRegistry'); 9 | 10 | await deployThroughDeterministicFactory({ 11 | deployer, 12 | name: 'StatefulChainlinkOracle', 13 | salt: 'MF-StatefulChainlink-Oracle-V2', 14 | contract: 'solidity/contracts/StatefulChainlinkOracle.sol:StatefulChainlinkOracle', 15 | bytecode, 16 | constructorArgs: { 17 | types: ['address', 'address', 'address[]'], 18 | values: [registry.address, msig, [msig]], 19 | }, 20 | log: !process.env.TEST, 21 | overrides: !!process.env.COVERAGE 22 | ? {} 23 | : { 24 | gasLimit: 3_000_000, 25 | }, 26 | }); 27 | }; 28 | 29 | deployFunction.tags = ['StatefulChainlinkOracle']; 30 | deployFunction.dependencies = ['ChainlinkFeedRegistry']; 31 | export default deployFunction; 32 | -------------------------------------------------------------------------------- /deploy/002_uniswap_v3_adapter.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { bytecode } from '../artifacts/solidity/contracts/adapters/UniswapV3Adapter.sol/UniswapV3Adapter.json'; 3 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 4 | import { DeployFunction } from '@0xged/hardhat-deploy/dist/types'; 5 | import moment from 'moment'; 6 | 7 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 8 | const supportedNetworks = [ 9 | 'ethereum-rinkeby', 10 | 'ethereum-kovan', 11 | 'ethereum-goerli', 12 | 'ethereum', 13 | 'optimism', 14 | 'optimism-kovan', 15 | 'optimism-goerli', 16 | 'arbitrum', 17 | 'arbitrum-rinkeby', 18 | 'polygon', 19 | 'polygon-mumbai', 20 | 'bnb', 21 | 'bnb-testnet', 22 | 'base', 23 | 'base-goerli', 24 | ]; 25 | 26 | if (!supportedNetworks.includes(hre.deployments.getNetworkName().toLowerCase())) { 27 | return; 28 | } 29 | 30 | const { deployer, msig } = await hre.getNamedAccounts(); 31 | 32 | const minimumPeriod = moment.duration('5', 'minutes').as('seconds'); 33 | const maximumPeriod = moment.duration('45', 'minutes').as('seconds'); 34 | const period = moment.duration('10', 'minutes').as('seconds'); 35 | 36 | const config = { 37 | uniswapV3Oracle: '0xB210CE856631EeEB767eFa666EC7C1C57738d438', 38 | maxPeriod: maximumPeriod, 39 | minPeriod: minimumPeriod, 40 | initialPeriod: period, 41 | superAdmin: msig, 42 | initialAdmins: [msig], 43 | }; 44 | 45 | await deployThroughDeterministicFactory({ 46 | deployer, 47 | name: 'UniswapV3Adapter', 48 | salt: 'MF-Uniswap-V3-Adapter-V1', 49 | contract: 'solidity/contracts/adapters/UniswapV3Adapter.sol:UniswapV3Adapter', 50 | bytecode, 51 | constructorArgs: { 52 | types: [ 53 | 'tuple(address uniswapV3Oracle,uint16 maxPeriod,uint16 minPeriod,uint16 initialPeriod,address superAdmin,address[] initialAdmins)', 54 | ], 55 | values: [config], 56 | }, 57 | log: !process.env.TEST, 58 | overrides: !!process.env.COVERAGE 59 | ? {} 60 | : { 61 | gasLimit: 3_000_000, 62 | }, 63 | }); 64 | }; 65 | 66 | deployFunction.dependencies = []; 67 | deployFunction.tags = ['UniswapV3Adapter']; 68 | export default deployFunction; 69 | -------------------------------------------------------------------------------- /deploy/003_identity_oracle.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { bytecode } from '../artifacts/solidity/contracts/IdentityOracle.sol/IdentityOracle.json'; 3 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 4 | import { DeployFunction } from '@0xged/hardhat-deploy/dist/types'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer } = await hre.getNamedAccounts(); 8 | 9 | await deployThroughDeterministicFactory({ 10 | deployer, 11 | name: 'IdentityOracle', 12 | salt: 'MF-Identity-Oracle-V1', 13 | contract: 'solidity/contracts/IdentityOracle.sol:IdentityOracle', 14 | bytecode, 15 | constructorArgs: { 16 | types: [], 17 | values: [], 18 | }, 19 | log: !process.env.TEST, 20 | overrides: !!process.env.COVERAGE 21 | ? {} 22 | : { 23 | gasLimit: 3_000_000, 24 | }, 25 | }); 26 | }; 27 | 28 | deployFunction.dependencies = []; 29 | deployFunction.tags = ['IdentityOracle']; 30 | export default deployFunction; 31 | -------------------------------------------------------------------------------- /deploy/004_oracle_aggregator.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { bytecode } from '../artifacts/solidity/contracts/OracleAggregator.sol/OracleAggregator.json'; 3 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 4 | import { DeployFunction } from '@0xged/hardhat-deploy/dist/types'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer, msig } = await hre.getNamedAccounts(); 8 | 9 | const identityOracle = await hre.deployments.get('IdentityOracle'); 10 | const chainlinOracle = await hre.deployments.get('StatefulChainlinkOracle'); 11 | const uniswapAdapter = await hre.deployments.getOrNull('UniswapV3Adapter'); 12 | const oracles = [identityOracle.address, chainlinOracle.address]; 13 | if (uniswapAdapter) { 14 | oracles.push(uniswapAdapter.address); 15 | } 16 | 17 | await deployThroughDeterministicFactory({ 18 | deployer, 19 | name: 'OracleAggregator', 20 | salt: 'MF-Oracle-Aggregator-V1', 21 | contract: 'solidity/contracts/OracleAggregator.sol:OracleAggregator', 22 | bytecode, 23 | constructorArgs: { 24 | types: ['address[]', 'address', 'address[]'], 25 | values: [oracles, msig, [msig]], 26 | }, 27 | log: !process.env.TEST, 28 | overrides: !!process.env.COVERAGE 29 | ? {} 30 | : { 31 | gasLimit: 3_000_000, 32 | }, 33 | }); 34 | }; 35 | 36 | deployFunction.dependencies = ['StatefulChainlinkOracle', 'UniswapV3Adapter', 'IdentityOracle']; 37 | deployFunction.tags = ['OracleAggregator']; 38 | export default deployFunction; 39 | -------------------------------------------------------------------------------- /deploy/005_transformer_oracle.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { bytecode } from '../artifacts/solidity/contracts/TransformerOracle.sol/TransformerOracle.json'; 3 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 4 | import { DeployFunction } from '@0xged/hardhat-deploy/dist/types'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer, msig } = await hre.getNamedAccounts(); 8 | 9 | const transformerRegistry = await hre.deployments.get('TransformerRegistry'); 10 | const aggregator = await hre.deployments.get('OracleAggregator'); 11 | 12 | await deployThroughDeterministicFactory({ 13 | deployer, 14 | name: 'TransformerOracle', 15 | salt: 'MF-Transformer-Oracle-V2', 16 | contract: 'solidity/contracts/TransformerOracle.sol:TransformerOracle', 17 | bytecode, 18 | constructorArgs: { 19 | types: ['address', 'address', 'address', 'address[]'], 20 | values: [transformerRegistry.address, aggregator.address, msig, [msig]], 21 | }, 22 | log: !process.env.TEST, 23 | overrides: !!process.env.COVERAGE 24 | ? {} 25 | : { 26 | gasLimit: 3_000_000, 27 | }, 28 | }); 29 | }; 30 | 31 | deployFunction.dependencies = ['OracleAggregator', 'TransformerRegistry']; 32 | deployFunction.tags = ['TransformerOracle']; 33 | export default deployFunction; 34 | -------------------------------------------------------------------------------- /deploy/006_api3_chainlink_adapter_factory.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { bytecode } from '../artifacts/solidity/contracts/adapters/api3-chainlink-adapter/API3ChainlinkAdapterFactory.sol/API3ChainlinkAdapterFactory.json'; 3 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 4 | import { DeployFunction } from '@0xged/hardhat-deploy/dist/types'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer } = await hre.getNamedAccounts(); 8 | 9 | await deployThroughDeterministicFactory({ 10 | deployer, 11 | name: 'API3ChainlinkAdapterFactory', 12 | salt: 'MF-API3-Adapter-Factory-V2', 13 | contract: 'solidity/contracts/adapters/api3-chainlink-adapter/API3ChainlinkAdapterFactory.sol:API3ChainlinkAdapterFactory', 14 | bytecode, 15 | constructorArgs: { 16 | types: [], 17 | values: [], 18 | }, 19 | log: !process.env.TEST, 20 | overrides: !!process.env.COVERAGE 21 | ? {} 22 | : { 23 | gasLimit: 15_000_000, 24 | }, 25 | }); 26 | }; 27 | 28 | deployFunction.dependencies = []; 29 | deployFunction.tags = ['API3ChainlinkAdapterFactory']; 30 | export default deployFunction; 31 | -------------------------------------------------------------------------------- /deploy/007_dia_chainlink_adapter_factory.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { bytecode } from '../artifacts/solidity/contracts/adapters/dia-chainlink-adapter/DIAChainlinkAdapterFactory.sol/DIAChainlinkAdapterFactory.json'; 3 | import { deployThroughDeterministicFactory } from '@mean-finance/deterministic-factory/utils/deployment'; 4 | import { DeployFunction } from '@0xged/hardhat-deploy/dist/types'; 5 | 6 | const deployFunction: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 7 | const { deployer } = await hre.getNamedAccounts(); 8 | 9 | await deployThroughDeterministicFactory({ 10 | deployer, 11 | name: 'DIAChainlinkAdapterFactory', 12 | salt: 'MF-DIA-Adapter-Factory-V2', 13 | contract: 'solidity/contracts/adapters/dia-chainlink-adapter/DIAChainlinkAdapterFactory.sol:DIAChainlinkAdapterFactory', 14 | bytecode, 15 | constructorArgs: { 16 | types: [], 17 | values: [], 18 | }, 19 | log: !process.env.TEST, 20 | overrides: !!process.env.COVERAGE 21 | ? {} 22 | : { 23 | gasLimit: 15_000_000, 24 | }, 25 | }); 26 | }; 27 | 28 | deployFunction.dependencies = []; 29 | deployFunction.tags = ['DIAChainlinkAdapterFactory']; 30 | export default deployFunction; 31 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import '@nomiclabs/hardhat-waffle'; 3 | import '@nomiclabs/hardhat-ethers'; 4 | import '@nomiclabs/hardhat-etherscan'; 5 | import '@typechain/hardhat'; 6 | import '@typechain/hardhat/dist/type-extensions'; 7 | import { removeConsoleLog } from 'hardhat-preprocessor'; 8 | import 'hardhat-gas-reporter'; 9 | import '@0xged/hardhat-deploy'; 10 | import 'solidity-coverage'; 11 | import { HardhatUserConfig, MultiSolcUserConfig, NetworksUserConfig } from 'hardhat/types'; 12 | import * as env from './utils/env'; 13 | import 'tsconfig-paths/register'; 14 | import './tasks/npm-publish-clean-typechain'; 15 | 16 | const networks: NetworksUserConfig = 17 | env.isHardhatCompile() || env.isHardhatClean() || env.isTesting() 18 | ? { 19 | hardhat: { 20 | allowUnlimitedContractSize: true, 21 | }, 22 | } 23 | : { 24 | hardhat: { 25 | forking: { 26 | enabled: process.env.FORK ? true : false, 27 | url: env.getNodeUrl('ethereum'), 28 | }, 29 | }, 30 | ['ethereum-ropsten']: { 31 | url: env.getNodeUrl('ethereum-ropsten'), 32 | accounts: env.getAccounts('ethereum-ropsten'), 33 | }, 34 | ['ethereum-rinkeby']: { 35 | url: env.getNodeUrl('ethereum-rinkeby'), 36 | accounts: env.getAccounts('ethereum-rinkeby'), 37 | }, 38 | ['ethereum-kovan']: { 39 | url: env.getNodeUrl('ethereum-kovan'), 40 | accounts: env.getAccounts('ethereum-kovan'), 41 | }, 42 | ['ethereum-goerli']: { 43 | url: env.getNodeUrl('ethereum-goerli'), 44 | accounts: env.getAccounts('ethereum-goerli'), 45 | }, 46 | ethereum: { 47 | url: env.getNodeUrl('ethereum'), 48 | accounts: env.getAccounts('ethereum'), 49 | }, 50 | optimism: { 51 | url: env.getNodeUrl('optimism'), 52 | accounts: env.getAccounts('optimism'), 53 | }, 54 | ['optimism-kovan']: { 55 | url: env.getNodeUrl('optimism-kovan'), 56 | accounts: env.getAccounts('optimism-kovan'), 57 | }, 58 | arbitrum: { 59 | url: env.getNodeUrl('arbitrum'), 60 | accounts: env.getAccounts('arbitrum'), 61 | }, 62 | ['arbitrum-rinkeby']: { 63 | url: env.getNodeUrl('arbitrum-rinkeby'), 64 | accounts: env.getAccounts('arbitrum-rinkeby'), 65 | }, 66 | polygon: { 67 | url: env.getNodeUrl('polygon'), 68 | accounts: env.getAccounts('polygon'), 69 | }, 70 | ['polygon-mumbai']: { 71 | url: env.getNodeUrl('polygon-mumbai'), 72 | accounts: env.getAccounts('polygon-mumbai'), 73 | }, 74 | }; 75 | 76 | const config: HardhatUserConfig = { 77 | defaultNetwork: 'hardhat', 78 | namedAccounts: { 79 | deployer: { 80 | default: 4, 81 | }, 82 | eoaAdmin: '0x1a00e1E311009E56e3b0B9Ed6F86f5Ce128a1C01', 83 | msig: { 84 | ethereum: '0xEC864BE26084ba3bbF3cAAcF8F6961A9263319C4', 85 | optimism: '0x308810881807189cAe91950888b2cB73A1CC5920', 86 | polygon: '0xCe9F6991b48970d6c9Ef99Fffb112359584488e3', 87 | arbitrum: '0x84F4836e8022765Af9FBCE3Bb2887fD826c668f1', 88 | }, 89 | }, 90 | mocha: { 91 | timeout: process.env.MOCHA_TIMEOUT || 300000, 92 | }, 93 | networks, 94 | solidity: { 95 | compilers: [ 96 | { 97 | version: '0.8.16', 98 | settings: { 99 | optimizer: { 100 | enabled: true, 101 | runs: 9999, 102 | }, 103 | }, 104 | }, 105 | ], 106 | }, 107 | gasReporter: { 108 | currency: process.env.COINMARKETCAP_DEFAULT_CURRENCY || 'USD', 109 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 110 | enabled: process.env.REPORT_GAS ? true : false, 111 | showMethodSig: true, 112 | onlyCalledMethods: false, 113 | excludeContracts: ['ERC20'], 114 | }, 115 | preprocess: { 116 | eachLine: removeConsoleLog((hre) => hre.network.name !== 'hardhat'), 117 | }, 118 | etherscan: { 119 | apiKey: env.getEtherscanAPIKeys([ 120 | 'ethereum-goerli', 121 | 'ethereum', 122 | 'optimism', 123 | 'optimism-kovan', 124 | 'arbitrum', 125 | 'arbitrum-rinkeby', 126 | 'polygon', 127 | 'polygon-mumbai', 128 | ]), 129 | }, 130 | external: {}, 131 | typechain: { 132 | outDir: 'typechained', 133 | target: 'ethers-v5', 134 | externalArtifacts: [], 135 | }, 136 | paths: { 137 | sources: './solidity', 138 | }, 139 | }; 140 | 141 | if (process.env.TEST) { 142 | config.external!.contracts = [ 143 | { 144 | artifacts: 'node_modules/@mean-finance/chainlink-registry/artifacts', 145 | deploy: 'node_modules/@mean-finance/chainlink-registry/deploy', 146 | }, 147 | { 148 | artifacts: 'node_modules/@mean-finance/transformers/artifacts', 149 | deploy: 'node_modules/@mean-finance/transformers/deploy', 150 | }, 151 | ]; 152 | (config.solidity as MultiSolcUserConfig).compilers = (config.solidity as MultiSolcUserConfig).compilers.map((compiler) => { 153 | return { 154 | ...compiler, 155 | outputSelection: { 156 | '*': { 157 | '*': ['storageLayout'], 158 | }, 159 | }, 160 | }; 161 | }); 162 | } 163 | 164 | export default config; 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@balmy/oracles", 3 | "version": "2.6.0", 4 | "description": "Balmy Oracles", 5 | "keywords": [ 6 | "ethereum", 7 | "smart", 8 | "contracts", 9 | "test", 10 | "solidity", 11 | "hardhat", 12 | "oracle" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/balmy-protocol/oracles.git" 17 | }, 18 | "license": "AGPL-3.0-only", 19 | "contributors": [ 20 | { 21 | "name": "0xged", 22 | "url": "https://github.com/0xged" 23 | }, 24 | { 25 | "name": "nchamo", 26 | "url": "https://github.com/nchamo" 27 | }, 28 | { 29 | "name": "0xsambugs", 30 | "url": "https://github.com/0xsambugs" 31 | } 32 | ], 33 | "main": "dist", 34 | "types": "dist", 35 | "files": [ 36 | "dist", 37 | "solidity", 38 | "!solidity/contracts/test/**", 39 | "artifacts/solidity/**/*.json", 40 | "!artifacts/solidity/contracts/test/**", 41 | "!artifacts/solidity/**/**/*.dbg.json", 42 | "!/**/*test*", 43 | "!/**/*Mock*", 44 | "deploy", 45 | "!.env", 46 | "!**/.DS_Store" 47 | ], 48 | "scripts": { 49 | "compile": "hardhat compile", 50 | "compile:test": "cross-env TEST=true hardhat compile", 51 | "coverage": "TS_NODE_SKIP_IGNORE=true TEST=true COVERAGE=true npx hardhat coverage --solcoverjs ./solcover.js", 52 | "deploy": "npx hardhat deploy", 53 | "fork:node": "cross-env FORK=true hardhat node", 54 | "fork:script": "cross-env FORK=true hardhat run", 55 | "postinstall": "husky install && yarn compile:test", 56 | "lint:check": "cross-env solhint 'contracts/**/*.sol' 'interfaces/**/*.sol' && cross-env prettier --check './**'", 57 | "lint:fix": "sort-package-json && cross-env prettier --write './**' && cross-env solhint --fix 'contracts/**/*.sol' 'interfaces/**/*.sol'", 58 | "prepare": "husky install", 59 | "prepublishOnly": "hardhat clean && PUBLISHING_NPM=true hardhat compile && yarn transpile && pinst --disable", 60 | "postpublish": "pinst --enable", 61 | "release": "standard-version", 62 | "test": "yarn compile:test && cross-env TEST=true mocha", 63 | "test:all": "yarn test './test/integration/**/*.spec.ts' && cross-env TEST=true mocha 'test/unit/**/*.spec.ts'", 64 | "test:e2e": "yarn test './test/e2e/**/*.spec.ts'", 65 | "test:gas": "cross-env REPORT_GAS=1 npx hardhat test", 66 | "test:integration": "cross-env USE_RANDOM_SALT=true yarn test './test/integration/**/*.spec.ts'", 67 | "test:unit": "yarn test 'test/unit/**/*.spec.ts'", 68 | "transpile": "rm -rf dist && npx tsc -p tsconfig.publish.json" 69 | }, 70 | "lint-staged": { 71 | "*.{js,css,md,ts,sol,json}": "prettier --write", 72 | "*.sol": "cross-env solhint --fix 'contracts/**/*.sol' 'interfaces/**/*.sol'", 73 | "package.json": "sort-package-json" 74 | }, 75 | "resolutions": { 76 | "cli-table3@^0.5.0/colors": "1.4.0", 77 | "cli-table@^0.3.1/colors": "1.0.3", 78 | "eth-gas-reporter/colors": "1.4.0" 79 | }, 80 | "dependencies": { 81 | "@0xged/hardhat-deploy": "0.11.5", 82 | "@api3/contracts": "0.9.1", 83 | "@chainlink/contracts": "0.4.2", 84 | "@mean-finance/chainlink-registry": "2.2.0", 85 | "@mean-finance/deterministic-factory": "1.10.0", 86 | "@mean-finance/transformers": "1.3.0", 87 | "@mean-finance/uniswap-v3-oracle": "1.0.2", 88 | "@openzeppelin/contracts": "4.7.3", 89 | "@uniswap/v3-core": "1.0.1", 90 | "moment": "2.29.3" 91 | }, 92 | "devDependencies": { 93 | "@codechecks/client": "0.1.12", 94 | "@commitlint/cli": "16.2.4", 95 | "@commitlint/config-conventional": "16.2.4", 96 | "@defi-wonderland/smock": "2.4.0", 97 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@0.3.0-beta.13", 98 | "@nomiclabs/hardhat-etherscan": "3.1.0", 99 | "@nomiclabs/hardhat-waffle": "2.0.3", 100 | "@openzeppelin/test-helpers": "0.5.15", 101 | "@typechain/ethers-v5": "10.1.0", 102 | "@typechain/hardhat": "6.1.2", 103 | "@types/axios": "0.14.0", 104 | "@types/chai": "4.3.1", 105 | "@types/chai-as-promised": "7.1.5", 106 | "@types/lodash": "4.14.182", 107 | "@types/mocha": "9.1.1", 108 | "@types/node": "17.0.31", 109 | "axios": "0.27.2", 110 | "bignumber.js": "9.0.2", 111 | "chai": "4.3.6", 112 | "chai-as-promised": "7.1.1", 113 | "cross-env": "7.0.3", 114 | "dotenv": "16.0.0", 115 | "ethereum-waffle": "3.4.4", 116 | "ethers": "5.6.5", 117 | "hardhat": "2.23.0", 118 | "hardhat-gas-reporter": "1.0.8", 119 | "hardhat-preprocessor": "0.1.4", 120 | "husky": "7.0.4", 121 | "lint-staged": "12.4.1", 122 | "lodash": "4.17.21", 123 | "mocha": "10.0.0", 124 | "pinst": "3.0.0", 125 | "prettier": "2.6.2", 126 | "prettier-plugin-solidity": "1.0.0-beta.19", 127 | "solc-0.8": "npm:solc@0.8.13", 128 | "solhint": "3.3.7", 129 | "solhint-plugin-prettier": "0.0.5", 130 | "solidity-coverage": "https://github.com/adjisb/solidity-coverage", 131 | "solidity-docgen": "0.5.16", 132 | "sort-package-json": "1.57.0", 133 | "standard-version": "9.3.2", 134 | "ts-node": "10.7.0", 135 | "tsconfig-paths": "4.0.0", 136 | "typechain": "8.1.0", 137 | "typescript": "4.6.4" 138 | }, 139 | "publishConfig": { 140 | "access": "public" 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /scripts/verify-contracts.ts: -------------------------------------------------------------------------------- 1 | import { deployments } from 'hardhat'; 2 | import { run } from 'hardhat'; 3 | 4 | async function main() { 5 | await verify({ 6 | name: 'StatefulChainlinkOracleAdapter', 7 | path: 'solidity/contracts/adapters/StatefulChainlinkOracleAdapter.sol:StatefulChainlinkOracleAdapter', 8 | }); 9 | await verify({ 10 | name: 'UniswapV3Adapter', 11 | path: 'solidity/contracts/adapters/UniswapV3Adapter.sol:UniswapV3Adapter', 12 | }); 13 | await verify({ 14 | name: 'OracleAggregator', 15 | path: 'solidity/contracts/OracleAggregator.sol:OracleAggregator', 16 | }); 17 | } 18 | 19 | async function verify({ name, path }: { name: string; path: string }) { 20 | const contract = await deployments.getOrNull(name); 21 | try { 22 | await run('verify:verify', { 23 | address: contract!.address, 24 | constructorArguments: contract!.args, 25 | contract: path, 26 | }); 27 | } catch (e: any) { 28 | if (!e.message.toLowerCase().includes('already verified')) { 29 | throw e; 30 | } 31 | } 32 | } 33 | 34 | main() 35 | .then(() => process.exit(0)) 36 | .catch((error) => { 37 | console.error(error); 38 | process.exit(1); 39 | }); 40 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude_low": true, 3 | "detectors_to_exclude": "conformance-to-solidity-naming-conventions,incorrect-versions-of-solidity,similar-names,solc-version", 4 | "filter_paths": "node_modules|openzeppelin|interfaces|UniswapV3Pool.sol" 5 | } 6 | -------------------------------------------------------------------------------- /solidity/contracts/IdentityOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import './base/SimpleOracle.sol'; 5 | 6 | /// @notice An oracle that works for pairs where both tokens are the same 7 | contract IdentityOracle is SimpleOracle { 8 | /// @inheritdoc ITokenPriceOracle 9 | function canSupportPair(address _tokenA, address _tokenB) external pure returns (bool) { 10 | return _tokenA == _tokenB; 11 | } 12 | 13 | /// @inheritdoc ITokenPriceOracle 14 | function isPairAlreadySupported(address _tokenA, address _tokenB) public pure override returns (bool) { 15 | return _tokenA == _tokenB; 16 | } 17 | 18 | /// @inheritdoc ITokenPriceOracle 19 | function quote( 20 | address _tokenIn, 21 | uint256 _amountIn, 22 | address _tokenOut, 23 | bytes calldata 24 | ) external pure returns (uint256 _amountOut) { 25 | if (_tokenIn != _tokenOut) revert PairNotSupportedYet(_tokenIn, _tokenOut); 26 | return _amountIn; 27 | } 28 | 29 | function _addOrModifySupportForPair( 30 | address _tokenA, 31 | address _tokenB, 32 | bytes calldata 33 | ) internal pure override { 34 | if (_tokenA != _tokenB) revert PairCannotBeSupported(_tokenA, _tokenB); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /solidity/contracts/OracleAggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol'; 5 | import '@openzeppelin/contracts/access/AccessControl.sol'; 6 | import './base/SimpleOracle.sol'; 7 | import './libraries/TokenSorting.sol'; 8 | import '../interfaces/IOracleAggregator.sol'; 9 | 10 | contract OracleAggregator is AccessControl, SimpleOracle, IOracleAggregator { 11 | bytes32 public constant SUPER_ADMIN_ROLE = keccak256('SUPER_ADMIN_ROLE'); 12 | bytes32 public constant ADMIN_ROLE = keccak256('ADMIN_ROLE'); 13 | 14 | // A list of available oracles. Oracles first on the array will take precedence over those that come later 15 | ITokenPriceOracle[] internal _availableOracles; 16 | mapping(bytes32 => OracleAssignment) internal _assignedOracle; // key(tokenA, tokenB) => oracle 17 | 18 | constructor( 19 | address[] memory _initialOracles, 20 | address _superAdmin, 21 | address[] memory _initialAdmins 22 | ) { 23 | if (_superAdmin == address(0)) revert ZeroAddress(); 24 | // We are setting the super admin role as its own admin so we can transfer it 25 | _setRoleAdmin(SUPER_ADMIN_ROLE, SUPER_ADMIN_ROLE); 26 | _setRoleAdmin(ADMIN_ROLE, SUPER_ADMIN_ROLE); 27 | _setupRole(SUPER_ADMIN_ROLE, _superAdmin); 28 | for (uint256 i; i < _initialAdmins.length; i++) { 29 | _setupRole(ADMIN_ROLE, _initialAdmins[i]); 30 | } 31 | 32 | if (_initialOracles.length > 0) { 33 | for (uint256 i; i < _initialOracles.length; i++) { 34 | _revertIfNotOracle(_initialOracles[i]); 35 | _availableOracles.push(ITokenPriceOracle(_initialOracles[i])); 36 | } 37 | emit OracleListUpdated(_initialOracles); 38 | } 39 | } 40 | 41 | /// @inheritdoc ITokenPriceOracle 42 | function canSupportPair(address _tokenA, address _tokenB) external view returns (bool) { 43 | uint256 _length = _availableOracles.length; 44 | for (uint256 i; i < _length; i++) { 45 | if (_availableOracles[i].canSupportPair(_tokenA, _tokenB)) { 46 | return true; 47 | } 48 | } 49 | return false; 50 | } 51 | 52 | /// @inheritdoc ITokenPriceOracle 53 | function isPairAlreadySupported(address _tokenA, address _tokenB) public view override(ITokenPriceOracle, SimpleOracle) returns (bool) { 54 | ITokenPriceOracle _oracle = assignedOracle(_tokenA, _tokenB).oracle; 55 | // We check if the oracle still supports the pair, since it might have lost support 56 | return address(_oracle) != address(0) && _oracle.isPairAlreadySupported(_tokenA, _tokenB); 57 | } 58 | 59 | /// @inheritdoc ITokenPriceOracle 60 | function quote( 61 | address _tokenIn, 62 | uint256 _amountIn, 63 | address _tokenOut, 64 | bytes calldata _data 65 | ) external view returns (uint256 _amountOut) { 66 | ITokenPriceOracle _oracle = assignedOracle(_tokenIn, _tokenOut).oracle; 67 | if (address(_oracle) == address(0)) revert PairNotSupportedYet(_tokenIn, _tokenOut); 68 | return _oracle.quote(_tokenIn, _amountIn, _tokenOut, _data); 69 | } 70 | 71 | /// @inheritdoc ITokenPriceOracle 72 | function addOrModifySupportForPair( 73 | address _tokenA, 74 | address _tokenB, 75 | bytes calldata _data 76 | ) external override(ITokenPriceOracle, SimpleOracle) { 77 | OracleAssignment memory _assignment = assignedOracle(_tokenA, _tokenB); 78 | if (_canModifySupportForPair(_tokenA, _tokenB, _assignment)) { 79 | _addOrModifySupportForPair(_tokenA, _tokenB, _data); 80 | } 81 | } 82 | 83 | /// @inheritdoc IOracleAggregator 84 | function assignedOracle(address _tokenA, address _tokenB) public view returns (OracleAssignment memory) { 85 | return _assignedOracle[_keyForPair(_tokenA, _tokenB)]; 86 | } 87 | 88 | /// @inheritdoc IOracleAggregator 89 | function availableOracles() external view returns (ITokenPriceOracle[] memory) { 90 | return _availableOracles; 91 | } 92 | 93 | /// @inheritdoc IOracleAggregator 94 | function previewAddOrModifySupportForPair(address _tokenA, address _tokenB) external view returns (ITokenPriceOracle) { 95 | OracleAssignment memory _assignment = assignedOracle(_tokenA, _tokenB); 96 | return _canModifySupportForPair(_tokenA, _tokenB, _assignment) ? _findFirstOracleThatCanSupportPair(_tokenA, _tokenB) : _assignment.oracle; 97 | } 98 | 99 | /// @inheritdoc IOracleAggregator 100 | function forceOracle( 101 | address _tokenA, 102 | address _tokenB, 103 | address _oracle, 104 | bytes calldata _data 105 | ) external onlyRole(ADMIN_ROLE) { 106 | _revertIfNotOracle(_oracle); 107 | _setOracle(_tokenA, _tokenB, ITokenPriceOracle(_oracle), _data, true); 108 | } 109 | 110 | /// @inheritdoc IOracleAggregator 111 | function setAvailableOracles(address[] calldata _oracles) external onlyRole(ADMIN_ROLE) { 112 | uint256 _currentAvailableOracles = _availableOracles.length; 113 | uint256 _min = _currentAvailableOracles < _oracles.length ? _currentAvailableOracles : _oracles.length; 114 | 115 | uint256 i; 116 | for (; i < _min; i++) { 117 | // Rewrite storage 118 | _revertIfNotOracle(_oracles[i]); 119 | _availableOracles[i] = ITokenPriceOracle(_oracles[i]); 120 | } 121 | if (_currentAvailableOracles < _oracles.length) { 122 | // If have more oracles than before, then push 123 | for (; i < _oracles.length; i++) { 124 | _revertIfNotOracle(_oracles[i]); 125 | _availableOracles.push(ITokenPriceOracle(_oracles[i])); 126 | } 127 | } else if (_currentAvailableOracles > _oracles.length) { 128 | // If have less oracles than before, then remove extra oracles 129 | for (; i < _currentAvailableOracles; i++) { 130 | _availableOracles.pop(); 131 | } 132 | } 133 | 134 | emit OracleListUpdated(_oracles); 135 | } 136 | 137 | /// @inheritdoc IERC165 138 | function supportsInterface(bytes4 _interfaceId) public view virtual override(AccessControl, BaseOracle) returns (bool) { 139 | return 140 | _interfaceId == type(IOracleAggregator).interfaceId || 141 | AccessControl.supportsInterface(_interfaceId) || 142 | BaseOracle.supportsInterface(_interfaceId); 143 | } 144 | 145 | /** 146 | * @notice Checks all oracles again and re-assigns the first that supports the given pair. 147 | * It will also reconfigure the assigned oracle 148 | */ 149 | function _addOrModifySupportForPair( 150 | address _tokenA, 151 | address _tokenB, 152 | bytes calldata _data 153 | ) internal virtual override { 154 | ITokenPriceOracle _oracle = _findFirstOracleThatCanSupportPair(_tokenA, _tokenB); 155 | if (address(_oracle) == address(0)) revert PairCannotBeSupported(_tokenA, _tokenB); 156 | _setOracle(_tokenA, _tokenB, _oracle, _data, false); 157 | } 158 | 159 | function _canModifySupportForPair( 160 | address _tokenA, 161 | address _tokenB, 162 | OracleAssignment memory _assignment 163 | ) internal view returns (bool) { 164 | /* 165 | Only modify if one of the following is true: 166 | - There is no current oracle 167 | - The current oracle hasn't been forced by an admin 168 | - The current oracle has been forced but it has lost support for the pair 169 | - The caller is an admin 170 | */ 171 | return !_assignment.forced || hasRole(ADMIN_ROLE, msg.sender) || !_assignment.oracle.isPairAlreadySupported(_tokenA, _tokenB); 172 | } 173 | 174 | function _findFirstOracleThatCanSupportPair(address _tokenA, address _tokenB) internal view returns (ITokenPriceOracle) { 175 | uint256 _length = _availableOracles.length; 176 | for (uint256 i; i < _length; i++) { 177 | ITokenPriceOracle _oracle = _availableOracles[i]; 178 | if (_oracle.canSupportPair(_tokenA, _tokenB)) { 179 | return _oracle; 180 | } 181 | } 182 | return ITokenPriceOracle(address(0)); 183 | } 184 | 185 | function _setOracle( 186 | address _tokenA, 187 | address _tokenB, 188 | ITokenPriceOracle _oracle, 189 | bytes calldata _data, 190 | bool _forced 191 | ) internal { 192 | _oracle.addOrModifySupportForPair(_tokenA, _tokenB, _data); 193 | _assignedOracle[_keyForPair(_tokenA, _tokenB)] = OracleAssignment({oracle: _oracle, forced: _forced}); 194 | emit OracleAssigned(_tokenA, _tokenB, _oracle); 195 | } 196 | 197 | function _revertIfNotOracle(address _oracleToCheck) internal view { 198 | bool _isOracle = ERC165Checker.supportsInterface(_oracleToCheck, type(ITokenPriceOracle).interfaceId); 199 | if (!_isOracle) revert AddressIsNotOracle(_oracleToCheck); 200 | } 201 | 202 | function _keyForPair(address _tokenA, address _tokenB) internal pure returns (bytes32) { 203 | (address __tokenA, address __tokenB) = TokenSorting.sortTokens(_tokenA, _tokenB); 204 | return keccak256(abi.encodePacked(__tokenA, __tokenB)); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /solidity/contracts/adapters/UniswapV3Adapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '@openzeppelin/contracts/access/AccessControl.sol'; 5 | import '@openzeppelin/contracts/utils/math/SafeCast.sol'; 6 | import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; 7 | import '../../interfaces//adapters/IUniswapV3Adapter.sol'; 8 | import '../libraries/TokenSorting.sol'; 9 | import '../base/SimpleOracle.sol'; 10 | 11 | contract UniswapV3Adapter is AccessControl, SimpleOracle, IUniswapV3Adapter { 12 | using SafeCast for uint256; 13 | 14 | bytes32 public constant SUPER_ADMIN_ROLE = keccak256('SUPER_ADMIN_ROLE'); 15 | bytes32 public constant ADMIN_ROLE = keccak256('ADMIN_ROLE'); 16 | 17 | /// @inheritdoc IUniswapV3Adapter 18 | IStaticOracle public immutable UNISWAP_V3_ORACLE; 19 | /// @inheritdoc IUniswapV3Adapter 20 | uint32 public immutable MAX_PERIOD; 21 | /// @inheritdoc IUniswapV3Adapter 22 | uint32 public immutable MIN_PERIOD; 23 | /// @inheritdoc IUniswapV3Adapter 24 | uint32 public period; 25 | /// @inheritdoc IUniswapV3Adapter 26 | uint8 public cardinalityPerMinute; 27 | /// @inheritdoc IUniswapV3Adapter 28 | uint104 public gasPerCardinality = 22_250; 29 | /// @inheritdoc IUniswapV3Adapter 30 | uint112 public gasCostToSupportPool = 30_000; 31 | 32 | mapping(bytes32 => bool) internal _isPairDenylisted; // key(tokenA, tokenB) => is denylisted 33 | mapping(bytes32 => address[]) internal _poolsForPair; // key(tokenA, tokenB) => pools 34 | 35 | constructor(InitialConfig memory _initialConfig) { 36 | if (_initialConfig.superAdmin == address(0)) revert ZeroAddress(); 37 | UNISWAP_V3_ORACLE = _initialConfig.uniswapV3Oracle; 38 | MAX_PERIOD = _initialConfig.maxPeriod; 39 | MIN_PERIOD = _initialConfig.minPeriod; 40 | // We are setting the super admin role as its own admin so we can transfer it 41 | _setRoleAdmin(SUPER_ADMIN_ROLE, SUPER_ADMIN_ROLE); 42 | _setRoleAdmin(ADMIN_ROLE, SUPER_ADMIN_ROLE); 43 | _setupRole(SUPER_ADMIN_ROLE, _initialConfig.superAdmin); 44 | for (uint256 i; i < _initialConfig.initialAdmins.length; i++) { 45 | _setupRole(ADMIN_ROLE, _initialConfig.initialAdmins[i]); 46 | } 47 | 48 | // Set the period 49 | if (_initialConfig.initialPeriod < MIN_PERIOD || _initialConfig.initialPeriod > MAX_PERIOD) 50 | revert InvalidPeriod(_initialConfig.initialPeriod); 51 | period = _initialConfig.initialPeriod; 52 | emit PeriodChanged(_initialConfig.initialPeriod); 53 | 54 | // Set cardinality, by using the oracle's default 55 | uint8 _cardinality = UNISWAP_V3_ORACLE.CARDINALITY_PER_MINUTE(); 56 | cardinalityPerMinute = _cardinality; 57 | emit CardinalityPerMinuteChanged(_cardinality); 58 | } 59 | 60 | /// @inheritdoc ITokenPriceOracle 61 | function canSupportPair(address _tokenA, address _tokenB) external view returns (bool) { 62 | if (_isPairDenylisted[_keyForPair(_tokenA, _tokenB)]) { 63 | return false; 64 | } 65 | try UNISWAP_V3_ORACLE.getAllPoolsForPair(_tokenA, _tokenB) returns (address[] memory _pools) { 66 | return _pools.length > 0; 67 | } catch { 68 | return false; 69 | } 70 | } 71 | 72 | /// @inheritdoc ITokenPriceOracle 73 | function isPairAlreadySupported(address _tokenA, address _tokenB) public view override(ITokenPriceOracle, SimpleOracle) returns (bool) { 74 | return _poolsForPair[_keyForPair(_tokenA, _tokenB)].length > 0; 75 | } 76 | 77 | /// @inheritdoc ITokenPriceOracle 78 | function quote( 79 | address _tokenIn, 80 | uint256 _amountIn, 81 | address _tokenOut, 82 | bytes calldata 83 | ) external view returns (uint256) { 84 | address[] memory _pools = _poolsForPair[_keyForPair(_tokenIn, _tokenOut)]; 85 | if (_pools.length == 0) revert PairNotSupportedYet(_tokenIn, _tokenOut); 86 | return UNISWAP_V3_ORACLE.quoteSpecificPoolsWithTimePeriod(_amountIn.toUint128(), _tokenIn, _tokenOut, _pools, period); 87 | } 88 | 89 | /// @inheritdoc IUniswapV3Adapter 90 | function isPairDenylisted(address _tokenA, address _tokenB) external view returns (bool) { 91 | return _isPairDenylisted[_keyForPair(_tokenA, _tokenB)]; 92 | } 93 | 94 | /// @inheritdoc IUniswapV3Adapter 95 | function getPoolsPreparedForPair(address _tokenA, address _tokenB) external view returns (address[] memory) { 96 | return _poolsForPair[_keyForPair(_tokenA, _tokenB)]; 97 | } 98 | 99 | /// @inheritdoc IUniswapV3Adapter 100 | function setPeriod(uint32 _newPeriod) external onlyRole(ADMIN_ROLE) { 101 | if (_newPeriod < MIN_PERIOD || _newPeriod > MAX_PERIOD) revert InvalidPeriod(_newPeriod); 102 | period = _newPeriod; 103 | emit PeriodChanged(_newPeriod); 104 | } 105 | 106 | /// @inheritdoc IUniswapV3Adapter 107 | function setCardinalityPerMinute(uint8 _cardinalityPerMinute) external onlyRole(ADMIN_ROLE) { 108 | if (_cardinalityPerMinute == 0) revert InvalidCardinalityPerMinute(); 109 | cardinalityPerMinute = _cardinalityPerMinute; 110 | emit CardinalityPerMinuteChanged(_cardinalityPerMinute); 111 | } 112 | 113 | /// @inheritdoc IUniswapV3Adapter 114 | function setGasPerCardinality(uint104 _gasPerCardinality) external onlyRole(ADMIN_ROLE) { 115 | if (_gasPerCardinality == 0) revert InvalidGasPerCardinality(); 116 | gasPerCardinality = _gasPerCardinality; 117 | emit GasPerCardinalityChanged(_gasPerCardinality); 118 | } 119 | 120 | /// @inheritdoc IUniswapV3Adapter 121 | function setGasCostToSupportPool(uint112 _gasCostToSupportPool) external onlyRole(ADMIN_ROLE) { 122 | if (_gasCostToSupportPool == 0) revert InvalidGasCostToSupportPool(); 123 | gasCostToSupportPool = _gasCostToSupportPool; 124 | emit GasCostToSupportPoolChanged(_gasCostToSupportPool); 125 | } 126 | 127 | /// @inheritdoc IUniswapV3Adapter 128 | function setDenylisted(Pair[] calldata _pairs, bool[] calldata _denylisted) external onlyRole(ADMIN_ROLE) { 129 | if (_pairs.length != _denylisted.length) revert InvalidDenylistParams(); 130 | for (uint256 i; i < _pairs.length; i++) { 131 | bytes32 _pairKey = _keyForPair(_pairs[i].tokenA, _pairs[i].tokenB); 132 | _isPairDenylisted[_pairKey] = _denylisted[i]; 133 | if (_denylisted[i] && _poolsForPair[_pairKey].length > 0) { 134 | delete _poolsForPair[_pairKey]; 135 | } 136 | } 137 | emit DenylistChanged(_pairs, _denylisted); 138 | } 139 | 140 | /// @inheritdoc IERC165 141 | function supportsInterface(bytes4 _interfaceId) public view virtual override(AccessControl, BaseOracle) returns (bool) { 142 | return 143 | _interfaceId == type(IUniswapV3Adapter).interfaceId || 144 | AccessControl.supportsInterface(_interfaceId) || 145 | BaseOracle.supportsInterface(_interfaceId); 146 | } 147 | 148 | function _addOrModifySupportForPair( 149 | address _tokenA, 150 | address _tokenB, 151 | bytes calldata 152 | ) internal override { 153 | bytes32 _pairKey = _keyForPair(_tokenA, _tokenB); 154 | if (_isPairDenylisted[_pairKey]) revert PairCannotBeSupported(_tokenA, _tokenB); 155 | 156 | address[] memory _pools = _getAllPoolsSortedByLiquidity(_tokenA, _tokenB); 157 | if (_pools.length == 0) revert PairCannotBeSupported(_tokenA, _tokenB); 158 | 159 | // Load to mem to avoid multiple storage reads 160 | address[] storage _storagePools = _poolsForPair[_pairKey]; 161 | uint256 _poolsPreviouslyInStorage = _storagePools.length; 162 | uint104 _gasCostPerCardinality = gasPerCardinality; 163 | uint112 _gasCostToSupportPool = gasCostToSupportPool; 164 | 165 | uint16 _targetCardinality = uint16((period * cardinalityPerMinute) / 60) + 1; 166 | uint256 _preparedPools; 167 | for (uint256 i; i < _pools.length; i++) { 168 | address _pool = _pools[i]; 169 | (, , , , uint16 _currentCardinality, , ) = IUniswapV3Pool(_pool).slot0(); 170 | if (_currentCardinality < _targetCardinality) { 171 | uint112 _gasCostToIncreaseAndAddSupport = uint112(_targetCardinality - _currentCardinality) * 172 | _gasCostPerCardinality + 173 | _gasCostToSupportPool; 174 | if (_gasCostToIncreaseAndAddSupport > gasleft()) { 175 | continue; 176 | } 177 | IUniswapV3Pool(_pool).increaseObservationCardinalityNext(_targetCardinality); 178 | } 179 | if (_preparedPools < _poolsPreviouslyInStorage) { 180 | // Rewrite storage 181 | _storagePools[_preparedPools++] = _pool; 182 | } else { 183 | // If I have more pools than before, then push 184 | _storagePools.push(_pool); 185 | _preparedPools++; 186 | } 187 | } 188 | 189 | if (_preparedPools == 0) revert GasTooLow(); 190 | 191 | // If I have less pools than before, then remove the extra pools 192 | for (uint256 i = _preparedPools; i < _poolsPreviouslyInStorage; i++) { 193 | _storagePools.pop(); 194 | } 195 | 196 | emit UpdatedSupport(_tokenA, _tokenB, _preparedPools); 197 | } 198 | 199 | function _getAllPoolsSortedByLiquidity(address _tokenA, address _tokenB) internal view virtual returns (address[] memory _pools) { 200 | _pools = UNISWAP_V3_ORACLE.getAllPoolsForPair(_tokenA, _tokenB); 201 | if (_pools.length > 1) { 202 | // Store liquidity by pool 203 | uint128[] memory _poolLiquidity = new uint128[](_pools.length); 204 | for (uint256 i; i < _pools.length; i++) { 205 | _poolLiquidity[i] = IUniswapV3Pool(_pools[i]).liquidity(); 206 | } 207 | 208 | // Sort both arrays together 209 | for (uint256 i; i < _pools.length - 1; i++) { 210 | uint256 _biggestLiquidityIndex = i; 211 | for (uint256 j = i + 1; j < _pools.length; j++) { 212 | if (_poolLiquidity[j] > _poolLiquidity[_biggestLiquidityIndex]) { 213 | _biggestLiquidityIndex = j; 214 | } 215 | } 216 | if (_biggestLiquidityIndex != i) { 217 | // Swap pools 218 | (_pools[i], _pools[_biggestLiquidityIndex]) = (_pools[_biggestLiquidityIndex], _pools[i]); 219 | 220 | // Don't need to swap both ways, can just move the liquidity in i to its new place 221 | _poolLiquidity[_biggestLiquidityIndex] = _poolLiquidity[i]; 222 | } 223 | } 224 | } 225 | } 226 | 227 | function _keyForPair(address _tokenA, address _tokenB) internal pure returns (bytes32) { 228 | (address __tokenA, address __tokenB) = TokenSorting.sortTokens(_tokenA, _tokenB); 229 | return keccak256(abi.encodePacked(__tokenA, __tokenB)); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /solidity/contracts/adapters/api3-chainlink-adapter/API3ChainlinkAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '@chainlink/contracts/src/v0.8/interfaces/AggregatorV2V3Interface.sol'; 5 | import '@api3/contracts/v0.8/interfaces/IProxy.sol'; 6 | 7 | contract API3ChainlinkAdapter is AggregatorV2V3Interface { 8 | /// @notice Thrown when trying to query a round that is not the latest one 9 | error OnlyLatestRoundIsAvailable(); 10 | 11 | /// @notice The address of the underlying API3 Proxy 12 | IProxy public immutable API3_PROXY; 13 | uint8 public immutable decimals; 14 | string public description; 15 | 16 | /// @notice The round number we'll use to represent the latest round 17 | uint80 internal constant LATEST_ROUND = 0; 18 | /// @notice Magnitude to convert API3 values to Chainlink values 19 | uint256 internal immutable _magnitudeConversion; 20 | 21 | constructor( 22 | IProxy _api3Proxy, 23 | uint8 _decimals, 24 | string memory _description 25 | ) { 26 | API3_PROXY = _api3Proxy; 27 | decimals = _decimals; 28 | description = _description; 29 | _magnitudeConversion = 10**(18 - _decimals); 30 | } 31 | 32 | function version() external pure returns (uint256) { 33 | // Not sure what this represents, but current Chainlink feeds use this value 34 | return 4; 35 | } 36 | 37 | function getRoundData(uint80 __roundId) 38 | external 39 | view 40 | returns ( 41 | uint80 _roundId, 42 | int256 _answer, 43 | uint256 _startedAt, 44 | uint256 _updatedAt, 45 | uint80 _answeredInRound 46 | ) 47 | { 48 | if (__roundId != LATEST_ROUND) revert OnlyLatestRoundIsAvailable(); 49 | return latestRoundData(); 50 | } 51 | 52 | function latestRoundData() 53 | public 54 | view 55 | returns ( 56 | uint80 _roundId, 57 | int256 _answer, 58 | uint256 _startedAt, 59 | uint256 _updatedAt, 60 | uint80 _answeredInRound 61 | ) 62 | { 63 | (_answer, _updatedAt) = _read(); 64 | _roundId = _answeredInRound = LATEST_ROUND; 65 | _startedAt = _updatedAt; 66 | } 67 | 68 | function latestAnswer() public view returns (int256 _value) { 69 | (_value, ) = _read(); 70 | } 71 | 72 | function latestTimestamp() public view returns (uint256 _timestamp) { 73 | (, _timestamp) = _read(); 74 | } 75 | 76 | function latestRound() external pure returns (uint256) { 77 | return LATEST_ROUND; 78 | } 79 | 80 | function getAnswer(uint256 _roundId) external view returns (int256) { 81 | if (_roundId != LATEST_ROUND) revert OnlyLatestRoundIsAvailable(); 82 | return latestAnswer(); 83 | } 84 | 85 | function getTimestamp(uint256 _roundId) external view returns (uint256) { 86 | if (_roundId != LATEST_ROUND) revert OnlyLatestRoundIsAvailable(); 87 | return latestTimestamp(); 88 | } 89 | 90 | function _read() internal view returns (int224 _value, uint32 _timestamp) { 91 | (_value, _timestamp) = API3_PROXY.read(); 92 | unchecked { 93 | // API3 uses 18 decimals, while Chainlink feeds might use a different amount 94 | _value /= int224(int256(_magnitudeConversion)); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /solidity/contracts/adapters/api3-chainlink-adapter/API3ChainlinkAdapterFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import './API3ChainlinkAdapter.sol'; 5 | 6 | contract API3ChainlinkAdapterFactory { 7 | /// @notice Emitted when a new adapter is deployed 8 | event AdapterCreated(API3ChainlinkAdapter adapter); 9 | 10 | function createAdapter( 11 | IProxy _api3Proxy, 12 | uint8 _decimals, 13 | string calldata _description 14 | ) external returns (API3ChainlinkAdapter _adapter) { 15 | _adapter = new API3ChainlinkAdapter{salt: bytes32(0)}(_api3Proxy, _decimals, _description); 16 | emit AdapterCreated(_adapter); 17 | } 18 | 19 | function computeAdapterAddress( 20 | IProxy _api3Proxy, 21 | uint8 _decimals, 22 | string calldata _description 23 | ) external view returns (address _adapter) { 24 | return 25 | _computeCreate2Address( 26 | keccak256( 27 | abi.encodePacked( 28 | // Deployment bytecode: 29 | type(API3ChainlinkAdapter).creationCode, 30 | // Constructor arguments: 31 | abi.encode(_api3Proxy, _decimals, _description) 32 | ) 33 | ) 34 | ); 35 | } 36 | 37 | function _computeCreate2Address(bytes32 _bytecodeHash) internal view virtual returns (address) { 38 | // Prefix: 39 | // Creator: 40 | // Salt: 41 | // Bytecode hash: 42 | return fromLast20Bytes(keccak256(abi.encodePacked(bytes1(0xFF), address(this), bytes32(0), _bytecodeHash))); 43 | } 44 | 45 | function fromLast20Bytes(bytes32 _bytesValue) internal pure returns (address) { 46 | // Convert the CREATE2 hash into an address. 47 | return address(uint160(uint256(_bytesValue))); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /solidity/contracts/adapters/dia-chainlink-adapter/DIAChainlinkAdapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '@chainlink/contracts/src/v0.8/interfaces/AggregatorV2V3Interface.sol'; 5 | 6 | interface IDIAOracleV2 { 7 | function getValue(string memory key) external view returns (uint128, uint128); 8 | } 9 | 10 | contract DIAChainlinkAdapter is AggregatorV2V3Interface { 11 | /// @notice Thrown when trying to query a round that is not the latest one 12 | error OnlyLatestRoundIsAvailable(); 13 | 14 | /// @notice The address of the underlying DIA Oracle 15 | address public immutable DIA_ORACLE; 16 | uint8 public immutable decimals; 17 | string public description; 18 | 19 | /// @notice The round number we'll use to represent the latest round 20 | uint80 internal constant LATEST_ROUND = 0; 21 | 22 | /// @notice Magnitude to convert DIA values to Chainlink values 23 | uint256 internal immutable _magnitudeConversion; 24 | 25 | bool internal immutable _decimalsGreaterThanOracle; 26 | 27 | constructor( 28 | address _diaOracle, 29 | uint8 _oracleDecimals, 30 | uint8 _feedDecimals, 31 | string memory _description 32 | ) { 33 | DIA_ORACLE = _diaOracle; 34 | decimals = _feedDecimals; 35 | description = _description; 36 | _decimalsGreaterThanOracle = decimals > _oracleDecimals; 37 | _magnitudeConversion = 10**(_decimalsGreaterThanOracle ? _feedDecimals - _oracleDecimals : _oracleDecimals - _feedDecimals); 38 | } 39 | 40 | function version() external pure returns (uint256) { 41 | // Not sure what this represents, but current Chainlink feeds use this value 42 | return 4; 43 | } 44 | 45 | function getRoundData(uint80 __roundId) 46 | external 47 | view 48 | returns ( 49 | uint80 _roundId, 50 | int256 _answer, 51 | uint256 _startedAt, 52 | uint256 _updatedAt, 53 | uint80 _answeredInRound 54 | ) 55 | { 56 | if (__roundId != LATEST_ROUND) revert OnlyLatestRoundIsAvailable(); 57 | return latestRoundData(); 58 | } 59 | 60 | function latestRoundData() 61 | public 62 | view 63 | returns ( 64 | uint80 _roundId, 65 | int256 _answer, 66 | uint256 _startedAt, 67 | uint256 _updatedAt, 68 | uint80 _answeredInRound 69 | ) 70 | { 71 | (_answer, _updatedAt) = _read(); 72 | _roundId = _answeredInRound = LATEST_ROUND; 73 | _startedAt = _updatedAt; 74 | } 75 | 76 | function latestAnswer() public view returns (int256 _value) { 77 | (_value, ) = _read(); 78 | } 79 | 80 | function latestTimestamp() public view returns (uint256 _timestamp) { 81 | (, _timestamp) = _read(); 82 | } 83 | 84 | function latestRound() external pure returns (uint256) { 85 | return LATEST_ROUND; 86 | } 87 | 88 | function getAnswer(uint256 _roundId) external view returns (int256) { 89 | if (_roundId != LATEST_ROUND) revert OnlyLatestRoundIsAvailable(); 90 | return latestAnswer(); 91 | } 92 | 93 | function getTimestamp(uint256 _roundId) external view returns (uint256) { 94 | if (_roundId != LATEST_ROUND) revert OnlyLatestRoundIsAvailable(); 95 | return latestTimestamp(); 96 | } 97 | 98 | function _read() internal view returns (int256 _value, uint256 _timestamp) { 99 | (uint128 __value, uint128 __timestamp) = IDIAOracleV2(DIA_ORACLE).getValue(description); 100 | unchecked { 101 | _value = _decimalsGreaterThanOracle 102 | ? (int256(int128(__value)) * int256(_magnitudeConversion)) 103 | : (int256(int128(__value)) / int256(_magnitudeConversion)); 104 | _timestamp = uint256(__timestamp); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /solidity/contracts/adapters/dia-chainlink-adapter/DIAChainlinkAdapterFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import './DIAChainlinkAdapter.sol'; 5 | 6 | contract DIAChainlinkAdapterFactory { 7 | /// @notice Emitted when a new adapter is deployed 8 | event AdapterCreated(DIAChainlinkAdapter adapter); 9 | 10 | function createAdapter( 11 | address _diaOracle, 12 | uint8 _oracleDecimals, 13 | uint8 _feedDecimals, 14 | string calldata _description 15 | ) external returns (DIAChainlinkAdapter _adapter) { 16 | _adapter = new DIAChainlinkAdapter{salt: bytes32(0)}(_diaOracle, _oracleDecimals, _feedDecimals, _description); 17 | emit AdapterCreated(_adapter); 18 | } 19 | 20 | function computeAdapterAddress( 21 | address _diaOracle, 22 | uint8 _oracleDecimals, 23 | uint8 _feedDecimals, 24 | string calldata _description 25 | ) external view returns (address _adapter) { 26 | return 27 | _computeCreate2Address( 28 | keccak256( 29 | abi.encodePacked( 30 | // Deployment bytecode: 31 | type(DIAChainlinkAdapter).creationCode, 32 | // Constructor arguments: 33 | abi.encode(_diaOracle, _oracleDecimals, _feedDecimals, _description) 34 | ) 35 | ) 36 | ); 37 | } 38 | 39 | function _computeCreate2Address(bytes32 _bytecodeHash) internal view virtual returns (address) { 40 | // Prefix: 41 | // Creator: 42 | // Salt: 43 | // Bytecode hash: 44 | return fromLast20Bytes(keccak256(abi.encodePacked(bytes1(0xFF), address(this), bytes32(0), _bytecodeHash))); 45 | } 46 | 47 | function fromLast20Bytes(bytes32 _bytesValue) internal pure returns (address) { 48 | // Convert the CREATE2 hash into an address. 49 | return address(uint160(uint256(_bytesValue))); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /solidity/contracts/base/BaseOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '@openzeppelin/contracts/utils/introspection/ERC165.sol'; 5 | import '@openzeppelin/contracts/utils/Multicall.sol'; 6 | import '../../interfaces/ITokenPriceOracle.sol'; 7 | 8 | /// @title A base implementation of `ITokenPriceOracle` that implements `ERC165` and `Multicall` 9 | abstract contract BaseOracle is Multicall, ERC165, ITokenPriceOracle { 10 | /// @inheritdoc IERC165 11 | function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { 12 | return 13 | _interfaceId == type(ITokenPriceOracle).interfaceId || 14 | _interfaceId == type(Multicall).interfaceId || 15 | super.supportsInterface(_interfaceId); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /solidity/contracts/base/SimpleOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import './BaseOracle.sol'; 5 | 6 | /** 7 | * @title A simple implementation of `BaseOracle` that already implements functions to add support 8 | * @notice Most implementations of `ITokenPriceOracle` will have an internal function that is called in both 9 | * `addSupportForPairIfNeeded` and `addOrModifySupportForPair`. This oracle is now making this explicit, and 10 | * implementing these two functions. They remain virtual so that they can be overriden if needed. 11 | */ 12 | abstract contract SimpleOracle is BaseOracle { 13 | ///@inheritdoc ITokenPriceOracle 14 | function isPairAlreadySupported(address tokenA, address tokenB) public view virtual returns (bool); 15 | 16 | ///@inheritdoc ITokenPriceOracle 17 | function addOrModifySupportForPair( 18 | address _tokenA, 19 | address _tokenB, 20 | bytes calldata _data 21 | ) external virtual { 22 | _addOrModifySupportForPair(_tokenA, _tokenB, _data); 23 | } 24 | 25 | ///@inheritdoc ITokenPriceOracle 26 | function addSupportForPairIfNeeded( 27 | address _tokenA, 28 | address _tokenB, 29 | bytes calldata _data 30 | ) external virtual { 31 | if (!isPairAlreadySupported(_tokenA, _tokenB)) { 32 | _addOrModifySupportForPair(_tokenA, _tokenB, _data); 33 | } 34 | } 35 | 36 | /** 37 | * @notice Add or reconfigures the support for a given pair. This function will let the oracle take some actions 38 | * to configure the pair, in preparation for future quotes. Can be called many times in order to let the oracle 39 | * re-configure for a new context 40 | * @dev Will revert if pair cannot be supported. tokenA and tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 41 | * @param tokenA One of the pair's tokens 42 | * @param tokenB The other of the pair's tokens 43 | * @param data Custom data that the oracle might need to operate 44 | */ 45 | function _addOrModifySupportForPair( 46 | address tokenA, 47 | address tokenB, 48 | bytes calldata data 49 | ) internal virtual; 50 | } 51 | -------------------------------------------------------------------------------- /solidity/contracts/libraries/TokenSorting.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >0.6; 3 | 4 | /** 5 | * @title TokenSorting library 6 | * @notice Provides functions to sort tokens easily 7 | */ 8 | library TokenSorting { 9 | /** 10 | * @notice Takes two tokens, and returns them sorted 11 | * @param _tokenA One of the tokens 12 | * @param _tokenB The other token 13 | * @return __tokenA The first of the tokens 14 | * @return __tokenB The second of the tokens 15 | */ 16 | function sortTokens(address _tokenA, address _tokenB) internal pure returns (address __tokenA, address __tokenB) { 17 | (__tokenA, __tokenB) = _tokenA < _tokenB ? (_tokenA, _tokenB) : (_tokenB, _tokenA); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /solidity/contracts/test/OracleAggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '../OracleAggregator.sol'; 5 | 6 | contract OracleAggregatorMock is OracleAggregator { 7 | struct InternalCall { 8 | bool wasCalled; 9 | bytes data; 10 | } 11 | 12 | mapping(address => mapping(address => InternalCall)) public internalAddOrModifyCalled; 13 | 14 | constructor( 15 | address[] memory _initialOracles, 16 | address _superAdmin, 17 | address[] memory _initialAdmins 18 | ) OracleAggregator(_initialOracles, _superAdmin, _initialAdmins) {} 19 | 20 | function internalAddOrModifySupportForPair( 21 | address _tokenA, 22 | address _tokenB, 23 | bytes calldata _data 24 | ) external { 25 | _addOrModifySupportForPair(_tokenA, _tokenB, _data); 26 | } 27 | 28 | function setOracle( 29 | address _tokenA, 30 | address _tokenB, 31 | ITokenPriceOracle _oracle, 32 | bool _forced 33 | ) external { 34 | _assignedOracle[_keyForPair(_tokenA, _tokenB)] = OracleAssignment({oracle: _oracle, forced: _forced}); 35 | } 36 | 37 | function _addOrModifySupportForPair( 38 | address _tokenA, 39 | address _tokenB, 40 | bytes calldata _data 41 | ) internal override { 42 | (address __tokenA, address __tokenB) = TokenSorting.sortTokens(_tokenA, _tokenB); 43 | internalAddOrModifyCalled[__tokenA][__tokenB] = InternalCall({wasCalled: true, data: _data}); 44 | super._addOrModifySupportForPair(_tokenA, _tokenB, _data); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /solidity/contracts/test/StatefulChainlinkOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity >=0.8.7 <0.9.0; 4 | 5 | import '../StatefulChainlinkOracle.sol'; 6 | 7 | contract StatefulChainlinkOracleMock is StatefulChainlinkOracle { 8 | struct MockedPricingPlan { 9 | PricingPlan plan; 10 | bool isSet; 11 | } 12 | 13 | mapping(address => mapping(address => MockedPricingPlan)) private _pricingPlan; 14 | 15 | constructor( 16 | FeedRegistryInterface _registry, 17 | address _superAdmin, 18 | address[] memory _initialAdmins 19 | ) StatefulChainlinkOracle(_registry, _superAdmin, _initialAdmins) {} 20 | 21 | function internalAddOrModifySupportForPair( 22 | address _tokenA, 23 | address _tokenB, 24 | bytes calldata _data 25 | ) external { 26 | _addOrModifySupportForPair(_tokenA, _tokenB, _data); 27 | } 28 | 29 | function determinePricingPlan( 30 | address _tokenA, 31 | address _tokenB, 32 | PricingPlan _plan 33 | ) external { 34 | (address __tokenA, address __tokenB) = TokenSorting.sortTokens(_tokenA, _tokenB); 35 | _pricingPlan[__tokenA][__tokenB] = MockedPricingPlan({plan: _plan, isSet: true}); 36 | } 37 | 38 | function intercalCallRegistry(address _quote, address _base) external view returns (uint256) { 39 | return _callRegistry(_quote, _base); 40 | } 41 | 42 | function setPlanForPair( 43 | address _tokenA, 44 | address _tokenB, 45 | PricingPlan _plan 46 | ) external { 47 | (address __tokenA, address __tokenB) = TokenSorting.sortTokens(_tokenA, _tokenB); 48 | _planForPair[_keyForSortedPair(__tokenA, __tokenB)] = _plan; 49 | } 50 | 51 | function _determinePricingPlan(address _tokenA, address _tokenB) internal view override returns (PricingPlan) { 52 | (address __tokenA, address __tokenB) = TokenSorting.sortTokens(_tokenA, _tokenB); 53 | MockedPricingPlan memory _plan = _pricingPlan[__tokenA][__tokenB]; 54 | if (_plan.isSet) { 55 | return _plan.plan; 56 | } else { 57 | return super._determinePricingPlan(__tokenA, __tokenB); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /solidity/contracts/test/TransformerOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '../TransformerOracle.sol'; 5 | 6 | contract TransformerOracleMock is TransformerOracle { 7 | mapping(address => mapping(address => address[])) internal _mappingForPair; 8 | mapping(address => mapping(address => ITransformer[])) internal _transformersForPair; 9 | 10 | constructor( 11 | ITransformerRegistry _registry, 12 | ITokenPriceOracle _underlyingOracle, 13 | address _superAdmin, 14 | address[] memory _initialAdmins 15 | ) TransformerOracle(_registry, _underlyingOracle, _superAdmin, _initialAdmins) {} 16 | 17 | function setMappingForPair( 18 | address _tokenA, 19 | address _tokenB, 20 | address _mappedTokenA, 21 | address _mappedTokenB 22 | ) external { 23 | _mappingForPair[_tokenA][_tokenB].push(_mappedTokenA); 24 | _mappingForPair[_tokenA][_tokenB].push(_mappedTokenB); 25 | } 26 | 27 | function setTransformersForPair( 28 | address _tokenA, 29 | address _tokenB, 30 | ITransformer _transformerTokenA, 31 | ITransformer _transformerTokenB 32 | ) external { 33 | _transformersForPair[_tokenA][_tokenB] = [_transformerTokenA, _transformerTokenB]; 34 | } 35 | 36 | function internalFetchTransformers( 37 | address _tokenA, 38 | address _tokenB, 39 | bool _shouldCheckA, 40 | bool _shouldCheckB 41 | ) external view returns (ITransformer _transformerTokenA, ITransformer _transformerTokenB) { 42 | return _fetchTransformers(_tokenA, _tokenB, _shouldCheckA, _shouldCheckB); 43 | } 44 | 45 | function internalHideTransformersBasedOnConfig( 46 | address _tokenA, 47 | address _tokenB, 48 | ITransformer _transformerTokenA, 49 | ITransformer _transformerTokenB 50 | ) external view returns (ITransformer, ITransformer) { 51 | return _hideTransformersBasedOnConfig(_tokenA, _tokenB, _transformerTokenA, _transformerTokenB); 52 | } 53 | 54 | function _getTransformers( 55 | address _tokenA, 56 | address _tokenB, 57 | bool _shouldCheckA, 58 | bool _shouldCheckB 59 | ) internal view override returns (ITransformer _transformerTokenA, ITransformer _transformerTokenB) { 60 | ITransformer[] memory _transformers = _transformersForPair[_tokenA][_tokenB]; 61 | if (_transformers.length > 0) { 62 | return (_transformers[0], _transformers[1]); 63 | } else { 64 | return super._getTransformers(_tokenA, _tokenB, _shouldCheckA, _shouldCheckB); 65 | } 66 | } 67 | 68 | function getMappingForPair(address _tokenA, address _tokenB) public view override returns (address _mappedTokenA, address _mappedTokenB) { 69 | address[] memory _mapping = _mappingForPair[_tokenA][_tokenB]; 70 | if (_mapping.length == 0) { 71 | return super.getMappingForPair(_tokenA, _tokenB); 72 | } else { 73 | return (_mapping[0], _mapping[1]); 74 | } 75 | } 76 | 77 | function getRecursiveMappingForPair(address _tokenA, address _tokenB) 78 | public 79 | view 80 | override 81 | returns (address _mappedTokenA, address _mappedTokenB) 82 | { 83 | address[] memory _mapping = _mappingForPair[_tokenA][_tokenB]; 84 | if (_mapping.length == 0) { 85 | return super.getRecursiveMappingForPair(_tokenA, _tokenB); 86 | } else { 87 | return (_mapping[0], _mapping[1]); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /solidity/contracts/test/UniswapV3Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity >=0.8.7 <0.9.0; 4 | 5 | contract UniswapV3PoolMock { 6 | uint32 public immutable gasPerCardinality; 7 | 8 | constructor(uint32 _gasPerCardinality) { 9 | gasPerCardinality = _gasPerCardinality; 10 | } 11 | 12 | function increaseObservationCardinalityNext(uint16 _observationCardinalityNext) external view { 13 | _burnGas(_observationCardinalityNext * gasPerCardinality); 14 | } 15 | 16 | function _burnGas(uint256 _amountToBurn) internal view { 17 | assembly { 18 | let ptr := mload(0x40) 19 | mstore(ptr, shl(224, _amountToBurn)) 20 | let success := staticcall(gas(), 9, ptr, 213, 0, 0) 21 | if iszero(success) { 22 | revert(0, 0) 23 | } 24 | } 25 | } 26 | 27 | function slot0() 28 | external 29 | view 30 | returns ( 31 | uint160 sqrtPriceX96, 32 | int24 tick, 33 | uint16 observationIndex, 34 | uint16 observationCardinality, 35 | uint16 observationCardinalityNext, 36 | uint8 feeProtocol, 37 | bool unlocked 38 | ) 39 | {} 40 | } 41 | -------------------------------------------------------------------------------- /solidity/contracts/test/adapters/UniswapV3Adapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '../../adapters/UniswapV3Adapter.sol'; 5 | 6 | contract UniswapV3AdapterMock is UniswapV3Adapter { 7 | constructor(InitialConfig memory _initialConfig) UniswapV3Adapter(_initialConfig) {} 8 | 9 | mapping(address => mapping(address => address[])) private _allPoolsSorted; 10 | bool _sortedPoolsSet; 11 | 12 | function internalAddOrModifySupportForPair( 13 | address _tokenA, 14 | address _tokenB, 15 | bytes calldata _data 16 | ) external { 17 | _addOrModifySupportForPair(_tokenA, _tokenB, _data); 18 | } 19 | 20 | function internalGetAllPoolsSortedByLiquidity(address _tokenA, address _tokenB) external view returns (address[] memory) { 21 | return _getAllPoolsSortedByLiquidity(_tokenA, _tokenB); 22 | } 23 | 24 | function _getAllPoolsSortedByLiquidity(address _tokenA, address _tokenB) internal view override returns (address[] memory _pools) { 25 | if (_sortedPoolsSet) { 26 | return _allPoolsSorted[_tokenA][_tokenB]; 27 | } else { 28 | return super._getAllPoolsSortedByLiquidity(_tokenA, _tokenB); 29 | } 30 | } 31 | 32 | function setAvailablePools( 33 | address _tokenA, 34 | address _tokenB, 35 | address[] calldata _available 36 | ) external { 37 | delete _allPoolsSorted[_tokenA][_tokenB]; 38 | for (uint256 i = 0; i < _available.length; i++) { 39 | _allPoolsSorted[_tokenA][_tokenB].push(_available[i]); 40 | } 41 | _sortedPoolsSet = true; 42 | } 43 | 44 | function setPools( 45 | address _tokenA, 46 | address _tokenB, 47 | address[] calldata _pools 48 | ) external { 49 | address[] storage _storagePools = _poolsForPair[_keyForPair(_tokenA, _tokenB)]; 50 | for (uint256 i; i < _pools.length; i++) { 51 | _storagePools.push(_pools[i]); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /solidity/contracts/test/base/SimpleOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '../../base/SimpleOracle.sol'; 5 | 6 | contract SimpleOracleMock is SimpleOracle { 7 | error NotImplemented(); 8 | 9 | struct InternalAddSupportCall { 10 | address tokenA; 11 | address tokenB; 12 | bytes data; 13 | } 14 | 15 | mapping(address => mapping(address => bool)) private _isPairAlreadySupported; 16 | InternalAddSupportCall public lastCall; 17 | 18 | function canSupportPair(address, address) external pure returns (bool) { 19 | revert NotImplemented(); 20 | } 21 | 22 | function isPairAlreadySupported(address _tokenA, address _tokenB) public view override returns (bool) { 23 | return _isPairAlreadySupported[_tokenA][_tokenB]; 24 | } 25 | 26 | function quote( 27 | address, 28 | uint256, 29 | address, 30 | bytes calldata 31 | ) external pure returns (uint256) { 32 | revert NotImplemented(); 33 | } 34 | 35 | function _addOrModifySupportForPair( 36 | address _tokenA, 37 | address _tokenB, 38 | bytes calldata _data 39 | ) internal override { 40 | require(lastCall.tokenA == address(0), 'Already called'); 41 | lastCall = InternalAddSupportCall(_tokenA, _tokenB, _data); 42 | } 43 | 44 | function setPairAlreadySupported(address _tokenA, address _tokenB) external { 45 | _isPairAlreadySupported[_tokenA][_tokenB] = true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /solidity/interfaces/IOracleAggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | import './ITokenPriceOracle.sol'; 5 | 6 | /** 7 | * @title An implementation of `ITokenPriceOracle` that aggregates two or more oracles. It's important to 8 | * note that this oracle is permissioned. Admins can determine available oracles and they can 9 | * also force an oracle for a specific pair 10 | * @notice This oracle will use two or more oracles to support price quotes 11 | */ 12 | interface IOracleAggregator is ITokenPriceOracle { 13 | /// @notice An oracle's assignment for a specific pair 14 | struct OracleAssignment { 15 | // The oracle's address 16 | ITokenPriceOracle oracle; 17 | // Whether the oracle was forced by an admin. If forced, only an admin can modify it 18 | bool forced; 19 | } 20 | 21 | /// @notice Thrown when one of the parameters is a zero address 22 | error ZeroAddress(); 23 | 24 | /** 25 | * @notice Thrown when trying to register an address that is not an oracle 26 | * @param notOracle The address that was not a oracle 27 | */ 28 | error AddressIsNotOracle(address notOracle); 29 | 30 | /** 31 | * @notice Emitted when the list of oracles is updated 32 | * @param oracles The new list of oracles 33 | */ 34 | event OracleListUpdated(address[] oracles); 35 | 36 | /** 37 | * @notice Emitted when an oracle is assigned to a pair 38 | * @param tokenA One of the pair's tokens 39 | * @param tokenB The other of the pair's tokens 40 | * @param oracle The oracle that was assigned to the pair 41 | */ 42 | event OracleAssigned(address tokenA, address tokenB, ITokenPriceOracle oracle); 43 | 44 | /** 45 | * @notice Returns the assigned oracle (or the zero address if there isn't one) for the given pair 46 | * @dev tokenA and tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 47 | * @param tokenA One of the pair's tokens 48 | * @param tokenB The other of the pair's tokens 49 | * @return The assigned oracle for the given pair 50 | */ 51 | function assignedOracle(address tokenA, address tokenB) external view returns (OracleAssignment memory); 52 | 53 | /** 54 | * @notice Returns whether this oracle can support the given pair of tokens 55 | * @return Whether the given pair of tokens can be supported by the oracle 56 | */ 57 | function availableOracles() external view returns (ITokenPriceOracle[] memory); 58 | 59 | /** 60 | * @notice Returns the oracle that would be assigned to the pair if `addOrModifySupportForPair` 61 | * was called by the same caller 62 | * @dev tokenA and tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 63 | * @param tokenA One of the pair's tokens 64 | * @param tokenB The other of the pair's tokens 65 | * @return The oracle that would be assigned (or the zero address if none could be assigned) 66 | */ 67 | function previewAddOrModifySupportForPair(address tokenA, address tokenB) external view returns (ITokenPriceOracle); 68 | 69 | /** 70 | * @notice Sets a new oracle for the given pair. After it's sent, only other admins will be able 71 | * to modify the pair's oracle 72 | * @dev Can only be called by users with the admin role 73 | * tokenA and tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 74 | * @param tokenA One of the pair's tokens 75 | * @param tokenB The other of the pair's tokens 76 | * @param oracle The oracle to set 77 | * @param data Custom data that the oracle might need to operate 78 | */ 79 | function forceOracle( 80 | address tokenA, 81 | address tokenB, 82 | address oracle, 83 | bytes calldata data 84 | ) external; 85 | 86 | /** 87 | * @notice Sets a new list of oracles to be used by the aggregator 88 | * @dev Can only be called by users with the admin role 89 | * @param oracles The new list of oracles to set 90 | */ 91 | function setAvailableOracles(address[] calldata oracles) external; 92 | } 93 | -------------------------------------------------------------------------------- /solidity/interfaces/IStatefulChainlinkOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | import '@chainlink/contracts/src/v0.8/interfaces/FeedRegistryInterface.sol'; 5 | import './ITokenPriceOracle.sol'; 6 | 7 | /** 8 | * @title An implementation of IPriceOracle that uses Chainlink feeds 9 | * @notice This oracle will attempt to use all available feeds to determine prices between pairs 10 | */ 11 | interface IStatefulChainlinkOracle is ITokenPriceOracle { 12 | /// @notice The plan that will be used to calculate quotes for a given pair 13 | enum PricingPlan { 14 | // There is no plan calculated 15 | NONE, 16 | // Will use the ETH/USD feed 17 | ETH_USD_PAIR, 18 | // Will use a token/USD feed 19 | TOKEN_USD_PAIR, 20 | // Will use a token/ETH feed 21 | TOKEN_ETH_PAIR, 22 | // Will use tokenIn/USD and tokenOut/USD feeds 23 | TOKEN_TO_USD_TO_TOKEN_PAIR, 24 | // Will use tokenIn/ETH and tokenOut/ETH feeds 25 | TOKEN_TO_ETH_TO_TOKEN_PAIR, 26 | // Will use tokenA/USD, tokenB/ETH and ETH/USD feeds 27 | TOKEN_A_TO_USD_TO_ETH_TO_TOKEN_B, 28 | // Will use tokenA/ETH, tokenB/USD and ETH/USD feeds 29 | TOKEN_A_TO_ETH_TO_USD_TO_TOKEN_B, 30 | // Used then tokenA is the same as tokenB 31 | SAME_TOKENS 32 | } 33 | 34 | /** 35 | * @notice Emitted when the oracle updated the pricing plan for a pair 36 | * @param tokenA One of the pair's tokens 37 | * @param tokenB The other of the pair's tokens 38 | * @param plan The new plan 39 | */ 40 | event UpdatedPlanForPair(address tokenA, address tokenB, PricingPlan plan); 41 | 42 | /** 43 | * @notice Emitted when new tokens are considered USD 44 | * @param tokens The new tokens 45 | */ 46 | event TokensConsideredUSD(address[] tokens); 47 | 48 | /** 49 | * @notice Emitted when tokens should no longer be considered USD 50 | * @param tokens The tokens to no longer consider USD 51 | */ 52 | event TokensNoLongerConsideredUSD(address[] tokens); 53 | 54 | /** 55 | * @notice Emitted when new mappings are added 56 | * @param tokens The tokens 57 | * @param mappings Their new mappings 58 | */ 59 | event MappingsAdded(address[] tokens, address[] mappings); 60 | 61 | /// @notice Thrown when the price is non-positive 62 | error InvalidPrice(); 63 | 64 | /// @notice Thrown when the last price update was too long ago 65 | error LastUpdateIsTooOld(); 66 | 67 | /// @notice Thrown when one of the parameters is a zero address 68 | error ZeroAddress(); 69 | 70 | /// @notice Thrown when the input for adding mappings in invalid 71 | error InvalidMappingsInput(); 72 | 73 | /** 74 | * @notice Returns how old the last price update can be before the oracle reverts by considering it too old 75 | * @dev Cannot be modified 76 | * @return How old the last price update can be in seconds 77 | */ 78 | function MAX_DELAY() external view returns (uint32); 79 | 80 | /** 81 | * @notice Returns the Chainlink feed registry 82 | * @return The Chainlink registry 83 | */ 84 | function registry() external view returns (FeedRegistryInterface); 85 | 86 | /** 87 | * @notice Returns the pricing plan that will be used when quoting the given pair 88 | * @dev tokenA and tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 89 | * @return The pricing plan that will be used 90 | */ 91 | function planForPair(address tokenA, address tokenB) external view returns (PricingPlan); 92 | 93 | /** 94 | * @notice Returns the mapping of the given token, if it exists. If it doesn't, then the original token is returned 95 | * @return If it exists, the mapping is returned. Otherwise, the original token is returned 96 | */ 97 | function mappedToken(address token) external view returns (address); 98 | 99 | /** 100 | * @notice Adds new token mappings 101 | * @param addresses The addresses of the tokens 102 | * @param mappings The addresses of their mappings 103 | */ 104 | function addMappings(address[] calldata addresses, address[] calldata mappings) external; 105 | } 106 | -------------------------------------------------------------------------------- /solidity/interfaces/ITokenPriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | /** 5 | * @title The interface for an oracle that provides price quotes 6 | * @notice These methods allow users to add support for pairs, and then ask for quotes 7 | */ 8 | interface ITokenPriceOracle { 9 | /// @notice Thrown when trying to add support for a pair that cannot be supported 10 | error PairCannotBeSupported(address tokenA, address tokenB); 11 | 12 | /// @notice Thrown when trying to execute a quote with a pair that isn't supported yet 13 | error PairNotSupportedYet(address tokenA, address tokenB); 14 | 15 | /** 16 | * @notice Returns whether this oracle can support the given pair of tokens 17 | * @dev tokenA and tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 18 | * @param tokenA One of the pair's tokens 19 | * @param tokenB The other of the pair's tokens 20 | * @return Whether the given pair of tokens can be supported by the oracle 21 | */ 22 | function canSupportPair(address tokenA, address tokenB) external view returns (bool); 23 | 24 | /** 25 | * @notice Returns whether this oracle is already supporting the given pair of tokens 26 | * @dev tokenA and tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 27 | * @param tokenA One of the pair's tokens 28 | * @param tokenB The other of the pair's tokens 29 | * @return Whether the given pair of tokens is already being supported by the oracle 30 | */ 31 | function isPairAlreadySupported(address tokenA, address tokenB) external view returns (bool); 32 | 33 | /** 34 | * @notice Returns a quote, based on the given tokens and amount 35 | * @dev Will revert if pair isn't supported 36 | * @param tokenIn The token that will be provided 37 | * @param amountIn The amount that will be provided 38 | * @param tokenOut The token we would like to quote 39 | * @param data Custom data that the oracle might need to operate 40 | * @return amountOut How much `tokenOut` will be returned in exchange for `amountIn` amount of `tokenIn` 41 | */ 42 | function quote( 43 | address tokenIn, 44 | uint256 amountIn, 45 | address tokenOut, 46 | bytes calldata data 47 | ) external view returns (uint256 amountOut); 48 | 49 | /** 50 | * @notice Add or reconfigures the support for a given pair. This function will let the oracle take some actions 51 | * to configure the pair, in preparation for future quotes. Can be called many times in order to let the oracle 52 | * re-configure for a new context 53 | * @dev Will revert if pair cannot be supported. tokenA and tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 54 | * @param tokenA One of the pair's tokens 55 | * @param tokenB The other of the pair's tokens 56 | * @param data Custom data that the oracle might need to operate 57 | */ 58 | function addOrModifySupportForPair( 59 | address tokenA, 60 | address tokenB, 61 | bytes calldata data 62 | ) external; 63 | 64 | /** 65 | * @notice Adds support for a given pair if the oracle didn't support it already. If called for a pair that is already supported, 66 | * then nothing will happen. This function will let the oracle take some actions to configure the pair, in preparation 67 | * for future quotes 68 | * @dev Will revert if pair cannot be supported. tokenA and tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 69 | * @param tokenA One of the pair's tokens 70 | * @param tokenB The other of the pair's tokens 71 | * @param data Custom data that the oracle might need to operate 72 | */ 73 | function addSupportForPairIfNeeded( 74 | address tokenA, 75 | address tokenB, 76 | bytes calldata data 77 | ) external; 78 | } 79 | -------------------------------------------------------------------------------- /solidity/interfaces/ITransformerOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | import '@mean-finance/transformers/solidity/interfaces/ITransformerRegistry.sol'; 5 | import './ITokenPriceOracle.sol'; 6 | 7 | /** 8 | * @title An implementation of `ITokenPriceOracle` that handles transformations between tokens 9 | * @notice This oracle takes the transformer registry, and will transform some dependent tokens into their underlying 10 | * tokens before quoting. We do this because it's hard to quote `yield-bearing(USDC) => yield-bearing(ETH)`. 11 | * But we can easily do something like `yield-bearing(USDC) => USDC => ETH => yield-bearing(ETH)`. So the 12 | * idea is to use the transformer registry to transform between dependent and their underlying, and then 13 | * quote the underlyings. 14 | */ 15 | interface ITransformerOracle is ITokenPriceOracle { 16 | /// @notice How a specific pair will be mapped to their underlying tokens 17 | struct PairSpecificMappingConfig { 18 | // Whether tokenA will be mapped to its underlying (tokenA < tokenB) 19 | bool mapTokenAToUnderlying; 20 | // Whether tokenB will be mapped to its underlying (tokenA < tokenB) 21 | bool mapTokenBToUnderlying; 22 | // Whether the config is set 23 | bool isSet; 24 | } 25 | 26 | /// @notice Pair-specifig mapping configuration to set 27 | struct PairSpecificMappingConfigToSet { 28 | // One of the pair's tokens 29 | address tokenA; 30 | // The other of the pair's tokens 31 | address tokenB; 32 | // Whether to map tokenA to its underlying 33 | bool mapTokenAToUnderlying; 34 | // Whether to map tokenB to its underlying 35 | bool mapTokenBToUnderlying; 36 | } 37 | 38 | /// @notice A pair of tokens 39 | struct Pair { 40 | // One of the pair's tokens 41 | address tokenA; 42 | // The other of the pair's tokens 43 | address tokenB; 44 | } 45 | 46 | /// @notice Thrown when a parameter is the zero address 47 | error ZeroAddress(); 48 | 49 | /** 50 | * @notice Emitted when new dependents are set to avoid mapping to their underlying counterparts 51 | * @param dependents The tokens that will avoid mapping 52 | */ 53 | event DependentsWillAvoidMappingToUnderlying(address[] dependents); 54 | 55 | /** 56 | * @notice Emitted when dependents are set to map to their underlying counterparts 57 | * @param dependents The tokens that will map to underlying 58 | */ 59 | event DependentsWillMapToUnderlying(address[] dependents); 60 | 61 | /** 62 | * @notice Emitted when dependents pair-specific mapping config is set 63 | * @param config The config that was set 64 | */ 65 | event PairSpecificConfigSet(PairSpecificMappingConfigToSet[] config); 66 | 67 | /** 68 | * @notice Emitted when dependents pair-specific mapping config is cleared 69 | * @param pairs The pairs that had their config cleared 70 | */ 71 | event PairSpecificConfigCleared(Pair[] pairs); 72 | 73 | /** 74 | * @notice Returns the address of the transformer registry 75 | * @dev Cannot be modified 76 | * @return The address of the transformer registry 77 | */ 78 | function REGISTRY() external view returns (ITransformerRegistry); 79 | 80 | /** 81 | * @notice Returns the address of the underlying oracle 82 | * @dev Cannot be modified 83 | * @return The address of the underlying oracle 84 | */ 85 | function UNDERLYING_ORACLE() external view returns (ITokenPriceOracle); 86 | 87 | /** 88 | * @notice Returns whether the given dependent will avoid mapping to their underlying counterparts 89 | * @param dependent The dependent token to check 90 | * @return Whether the given dependent will avoid mapping to their underlying counterparts 91 | */ 92 | function willAvoidMappingToUnderlying(address dependent) external view returns (bool); 93 | 94 | /** 95 | * @notice Takes a pair of tokens, and maps them to their underlying counterparts if they exist, and if they 96 | * haven't been configured to avoid mapping. Pair-specific config will be prioritized, but if it isn't 97 | * set, then global config will be used. 98 | * @param tokenA One of the pair's tokens 99 | * @param tokenB The other of the pair's tokens 100 | * @return mappedTokenA tokenA's underlying token, if exists and isn't configured to avoid mapping. 101 | * Otherwise tokenA 102 | * @return mappedTokenB tokenB's underlying token, if exists and isn't configured to avoid mapping. 103 | * Otherwise tokenB 104 | */ 105 | function getMappingForPair(address tokenA, address tokenB) external view returns (address mappedTokenA, address mappedTokenB); 106 | 107 | /** 108 | * @notice Very similar to `getMappingForPair`, but recursive. Since an underlying could have an underlying, we might need to map 109 | * the given pair recursively 110 | */ 111 | function getRecursiveMappingForPair(address tokenA, address tokenB) external view returns (address mappedTokenA, address mappedTokenB); 112 | 113 | /** 114 | * @notice Returns any pair-specific mapping configuration for the given tokens 115 | * @dev tokenA and tokenB may be passed in either tokenA/tokenB or tokenB/tokenA order 116 | * @param tokenA One of the pair's tokens 117 | * @param tokenB The other of the pair's tokens 118 | */ 119 | function pairSpecificMappingConfig(address tokenA, address tokenB) external view returns (PairSpecificMappingConfig memory); 120 | 121 | /** 122 | * @notice Determines that the given dependents will avoid mapping to their underlying counterparts, and 123 | * instead perform quotes with their own addreses. This comes in handy with situations such as 124 | * ETH/WETH, where some oracles use WETH instead of ETH 125 | * @param dependents The dependent tokens that should avoid mapping to underlying 126 | */ 127 | function avoidMappingToUnderlying(address[] calldata dependents) external; 128 | 129 | /** 130 | * @notice Determines that the given dependents go back to mapping to their underlying counterparts (the 131 | * default behaviour) 132 | * @param dependents The dependent tokens that should go back to mapping to underlying 133 | */ 134 | function shouldMapToUnderlying(address[] calldata dependents) external; 135 | 136 | /** 137 | * @notice Determines how the given pairs should be mapped to their underlying tokens 138 | * @param config A list of pairs to configure 139 | */ 140 | function setPairSpecificMappingConfig(PairSpecificMappingConfigToSet[] calldata config) external; 141 | 142 | /** 143 | * @notice Cleares any pair-specific mapping config for the given list of pairs 144 | * @param pairs The pairs that will have their config cleared 145 | */ 146 | function clearPairSpecificMappingConfig(Pair[] calldata pairs) external; 147 | } 148 | -------------------------------------------------------------------------------- /solidity/interfaces/adapters/IUniswapV3Adapter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.7 <0.9.0; 3 | 4 | import '@mean-finance/uniswap-v3-oracle/solidity/interfaces/IStaticOracle.sol'; 5 | import '../ITokenPriceOracle.sol'; 6 | 7 | interface IUniswapV3Adapter is ITokenPriceOracle { 8 | /// @notice The initial adapter's configuration 9 | struct InitialConfig { 10 | IStaticOracle uniswapV3Oracle; 11 | uint32 maxPeriod; 12 | uint32 minPeriod; 13 | uint32 initialPeriod; 14 | address superAdmin; 15 | address[] initialAdmins; 16 | } 17 | 18 | /// @notice A pair of tokens 19 | struct Pair { 20 | address tokenA; 21 | address tokenB; 22 | } 23 | 24 | /** 25 | * @notice Emitted when a new period is set 26 | * @param period The new period 27 | */ 28 | event PeriodChanged(uint32 period); 29 | 30 | /** 31 | * @notice Emitted when a new cardinality per minute is set 32 | * @param cardinalityPerMinute The new cardinality per minute 33 | */ 34 | event CardinalityPerMinuteChanged(uint8 cardinalityPerMinute); 35 | 36 | /** 37 | * @notice Emitted when a new gas cost per cardinality is set 38 | * @param gasPerCardinality The new gas per cardinality 39 | */ 40 | event GasPerCardinalityChanged(uint104 gasPerCardinality); 41 | 42 | /** 43 | * @notice Emitted when a new gas cost to support pools is set 44 | * @param gasCostToSupportPool The new gas cost 45 | */ 46 | event GasCostToSupportPoolChanged(uint112 gasCostToSupportPool); 47 | 48 | /** 49 | * @notice Emitted when the denylist status is updated for some pairs 50 | * @param pairs The pairs that were updated 51 | * @param denylisted Whether they will be denylisted or not 52 | */ 53 | event DenylistChanged(Pair[] pairs, bool[] denylisted); 54 | 55 | /** 56 | * @notice Emitted when support is updated (added or modified) for a new pair 57 | * @param tokenA One of the pair's tokens 58 | * @param tokenB The other of the pair's tokens 59 | * @param preparedPools The amount of pools that were prepared to support the pair 60 | */ 61 | event UpdatedSupport(address tokenA, address tokenB, uint256 preparedPools); 62 | 63 | /// @notice Thrown when one of the parameters is the zero address 64 | error ZeroAddress(); 65 | 66 | /// @notice Thrown when trying to set an invalid period 67 | error InvalidPeriod(uint32 period); 68 | 69 | /// @notice Thrown when trying to set an invalid cardinality 70 | error InvalidCardinalityPerMinute(); 71 | 72 | /// @notice Thrown when trying to set an invalid gas cost per cardinality 73 | error InvalidGasPerCardinality(); 74 | 75 | /// @notice Thrown when trying to set an invalid gas cost to support a pools 76 | error InvalidGasCostToSupportPool(); 77 | 78 | /// @notice Thrown when trying to set a denylist but the given parameters are invalid 79 | error InvalidDenylistParams(); 80 | 81 | /// @notice Thrown when the gas limit is so low that no pools can be initialized 82 | error GasTooLow(); 83 | 84 | /** 85 | * @notice Returns the address of the Uniswap oracle 86 | * @dev Cannot be modified 87 | * @return The address of the Uniswap oracle 88 | */ 89 | function UNISWAP_V3_ORACLE() external view returns (IStaticOracle); 90 | 91 | /** 92 | * @notice Returns the maximum possible period 93 | * @dev Cannot be modified 94 | * @return The maximum possible period 95 | */ 96 | function MAX_PERIOD() external view returns (uint32); 97 | 98 | /** 99 | * @notice Returns the minimum possible period 100 | * @dev Cannot be modified 101 | * @return The minimum possible period 102 | */ 103 | function MIN_PERIOD() external view returns (uint32); 104 | 105 | /** 106 | * @notice Returns the period used for the TWAP calculation 107 | * @return The period used for the TWAP 108 | */ 109 | function period() external view returns (uint32); 110 | 111 | /** 112 | * @notice Returns the cardinality per minute used for adding support to pairs 113 | * @return The cardinality per minute used for increase cardinality calculations 114 | */ 115 | function cardinalityPerMinute() external view returns (uint8); 116 | 117 | /** 118 | * @notice Returns the approximate gas cost per each increased cardinality 119 | * @return The gas cost per cardinality increase 120 | */ 121 | function gasPerCardinality() external view returns (uint104); 122 | 123 | /** 124 | * @notice Returns the approximate gas cost to add support for a new pool internally 125 | * @return The gas cost to support a new pool 126 | */ 127 | function gasCostToSupportPool() external view returns (uint112); 128 | 129 | /** 130 | * @notice Returns whether the given pair is denylisted or not 131 | * @param tokenA One of the pair's tokens 132 | * @param tokenB The other of the pair's tokens 133 | * @return Whether the given pair is denylisted or not 134 | */ 135 | function isPairDenylisted(address tokenA, address tokenB) external view returns (bool); 136 | 137 | /** 138 | * @notice When a pair is added to the oracle adapter, we will prepare all pools for the pair. Now, it could 139 | * happen that certain pools are added for the pair at a later stage, and we can't be sure if those pools 140 | * will be configured correctly. So be basically store the pools that ready for sure, and use only those 141 | * for quotes. This functions returns this list of pools known to be prepared 142 | * @param tokenA One of the pair's tokens 143 | * @param tokenB The other of the pair's tokens 144 | * @return The list of pools that will be used for quoting 145 | */ 146 | function getPoolsPreparedForPair(address tokenA, address tokenB) external view returns (address[] memory); 147 | 148 | /** 149 | * @notice Sets the period to be used for the TWAP calculation 150 | * @dev Will revert it is lower than the minimum period or greater than maximum period. 151 | * Can only be called by users with the admin role 152 | * WARNING: increasing the period could cause big problems, because Uniswap V3 pools might not support a TWAP so old 153 | * @param newPeriod The new period 154 | */ 155 | function setPeriod(uint32 newPeriod) external; 156 | 157 | /** 158 | * @notice Sets the cardinality per minute to be used when increasing observation cardinality at the moment of adding support for pairs 159 | * @dev Will revert if the given cardinality is zero 160 | * Can only be called by users with the admin role 161 | * WARNING: increasing the cardinality per minute will make adding support to a pair significantly costly 162 | * @param cardinalityPerMinute The new cardinality per minute 163 | */ 164 | function setCardinalityPerMinute(uint8 cardinalityPerMinute) external; 165 | 166 | /** 167 | * @notice Sets the gas cost per cardinality 168 | * @dev Will revert if the given gas cost is zero 169 | * Can only be called by users with the admin role 170 | * @param gasPerCardinality The gas cost to set 171 | */ 172 | function setGasPerCardinality(uint104 gasPerCardinality) external; 173 | 174 | /** 175 | * @notice Sets the gas cost to support a new pool 176 | * @dev Will revert if the given gas cost is zero 177 | * Can only be called by users with the admin role 178 | * @param gasCostToSupportPool The gas cost to set 179 | */ 180 | function setGasCostToSupportPool(uint112 gasCostToSupportPool) external; 181 | 182 | /** 183 | * @notice Sets the denylist status for a set of pairs 184 | * @dev Will revert if amount of pairs does not match the amount of bools 185 | * Can only be called by users with the admin role 186 | * @param pairs The pairs to update 187 | * @param denylisted Whether they will be denylisted or not 188 | */ 189 | function setDenylisted(Pair[] calldata pairs, bool[] calldata denylisted) external; 190 | } 191 | -------------------------------------------------------------------------------- /tasks/npm-publish-clean-typechain.ts: -------------------------------------------------------------------------------- 1 | import { subtask } from 'hardhat/config'; 2 | import { TASK_COMPILE_SOLIDITY_COMPILE_JOBS } from 'hardhat/builtin-tasks/task-names'; 3 | import fs from 'fs/promises'; 4 | 5 | subtask(TASK_COMPILE_SOLIDITY_COMPILE_JOBS, 'Clean tests from types if needed').setAction(async (taskArgs, { run }, runSuper) => { 6 | const compileSolOutput = await runSuper(taskArgs); 7 | if (!!process.env.PUBLISHING_NPM) { 8 | console.log('🫠 Removing all test references from typechain'); 9 | // Cleaning typechained/index 10 | console.log(` 🧹 Excluding from main index`); 11 | const typechainIndexBuffer = await fs.readFile('./typechained/index.ts'); 12 | const finalTypechainIndex = typechainIndexBuffer 13 | .toString('utf-8') 14 | .split(/\r?\n/) 15 | .filter((line) => !line.includes('test')) 16 | .join('\n'); 17 | await fs.writeFile('./typechained/index.ts', finalTypechainIndex, 'utf-8'); 18 | // Cleaning typechained/solidity/contracts/index 19 | console.log(` 🧹 Excluding from contracts index`); 20 | const typechainContractsIndex = await fs.readFile('./typechained/solidity/contracts/index.ts'); 21 | const finalTypechainContractsIndex = typechainContractsIndex 22 | .toString('utf-8') 23 | .split(/\r?\n/) 24 | .filter((line) => !line.includes('test')) 25 | .join('\n'); 26 | await fs.writeFile('./typechained/solidity/contracts/index.ts', finalTypechainContractsIndex, 'utf-8'); 27 | // Cleaning typechained/factories/contracts/index 28 | console.log(` 🧹 Excluding from factories contract's index`); 29 | const typechainFactoriesIndexBuffer = await fs.readFile('./typechained/factories/solidity/contracts/index.ts'); 30 | const finalTypechainFactoriesIndex = typechainFactoriesIndexBuffer 31 | .toString('utf-8') 32 | .split(/\r?\n/) 33 | .filter((line) => !line.includes('test')) 34 | .join('\n'); 35 | await fs.writeFile('./typechained/factories/solidity/contracts/index.ts', finalTypechainFactoriesIndex, 'utf-8'); 36 | } 37 | return compileSolOutput; 38 | }); 39 | -------------------------------------------------------------------------------- /test/e2e/oracle-aggregator.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { given, then, when } from '@utils/bdd'; 4 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 5 | import { OracleAggregatorMock, OracleAggregatorMock__factory, BaseOracle, ERC165__factory, ITokenPriceOracle__factory } from '@typechained'; 6 | import { snapshot } from '@utils/evm'; 7 | import { smock, FakeContract } from '@defi-wonderland/smock'; 8 | import { BigNumber } from 'ethers'; 9 | import { getInterfaceId } from '@utils/erc165'; 10 | 11 | chai.use(smock.matchers); 12 | 13 | describe('OracleAggregator', () => { 14 | const TOKEN_A = '0x0000000000000000000000000000000000000001'; 15 | const TOKEN_B = '0x0000000000000000000000000000000000000002'; 16 | const TOKEN_C = '0x0000000000000000000000000000000000000003'; 17 | const BYTES = '0xf2c047db4a7cf81f935c'; // Some random bytes 18 | let superAdmin: SignerWithAddress, admin: SignerWithAddress; 19 | let oracleAggregator: OracleAggregatorMock; 20 | let superAdminRole: string, adminRole: string; 21 | let oracle1: FakeContract, oracle2: FakeContract; 22 | let snapshotId: string; 23 | 24 | before('Setup accounts and contracts', async () => { 25 | [, superAdmin, admin] = await ethers.getSigners(); 26 | const oracleAggregatorFactory: OracleAggregatorMock__factory = await ethers.getContractFactory( 27 | 'solidity/contracts/OracleAggregator.sol:OracleAggregator' 28 | ); 29 | oracle1 = await deployFakeOracle(); 30 | oracle2 = await deployFakeOracle(); 31 | oracleAggregator = await oracleAggregatorFactory.deploy([oracle1.address, oracle2.address], superAdmin.address, [admin.address]); 32 | superAdminRole = await oracleAggregator.SUPER_ADMIN_ROLE(); 33 | adminRole = await oracleAggregator.ADMIN_ROLE(); 34 | snapshotId = await snapshot.take(); 35 | }); 36 | 37 | beforeEach('Deploy and configure', async () => { 38 | await snapshot.revert(snapshotId); 39 | }); 40 | 41 | describe('force and update', () => { 42 | when('an oracle is forced', () => { 43 | given(async () => { 44 | oracle1.canSupportPair.returns(true); 45 | oracle2.canSupportPair.returns(true); 46 | await oracleAggregator.connect(admin).forceOracle(TOKEN_A, TOKEN_B, oracle2.address, BYTES); 47 | }); 48 | describe('and then an admin updates the support', () => { 49 | given(async () => { 50 | await oracleAggregator.connect(admin).addOrModifySupportForPair(TOKEN_A, TOKEN_B, BYTES); 51 | }); 52 | then('a oracle that takes precedence will be assigned', async () => { 53 | const { oracle, forced } = await oracleAggregator.assignedOracle(TOKEN_A, TOKEN_B); 54 | expect(oracle).to.equal(oracle1.address); 55 | expect(forced).to.be.false; 56 | }); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('multicall', () => { 62 | const QUOTE_ORACLE_1 = BigNumber.from(10); 63 | const QUOTE_ORACLE_2 = BigNumber.from(20); 64 | when('executing multiple quotes', () => { 65 | let result1: string, result2: string; 66 | given(async () => { 67 | oracle1.quote.returns(QUOTE_ORACLE_1); 68 | oracle2.quote.returns(QUOTE_ORACLE_2); 69 | await oracleAggregator.connect(admin).forceOracle(TOKEN_A, TOKEN_B, oracle1.address, BYTES); 70 | await oracleAggregator.connect(admin).forceOracle(TOKEN_A, TOKEN_C, oracle2.address, BYTES); 71 | 72 | const { data: quote1Data } = await oracleAggregator.populateTransaction.quote(TOKEN_A, 1, TOKEN_B, BYTES); 73 | const { data: quote2Data } = await oracleAggregator.populateTransaction.quote(TOKEN_A, 1, TOKEN_C, BYTES); 74 | [result1, result2] = await oracleAggregator.callStatic.multicall([quote1Data!, quote2Data!]); 75 | }); 76 | then('first quote was returned correctly', async () => { 77 | expect(BigNumber.from(result1)).to.equal(QUOTE_ORACLE_1); 78 | }); 79 | then('second quote was returned correctly', async () => { 80 | expect(BigNumber.from(result2)).to.equal(QUOTE_ORACLE_2); 81 | }); 82 | }); 83 | }); 84 | async function deployFakeOracle() { 85 | const ERC_165_INTERFACE_ID = getInterfaceId(ERC165__factory.createInterface()); 86 | const PRICE_ORACLE_INTERFACE_ID = getInterfaceId(ITokenPriceOracle__factory.createInterface()); 87 | const oracle = await smock.fake('BaseOracle'); 88 | oracle.supportsInterface.returns( 89 | ({ _interfaceId }: { _interfaceId: string }) => _interfaceId === ERC_165_INTERFACE_ID || _interfaceId === PRICE_ORACLE_INTERFACE_ID 90 | ); 91 | return oracle; 92 | } 93 | }); 94 | -------------------------------------------------------------------------------- /test/integration/api3_chainlink_adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { deployments, ethers, getNamedAccounts } from 'hardhat'; 2 | import { evm, wallet } from '@utils'; 3 | import { API3ChainlinkAdapter, API3ChainlinkAdapterFactory, API3ChainlinkAdapter__factory, StatefulChainlinkOracle } from '@typechained'; 4 | import { BigNumber, BytesLike, constants, utils } from 'ethers'; 5 | import { DeterministicFactory, DeterministicFactory__factory } from '@mean-finance/deterministic-factory'; 6 | import { address as DETERMINISTIC_FACTORY_ADDRESS } from '@mean-finance/deterministic-factory/deployments/ethereum/DeterministicFactory.json'; 7 | import { JsonRpcSigner } from '@ethersproject/providers'; 8 | import { expect } from 'chai'; 9 | import { given, then, when } from '@utils/bdd'; 10 | import { snapshot } from '@utils/evm'; 11 | import { convertPriceToBigNumberWithDecimals, getTokenData } from '@utils/defillama'; 12 | import { ChainlinkRegistry } from '@mean-finance/chainlink-registry/dist'; 13 | 14 | const BLOCK_NUMBER = 43876587; 15 | const EMPTY_BYTES: BytesLike = []; 16 | const DAI = '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063'; 17 | const USD = '0x0000000000000000000000000000000000000348'; 18 | 19 | // Skipped because hardhat caches chain data, so if we try to test this on Polygon and then other tests use Ethereum, everything breakes 20 | // Tried a few workarounds, but failed :( So we will simply disable this test and run it manually when necessary. Also, we can't test this 21 | // on Ethereum since there are no funded/active feeds at the moment 22 | describe.skip('API3ChainlinkAdapter', () => { 23 | let factory: API3ChainlinkAdapterFactory; 24 | let oracle: StatefulChainlinkOracle; 25 | let registry: ChainlinkRegistry; 26 | let admin: JsonRpcSigner; 27 | let snapshotId: string; 28 | 29 | before(async () => { 30 | // Fork and deploy 31 | await fork({ chain: 'polygon', blockNumber: BLOCK_NUMBER }); 32 | await deployments.run(['ChainlinkFeedRegistry', 'StatefulChainlinkOracle', 'API3ChainlinkAdapterFactory'], { 33 | resetMemory: true, 34 | deletePreviousDeployments: false, 35 | writeDeploymentsToFiles: false, 36 | }); 37 | const { msig } = await getNamedAccounts(); 38 | admin = await wallet.impersonate(msig); 39 | 40 | // Set up contracts 41 | factory = await ethers.getContract('API3ChainlinkAdapterFactory'); 42 | registry = await ethers.getContract('ChainlinkFeedRegistry'); 43 | oracle = await ethers.getContract('StatefulChainlinkOracle'); 44 | 45 | // Set up DAI feed 46 | await registry.connect(admin).assignFeeds([{ base: DAI, quote: USD, feed: '0x4746DeC9e833A82EC7C2C1356372CcF2cfcD2F3D' }]); 47 | 48 | snapshotId = await snapshot.take(); 49 | }); 50 | 51 | beforeEach(async () => { 52 | await snapshot.revert(snapshotId); 53 | }); 54 | 55 | adapterTest({ 56 | symbol: 'LDO', 57 | address: '0xC3C7d422809852031b44ab29EEC9F1EfF2A58756', 58 | proxy: '0x774F0C833ceaacA9b472771FfBE3ada4d6805709', 59 | }); 60 | 61 | adapterTest({ 62 | symbol: 'SAND', 63 | address: '0xBbba073C31bF03b8ACf7c28EF0738DeCF3695683', 64 | proxy: '0x1bF3b112556a536d4C051a014B439d1F34cf4CD8', 65 | }); 66 | 67 | function adapterTest({ address, symbol, proxy }: { address: string; symbol: string; proxy: string }) { 68 | when(`using an adapter for ${symbol}`, () => { 69 | let adapter: API3ChainlinkAdapter; 70 | given(async () => { 71 | // Set up adapter 72 | await factory.createAdapter(proxy, 8, `${symbol}/USD`); 73 | const adapterAddress = await factory.computeAdapterAddress(proxy, 8, `${symbol}/USD`); 74 | adapter = API3ChainlinkAdapter__factory.connect(adapterAddress, ethers.provider); 75 | 76 | // Add to registry 77 | await registry.connect(admin).assignFeeds([{ base: address, quote: USD, feed: adapter.address }]); 78 | 79 | // Prepare support 80 | await oracle.addSupportForPairIfNeeded(address, DAI, EMPTY_BYTES); 81 | }); 82 | then('quote is calculated correctly', async () => { 83 | const quote = await oracle.quote(address, utils.parseEther('1'), DAI, EMPTY_BYTES); 84 | await validateQuote(quote, address, DAI); 85 | }); 86 | then('description is set correctly', async () => { 87 | expect(await adapter.description()).to.equal(`${symbol}/USD`); 88 | }); 89 | then('decimals are set correctly', async () => { 90 | expect(await adapter.decimals()).to.equal(8); 91 | }); 92 | }); 93 | } 94 | 95 | async function validateQuote(quote: BigNumber, tokenIn: string, tokenOut: string, thresholdPercentage?: number) { 96 | const { timestamp } = await ethers.provider.getBlock(BLOCK_NUMBER); 97 | const tokenInData = await getTokenData('polygon', tokenIn, timestamp); 98 | const tokenOutData = await getTokenData('polygon', tokenOut, timestamp); 99 | const expectedAmountOut = convertPriceToBigNumberWithDecimals(tokenInData.price / tokenOutData.price, tokenOutData.decimals); 100 | 101 | const TRESHOLD_PERCENTAGE = thresholdPercentage ?? 2; // 2% price diff tolerance 102 | 103 | const threshold = expectedAmountOut.mul(TRESHOLD_PERCENTAGE * 10).div(100 * 10); 104 | const [upperThreshold, lowerThreshold] = [expectedAmountOut.add(threshold), expectedAmountOut.sub(threshold)]; 105 | const diff = quote.sub(expectedAmountOut); 106 | const sign = diff.isNegative() ? '-' : '+'; 107 | const diffPercentage = diff.abs().mul(10000).div(expectedAmountOut).toNumber() / 100; 108 | 109 | expect( 110 | quote.lte(upperThreshold) && quote.gte(lowerThreshold), 111 | `Expected ${quote.toString()} to be within [${lowerThreshold.toString()},${upperThreshold.toString()}]. Diff was ${sign}${diffPercentage}%` 112 | ).to.be.true; 113 | } 114 | 115 | async function fork({ chain, blockNumber }: { chain: string; blockNumber?: number }): Promise { 116 | // Set fork of network 117 | await evm.reset({ 118 | network: chain, 119 | blockNumber, 120 | }); 121 | const { deployer: deployerAddress, msig } = await getNamedAccounts(); 122 | // Give deployer role to our deployer address 123 | const admin = await wallet.impersonate(msig); 124 | await wallet.setBalance({ account: admin._address, balance: constants.MaxUint256 }); 125 | const deterministicFactory = await ethers.getContractAt( 126 | DeterministicFactory__factory.abi, 127 | DETERMINISTIC_FACTORY_ADDRESS 128 | ); 129 | await deterministicFactory.connect(admin).grantRole(await deterministicFactory.DEPLOYER_ROLE(), deployerAddress); 130 | } 131 | }); 132 | -------------------------------------------------------------------------------- /test/integration/dia_chainlink_adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { deployments, ethers, getNamedAccounts } from 'hardhat'; 2 | import { evm, wallet } from '@utils'; 3 | import { DIAChainlinkAdapter, DIAChainlinkAdapterFactory, DIAChainlinkAdapter__factory, StatefulChainlinkOracle } from '@typechained'; 4 | import { BigNumber, BytesLike, constants, utils } from 'ethers'; 5 | import { DeterministicFactory, DeterministicFactory__factory } from '@mean-finance/deterministic-factory'; 6 | import { address as DETERMINISTIC_FACTORY_ADDRESS } from '@mean-finance/deterministic-factory/deployments/ethereum/DeterministicFactory.json'; 7 | import { JsonRpcSigner } from '@ethersproject/providers'; 8 | import { expect } from 'chai'; 9 | import { given, then, when } from '@utils/bdd'; 10 | import { snapshot } from '@utils/evm'; 11 | import { convertPriceToBigNumberWithDecimals, getTokenData } from '@utils/defillama'; 12 | import { ChainlinkRegistry } from '@mean-finance/chainlink-registry/dist'; 13 | 14 | const BLOCK_NUMBER = 47015984; 15 | const EMPTY_BYTES: BytesLike = []; 16 | const POLYGON_USDC = '0x2791bca1f2de4661ed88a30c99a7a9449aa84174'; 17 | const POLYGON_USD = '0x0000000000000000000000000000000000000348'; 18 | const CHAINLINK_POLYGON_USDC_USD = '0xfe4a8cc5b5b2366c1b58bea3858e81843581b2f7'; 19 | const DIA_ORACLE_POLYGON = '0xf44b3c104f39209cd8420a1d3ca4338818aa72ab'; 20 | 21 | // Skipped because hardhat caches chain data, so if we try to test this on Polygon and then other tests use Ethereum, everything breakes 22 | // Tried a few workarounds, but failed :( So we will simply disable this test and run it manually when necessary. Also, we can't test this 23 | // on Ethereum since there are no funded/active feeds at the moment 24 | describe.skip('DIAChainlinkAdapter', () => { 25 | let factory: DIAChainlinkAdapterFactory; 26 | let oracle: StatefulChainlinkOracle; 27 | let registry: ChainlinkRegistry; 28 | let admin: JsonRpcSigner; 29 | let snapshotId: string; 30 | 31 | before(async () => { 32 | // Fork and deploy 33 | await fork({ chain: 'polygon', blockNumber: BLOCK_NUMBER }); 34 | await deployments.run(['ChainlinkFeedRegistry', 'StatefulChainlinkOracle', 'DIAChainlinkAdapterFactory'], { 35 | resetMemory: true, 36 | deletePreviousDeployments: false, 37 | writeDeploymentsToFiles: false, 38 | }); 39 | const { msig } = await getNamedAccounts(); 40 | admin = await wallet.impersonate(msig); 41 | 42 | // Set up contracts 43 | factory = await ethers.getContract('DIAChainlinkAdapterFactory'); 44 | registry = await ethers.getContract('ChainlinkFeedRegistry'); 45 | oracle = await ethers.getContract('StatefulChainlinkOracle'); 46 | 47 | // Set up POLYGON_USDC feed 48 | await registry.connect(admin).assignFeeds([{ base: POLYGON_USDC, quote: POLYGON_USD, feed: CHAINLINK_POLYGON_USDC_USD }]); 49 | 50 | snapshotId = await snapshot.take(); 51 | }); 52 | 53 | beforeEach(async () => { 54 | await snapshot.revert(snapshotId); 55 | }); 56 | 57 | adapterTest({ 58 | symbol: 'ETH', 59 | address: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619', // POLYGON WETH 60 | decimals: 18, 61 | }); 62 | 63 | adapterTest({ 64 | symbol: 'BTC', 65 | address: '0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6', // POLYGON WBTC 66 | decimals: 8, 67 | }); 68 | 69 | function adapterTest({ address, symbol, decimals }: { address: string; symbol: string; decimals: number }) { 70 | when(`using an adapter for ${symbol}`, () => { 71 | let adapter: DIAChainlinkAdapter; 72 | given(async () => { 73 | // Set up adapter 74 | await factory.createAdapter(DIA_ORACLE_POLYGON, 8, 8, `${symbol}/USD`); 75 | const adapterAddress = await factory.computeAdapterAddress(DIA_ORACLE_POLYGON, 8, 8, `${symbol}/USD`); 76 | adapter = DIAChainlinkAdapter__factory.connect(adapterAddress, ethers.provider); 77 | 78 | // Add to registry 79 | await registry.connect(admin).assignFeeds([{ base: address, quote: POLYGON_USD, feed: adapter.address }]); 80 | 81 | // Prepare support 82 | await oracle.addSupportForPairIfNeeded(address, POLYGON_USDC, EMPTY_BYTES); 83 | }); 84 | then('quote is calculated correctly', async () => { 85 | const quote = await oracle.quote(address, utils.parseUnits('1', decimals), POLYGON_USDC, EMPTY_BYTES); 86 | await validateQuote(quote, address, POLYGON_USDC); 87 | }); 88 | then('description is set correctly', async () => { 89 | expect(await adapter.description()).to.equal(`${symbol}/USD`); 90 | }); 91 | then('decimals are set correctly', async () => { 92 | expect(await adapter.decimals()).to.equal(8); 93 | }); 94 | }); 95 | } 96 | 97 | async function validateQuote(quote: BigNumber, tokenIn: string, tokenOut: string, thresholdPercentage?: number) { 98 | const { timestamp } = await ethers.provider.getBlock(BLOCK_NUMBER); 99 | const tokenInData = await getTokenData('polygon', tokenIn, timestamp); 100 | const tokenOutData = await getTokenData('polygon', tokenOut, timestamp); 101 | const expectedAmountOut = convertPriceToBigNumberWithDecimals(tokenInData.price / tokenOutData.price, tokenOutData.decimals); 102 | 103 | const TRESHOLD_PERCENTAGE = thresholdPercentage ?? 2; // 2% price diff tolerance 104 | 105 | const threshold = expectedAmountOut.mul(TRESHOLD_PERCENTAGE * 10).div(100 * 10); 106 | const [upperThreshold, lowerThreshold] = [expectedAmountOut.add(threshold), expectedAmountOut.sub(threshold)]; 107 | const diff = quote.sub(expectedAmountOut); 108 | const sign = diff.isNegative() ? '-' : '+'; 109 | const diffPercentage = diff.abs().mul(10000).div(expectedAmountOut).toNumber() / 100; 110 | 111 | expect( 112 | quote.lte(upperThreshold) && quote.gte(lowerThreshold), 113 | `Expected ${quote.toString()} to be within [${lowerThreshold.toString()},${upperThreshold.toString()}]. Diff was ${sign}${diffPercentage}%` 114 | ).to.be.true; 115 | } 116 | 117 | async function fork({ chain, blockNumber }: { chain: string; blockNumber?: number }): Promise { 118 | // Set fork of network 119 | await evm.reset({ 120 | network: chain, 121 | blockNumber, 122 | }); 123 | const { deployer: deployerAddress, msig } = await getNamedAccounts(); 124 | // Give deployer role to our deployer address 125 | const admin = await wallet.impersonate(msig); 126 | await wallet.setBalance({ account: admin._address, balance: constants.MaxUint256 }); 127 | const deterministicFactory = await ethers.getContractAt( 128 | DeterministicFactory__factory.abi, 129 | DETERMINISTIC_FACTORY_ADDRESS 130 | ); 131 | await deterministicFactory.connect(admin).grantRole(await deterministicFactory.DEPLOYER_ROLE(), deployerAddress); 132 | } 133 | }); 134 | -------------------------------------------------------------------------------- /test/integration/stateful-chainlink-oracle.spec.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, utils } from 'ethers'; 2 | import { deployments, ethers, getNamedAccounts } from 'hardhat'; 3 | import { StatefulChainlinkOracle } from '@typechained'; 4 | import { evm, wallet } from '@utils'; 5 | import { contract, given, then, when } from '@utils/bdd'; 6 | import { expect } from 'chai'; 7 | import { convertPriceToBigNumberWithDecimals, getPrice } from '@utils/defillama'; 8 | import { DeterministicFactory, DeterministicFactory__factory } from '@mean-finance/deterministic-factory'; 9 | import { address as DETERMINISTIC_FACTORY_ADDRESS } from '@mean-finance/deterministic-factory/deployments/ethereum/DeterministicFactory.json'; 10 | 11 | let oracle: StatefulChainlinkOracle; 12 | 13 | const WETH = { address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', decimals: 18, symbol: 'WETH' }; 14 | const USDC = { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: 6, symbol: 'USDC' }; 15 | const USDT = { address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6, symbol: 'USDT' }; 16 | const AAVE = { address: '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9', decimals: 18, symbol: 'AAVE' }; 17 | const COMP = { address: '0xc00e94cb662c3520282e6f5717214004a7f26888', decimals: 18, symbol: 'COMP' }; 18 | const BNT = { address: '0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c', decimals: 18, symbol: 'BNT' }; 19 | const CRV = { address: '0xD533a949740bb3306d119CC777fa900bA034cd52', decimals: 18, symbol: 'CRV' }; 20 | const AMP = { address: '0xff20817765cb7f73d4bde2e66e067e58d11095c2', decimals: 18, symbol: 'AMP' }; 21 | const FXS = { address: '0x3432b6a60d23ca0dfca7761b7ab56459d9c964d0', decimals: 18, symbol: 'FXS' }; 22 | const ALPHA = { address: '0xa1faa113cbe53436df28ff0aee54275c13b40975', decimals: 18, symbol: 'ALPHA' }; 23 | const BOND = { address: '0x0391d2021f89dc339f60fff84546ea23e337750f', decimals: 18, symbol: 'BOND' }; 24 | const AXS = { address: '0xbb0e17ef65f82ab018d8edd776e8dd940327b28b', decimals: 18, symbol: 'AXS' }; 25 | const MATIC = { address: '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0', decimals: 18, symbol: 'MATIC' }; 26 | const WBTC = { address: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', decimals: 8, symbol: 'WBTC' }; 27 | const DAI = { address: '0x6b175474e89094c44da98b954eedeac495271d0f', decimals: 18, symbol: 'DAI' }; 28 | 29 | const PLANS: { tokenIn: Token; tokenOut: Token }[][] = [ 30 | [ 31 | // ETH_USD_PAIR 32 | { tokenIn: WETH, tokenOut: USDT }, // IN is ETH, OUT is USD 33 | { tokenIn: USDC, tokenOut: WETH }, // IN is USD, OUT is ETH 34 | ], 35 | [ 36 | // TOKEN_USD_PAIR 37 | { tokenIn: AAVE, tokenOut: USDT }, // IN (tokenA) => OUT (tokenB) is USD 38 | { tokenIn: CRV, tokenOut: USDC }, // IN (tokenB) => OUT (tokenA) is USD 39 | { tokenIn: USDC, tokenOut: COMP }, // IN (tokenA) is USD => OUT (tokenB) 40 | { tokenIn: USDT, tokenOut: WBTC }, // IN (tokenB) is USD => OUT (tokenA) 41 | ], 42 | [ 43 | // TOKEN_ETH_PAIR 44 | { tokenIn: BNT, tokenOut: WETH }, // IN (tokenA) => OUT (tokenB) is ETH 45 | { tokenIn: AXS, tokenOut: WETH }, // IN (tokenB) => OUT (tokenA) is ETH 46 | { tokenIn: WETH, tokenOut: WBTC }, // IN (tokenB) is ETH => OUT (tokenA) 47 | { tokenIn: WETH, tokenOut: CRV }, // IN (tokenA) is ETH => OUT (tokenB) 48 | ], 49 | [ 50 | // TOKEN_TO_USD_TO_TOKEN_PAIR 51 | { tokenIn: WBTC, tokenOut: COMP }, // IN (tokenA) => USD => OUT (tokenB) 52 | { tokenIn: CRV, tokenOut: AAVE }, // IN (tokenB) => USD => OUT (tokenA) 53 | ], 54 | [ 55 | // TOKEN_TO_ETH_TO_TOKEN_PAIR 56 | { tokenIn: BOND, tokenOut: AXS }, // IN (tokenA) => ETH => OUT (tokenB) 57 | { tokenIn: ALPHA, tokenOut: BOND }, // IN (tokenB) => ETH => OUT (tokenA) 58 | ], 59 | [ 60 | // TOKEN_A_TO_USD_TO_ETH_TO_TOKEN_B 61 | { tokenIn: FXS, tokenOut: WETH }, // IN (tokenA) => USD, OUT (tokenB) is ETH 62 | { tokenIn: WETH, tokenOut: MATIC }, // IN (tokenB) is ETH, USD => OUT (tokenA) 63 | 64 | { tokenIn: USDC, tokenOut: AXS }, // IN (tokenA) is USD, ETH => OUT (tokenB) 65 | { tokenIn: ALPHA, tokenOut: DAI }, // IN (tokenB) => ETH, OUT is USD (tokenA) 66 | 67 | { tokenIn: FXS, tokenOut: AXS }, // IN (tokenA) => USD, ETH => OUT (tokenB) 68 | { tokenIn: ALPHA, tokenOut: MATIC }, // IN (tokenB) => ETH, USD => OUT (tokenA) 69 | ], 70 | [ 71 | // TOKEN_A_TO_ETH_TO_USD_TO_TOKEN_B 72 | // We can't test the following two cases, because we would need a token that is 73 | // supported by chainlink and lower than USD (address(840)) 74 | // - IN (tokenA) => ETH, OUT (tokenB) is USD 75 | // - IN (tokenB) is USD, ETH => OUT (tokenA) 76 | 77 | { tokenIn: WETH, tokenOut: AMP }, // IN (tokenA) is ETH, USD => OUT (tokenB) 78 | { tokenIn: AMP, tokenOut: WETH }, // IN (tokenB) => USD, OUT is ETH (tokenA) 79 | 80 | { tokenIn: AXS, tokenOut: AMP }, // IN (tokenA) => ETH, USD => OUT (tokenB) 81 | { tokenIn: FXS, tokenOut: BOND }, // IN (tokenB) => USD, ETH => OUT (tokenA) 82 | ], 83 | [ 84 | // SAME_TOKENS 85 | { tokenIn: USDT, tokenOut: USDC }, // tokenA is USD, tokenB is USD 86 | { tokenIn: ALPHA, tokenOut: ALPHA }, // tokenA == token B 87 | ], 88 | ]; 89 | 90 | const TRESHOLD_PERCENTAGE = 3; // In mainnet, max threshold is usually 2%, but since we are combining pairs, it can sometimes be a little higher 91 | const BLOCK_NUMBER = 15591000; 92 | 93 | contract('StatefulChainlinkOracle', () => { 94 | before(async () => { 95 | // Set fork of network 96 | await evm.reset({ 97 | network: 'ethereum', 98 | blockNumber: BLOCK_NUMBER, 99 | }); 100 | 101 | const { deployer, msig } = await getNamedAccounts(); 102 | const admin = await wallet.impersonate(msig); 103 | const ethMsig = await wallet.impersonate('0xEC864BE26084ba3bbF3cAAcF8F6961A9263319C4'); 104 | await wallet.setBalance({ account: admin._address, balance: utils.parseEther('10') }); 105 | await wallet.setBalance({ account: ethMsig._address, balance: utils.parseEther('10') }); 106 | 107 | const deterministicFactory = await ethers.getContractAt( 108 | DeterministicFactory__factory.abi, 109 | DETERMINISTIC_FACTORY_ADDRESS 110 | ); 111 | 112 | await deterministicFactory.connect(ethMsig).grantRole(await deterministicFactory.DEPLOYER_ROLE(), deployer); 113 | await deployments.run(['ChainlinkFeedRegistry', 'StatefulChainlinkOracle'], { 114 | resetMemory: true, 115 | deletePreviousDeployments: false, 116 | writeDeploymentsToFiles: false, 117 | }); 118 | oracle = await ethers.getContract('StatefulChainlinkOracle'); 119 | await oracle 120 | .connect(admin) 121 | .addMappings( 122 | [WBTC.address, WETH.address, USDC.address, USDT.address, DAI.address], 123 | [ 124 | '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', 125 | '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', 126 | '0x0000000000000000000000000000000000000348', 127 | '0x0000000000000000000000000000000000000348', 128 | '0x0000000000000000000000000000000000000348', 129 | ] 130 | ); 131 | }); 132 | 133 | for (let i = 0; i < PLANS.length; i++) { 134 | for (const { tokenIn, tokenOut } of PLANS[i]) { 135 | describe(`quote (${tokenIn.symbol}, ${tokenOut.symbol})`, () => { 136 | given(async () => { 137 | await oracle.addSupportForPairIfNeeded(tokenIn.address, tokenOut.address, []); 138 | }); 139 | then(`returns correct quote`, async () => { 140 | const quote = await oracle.quote(tokenIn.address, utils.parseUnits('1', tokenIn.decimals), tokenOut.address, []); 141 | 142 | const coingeckoPrice = await getPriceBetweenTokens(tokenIn, tokenOut); 143 | const expected = convertPriceToBigNumberWithDecimals(coingeckoPrice, tokenOut.decimals); 144 | validateQuote(quote, expected); 145 | }); 146 | then(`pricing plan is the correct one`, async () => { 147 | const plan1 = await oracle.planForPair(tokenIn.address, tokenOut.address); 148 | const plan2 = await oracle.planForPair(tokenOut.address, tokenIn.address); 149 | expect(plan1).to.equal(i + 1); 150 | expect(plan2).to.equal(i + 1); 151 | }); 152 | }); 153 | } 154 | } 155 | when('quoting between forex addresses', () => { 156 | const GBP = '0x000000000000000000000000000000000000033A'; 157 | const EUR = '0x00000000000000000000000000000000000003D2'; 158 | given(async () => { 159 | await oracle.addSupportForPairIfNeeded(GBP, EUR, []); 160 | }); 161 | then(`returns correct quote`, async () => { 162 | const quote = await oracle.quote(GBP, utils.parseUnits('1', 8), EUR, []); 163 | const expected = utils.parseUnits('1.18', 8); // Checked manually 164 | validateQuote(quote, expected); 165 | }); 166 | then(`pricing plan is the correct one`, async () => { 167 | const plan1 = await oracle.planForPair(GBP, EUR); 168 | const plan2 = await oracle.planForPair(EUR, GBP); 169 | expect(plan1).to.equal(4); // 4 is GBP => USD => EUR 170 | expect(plan2).to.equal(4); 171 | }); 172 | }); 173 | 174 | function validateQuote(quote: BigNumber, expected: BigNumber) { 175 | const threshold = expected.mul(TRESHOLD_PERCENTAGE * 10).div(100 * 10); 176 | const [upperThreshold, lowerThreshold] = [expected.add(threshold), expected.sub(threshold)]; 177 | const diff = quote.sub(expected); 178 | const sign = diff.isNegative() ? '-' : '+'; 179 | const diffPercentage = diff.abs().mul(10000).div(expected).toNumber() / 100; 180 | 181 | expect( 182 | quote.lte(upperThreshold) && quote.gte(lowerThreshold), 183 | `Expected ${quote.toString()} to be within [${lowerThreshold.toString()},${upperThreshold.toString()}]. Diff was ${sign}${diffPercentage}%` 184 | ).to.be.true; 185 | } 186 | }); 187 | 188 | async function getPriceBetweenTokens(tokenA: Token, tokenB: Token) { 189 | const tokenAPrice = await fetchPrice(tokenA.address); 190 | const tokenBPrice = await fetchPrice(tokenB.address); 191 | return tokenAPrice / tokenBPrice; 192 | } 193 | 194 | let priceCache: Map = new Map(); 195 | async function fetchPrice(address: string): Promise { 196 | if (!priceCache.has(address)) { 197 | const { timestamp } = await ethers.provider.getBlock(BLOCK_NUMBER); 198 | const price = await getPrice('ethereum', address, timestamp); 199 | priceCache.set(address, price); 200 | } 201 | return priceCache.get(address)!; 202 | } 203 | 204 | type Token = { address: string; decimals: number; symbol: string }; 205 | -------------------------------------------------------------------------------- /test/integration/transformer-oracle.spec.ts: -------------------------------------------------------------------------------- 1 | import { deployments, ethers, getNamedAccounts } from 'hardhat'; 2 | import { evm, wallet } from '@utils'; 3 | import { TransformerOracle } from '@typechained'; 4 | import { BigNumber, BytesLike, constants, utils } from 'ethers'; 5 | import { DeterministicFactory, DeterministicFactory__factory } from '@mean-finance/deterministic-factory'; 6 | import { address as DETERMINISTIC_FACTORY_ADDRESS } from '@mean-finance/deterministic-factory/deployments/ethereum/DeterministicFactory.json'; 7 | import { ProtocolTokenWrapperTransformer, TransformerRegistry } from '@mean-finance/transformers'; 8 | import { JsonRpcSigner } from '@ethersproject/providers'; 9 | import { expect } from 'chai'; 10 | import { given, then, when } from '@utils/bdd'; 11 | import { snapshot } from '@utils/evm'; 12 | import { convertPriceToBigNumberWithDecimals, getTokenData } from '@utils/defillama'; 13 | 14 | const BLOCK_NUMBER = 16791195; 15 | const EMPTY_BYTES: BytesLike = []; 16 | 17 | const DAI = '0x6b175474e89094c44da98b954eedeac495271d0f'; 18 | const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; 19 | const STETH = '0xae7ab96520de3a18e5e111b5eaab095312d7fe84'; 20 | const WSTETH = '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0'; 21 | const EULER_WSTETH = '0x7C6D161b367Ec0605260628c37B8dd778446256b'; 22 | 23 | describe('TransformerOracle', () => { 24 | let oracle: TransformerOracle; 25 | let transformerRegistry: TransformerRegistry; 26 | let admin: JsonRpcSigner; 27 | let snapshotId: string; 28 | 29 | before(async () => { 30 | await fork({ chain: 'ethereum', blockNumber: BLOCK_NUMBER }); 31 | const { msig } = await getNamedAccounts(); 32 | admin = await wallet.impersonate(msig); 33 | await wallet.setBalance({ account: msig, balance: utils.parseEther('10') }); 34 | await deployments.run( 35 | [ 36 | 'ChainlinkFeedRegistry', 37 | 'TransformerRegistry', 38 | 'ProtocolTokenWrapperTransformer', 39 | 'wstETHTransformer', 40 | 'ERC4626Transformer', 41 | 'TransformerOracle', 42 | ], 43 | { 44 | resetMemory: true, 45 | deletePreviousDeployments: false, 46 | writeDeploymentsToFiles: false, 47 | } 48 | ); 49 | oracle = await ethers.getContract('TransformerOracle'); 50 | transformerRegistry = await ethers.getContract('TransformerRegistry'); 51 | snapshotId = await snapshot.take(); 52 | }); 53 | 54 | beforeEach(async () => { 55 | await snapshot.revert(snapshotId); 56 | }); 57 | describe('using the transformer oracle', async () => { 58 | when('using the wstETH transformer', () => { 59 | given(async () => { 60 | const wstETHTransformer = await ethers.getContract('wstETHTransformer'); 61 | // Register WSTETH to wstETH transformer 62 | await transformerRegistry.connect(admin).registerTransformers([ 63 | { 64 | transformer: wstETHTransformer.address, 65 | dependents: [WSTETH], 66 | }, 67 | ]); 68 | await oracle.addSupportForPairIfNeeded(WSTETH, STETH, EMPTY_BYTES); 69 | await oracle.addSupportForPairIfNeeded(STETH, DAI, EMPTY_BYTES); 70 | }); 71 | then('the pair is transformed correctly', async () => { 72 | const wstETHToDaiQuote = await oracle.quote(WSTETH, utils.parseEther('1'), DAI, EMPTY_BYTES); 73 | await validateQuote(wstETHToDaiQuote, WSTETH, DAI); 74 | const wstETHToSTETHQuote = await oracle.quote(WSTETH, utils.parseEther('1'), STETH, EMPTY_BYTES); 75 | await validateQuote(wstETHToSTETHQuote, WSTETH, STETH, 0.5); 76 | const stETHToWSTETH = await oracle.quote(STETH, utils.parseEther('1'), WSTETH, EMPTY_BYTES); 77 | await validateQuote(stETHToWSTETH, STETH, WSTETH, 0.5); 78 | }); 79 | }); 80 | 81 | when('using both the wstETH and erc4626 transformer', () => { 82 | given(async () => { 83 | const wstETHTransformer = await ethers.getContract('wstETHTransformer'); 84 | const erc4626Transformer = await ethers.getContract('ERC4626Transformer'); 85 | // Register transformers 86 | await transformerRegistry.connect(admin).registerTransformers([ 87 | { 88 | transformer: wstETHTransformer.address, 89 | dependents: [WSTETH], 90 | }, 91 | { 92 | transformer: erc4626Transformer.address, 93 | dependents: [EULER_WSTETH], 94 | }, 95 | ]); 96 | await oracle.addSupportForPairIfNeeded(EULER_WSTETH, DAI, EMPTY_BYTES); 97 | }); 98 | then('the pair is transformed correctly', async () => { 99 | const stETHToDAIQuote = await oracle.quote(STETH, utils.parseEther('1'), DAI, EMPTY_BYTES); 100 | await validateQuote(stETHToDAIQuote, STETH, DAI); 101 | 102 | const wstETHToDAIQuote = await oracle.quote(WSTETH, utils.parseEther('1'), DAI, EMPTY_BYTES); 103 | await validateQuote(wstETHToDAIQuote, WSTETH, DAI); 104 | 105 | const eulerWstETHToDaiQuote = await oracle.quote(EULER_WSTETH, utils.parseEther('1'), DAI, EMPTY_BYTES); 106 | // Note: since no one is using it, we can check the price of wstETH, since we don't have the price for euler's version yet 107 | await validateQuote(eulerWstETHToDaiQuote, WSTETH, DAI); 108 | }); 109 | }); 110 | 111 | /* 112 | This test is meant to use a lot of different components. The idea is that the quote for WETH => DAI will: 113 | 1. Transform WETH to ETH in the Transformer oracle 114 | 2. Delegate the quote from the aggregator to the Chainlink oracle 115 | 3. Use the Chainlink registry in the Chainlink Oracle 116 | */ 117 | when('using the protocol token transformer wrapper transformer', () => { 118 | given(async () => { 119 | const protocolTokenTransformer = await ethers.getContract('ProtocolTokenWrapperTransformer'); 120 | // Register WETH to protocol token transformer 121 | await transformerRegistry.connect(admin).registerTransformers([ 122 | { 123 | transformer: protocolTokenTransformer.address, 124 | dependents: [WETH], 125 | }, 126 | ]); 127 | await oracle.addSupportForPairIfNeeded(WETH, DAI, EMPTY_BYTES); 128 | }); 129 | then('the pair is transformed correctly', async () => { 130 | const quote = await oracle.quote(WETH, utils.parseEther('1'), DAI, EMPTY_BYTES); 131 | await validateQuote(quote, WETH, DAI); 132 | }); 133 | }); 134 | }); 135 | 136 | async function validateQuote(quote: BigNumber, tokenIn: string, tokenOut: string, thresholdPercentage?: number) { 137 | const { timestamp } = await ethers.provider.getBlock(BLOCK_NUMBER); 138 | const tokenInData = await getTokenData('ethereum', tokenIn, timestamp); 139 | const tokenOutData = await getTokenData('ethereum', tokenOut, timestamp); 140 | const expectedAmountOut = convertPriceToBigNumberWithDecimals(tokenInData.price / tokenOutData.price, tokenOutData.decimals); 141 | 142 | const TRESHOLD_PERCENTAGE = thresholdPercentage ?? 2; // 2% price diff tolerance 143 | 144 | const threshold = expectedAmountOut.mul(TRESHOLD_PERCENTAGE * 10).div(100 * 10); 145 | const [upperThreshold, lowerThreshold] = [expectedAmountOut.add(threshold), expectedAmountOut.sub(threshold)]; 146 | const diff = quote.sub(expectedAmountOut); 147 | const sign = diff.isNegative() ? '-' : '+'; 148 | const diffPercentage = diff.abs().mul(10000).div(expectedAmountOut).toNumber() / 100; 149 | 150 | expect( 151 | quote.lte(upperThreshold) && quote.gte(lowerThreshold), 152 | `Expected ${quote.toString()} to be within [${lowerThreshold.toString()},${upperThreshold.toString()}]. Diff was ${sign}${diffPercentage}%` 153 | ).to.be.true; 154 | } 155 | 156 | async function fork({ chain, blockNumber }: { chain: string; blockNumber?: number }): Promise { 157 | // Set fork of network 158 | await evm.reset({ 159 | network: chain, 160 | blockNumber, 161 | }); 162 | const { deployer: deployerAddress } = await getNamedAccounts(); 163 | // Give deployer role to our deployer address 164 | const admin = await wallet.impersonate('0xEC864BE26084ba3bbF3cAAcF8F6961A9263319C4'); 165 | await wallet.setBalance({ account: admin._address, balance: constants.MaxUint256 }); 166 | const deterministicFactory = await ethers.getContractAt( 167 | DeterministicFactory__factory.abi, 168 | DETERMINISTIC_FACTORY_ADDRESS 169 | ); 170 | await deterministicFactory.connect(admin).grantRole(await deterministicFactory.DEPLOYER_ROLE(), deployerAddress); 171 | } 172 | }); 173 | -------------------------------------------------------------------------------- /test/integration/uniswap-v3-add-support-gas.spec.ts: -------------------------------------------------------------------------------- 1 | import { deployments, ethers, getNamedAccounts } from 'hardhat'; 2 | import { evm, wallet } from '@utils'; 3 | import { UniswapV3Adapter } from '@typechained'; 4 | import { constants } from 'ethers'; 5 | import { DeterministicFactory, DeterministicFactory__factory } from '@mean-finance/deterministic-factory'; 6 | import { address as DETERMINISTIC_FACTORY_ADDRESS } from '@mean-finance/deterministic-factory/deployments/optimism/DeterministicFactory.json'; 7 | import { expect } from 'chai'; 8 | import { given, then, when } from '@utils/bdd'; 9 | import { snapshot } from '@utils/evm'; 10 | 11 | const BLOCK_NUMBER = 24283642; 12 | const BYTES = '0xf2c047db4a7cf81f935c'; // Some random bytes 13 | 14 | const DAI = '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1'; 15 | const RAI = '0x7fb688ccf682d58f86d7e38e03f9d22e7705448b'; 16 | 17 | describe('Uniswap v3 Add Support - Gas Test', () => { 18 | let oracle: UniswapV3Adapter; 19 | let snapshotId: string; 20 | 21 | before(async () => { 22 | await fork({ chain: 'optimism', blockNumber: BLOCK_NUMBER }); 23 | await deployments.fixture(['UniswapV3Adapter'], { keepExistingDeployments: false }); 24 | oracle = await ethers.getContract('UniswapV3Adapter'); 25 | snapshotId = await snapshot.take(); 26 | }); 27 | 28 | beforeEach(async () => { 29 | await snapshot.revert(snapshotId); 30 | }); 31 | 32 | describe('add support for uninitialized pools', () => { 33 | when('adding support for a pair with many uninitialized pools fails', async () => { 34 | given(async () => { 35 | expect(await oracle.getPoolsPreparedForPair(DAI, RAI)).to.have.lengthOf(0); 36 | await oracle.addSupportForPairIfNeeded(DAI, RAI, BYTES); 37 | }); 38 | then('pools were added correctly', async () => { 39 | expect(await oracle.getPoolsPreparedForPair(DAI, RAI)).to.have.lengthOf(2); 40 | }); 41 | }); 42 | }); 43 | 44 | async function fork({ chain, blockNumber }: { chain: string; blockNumber?: number }): Promise { 45 | // Set fork of network 46 | await evm.reset({ 47 | network: chain, 48 | blockNumber, 49 | }); 50 | const { deployer: deployerAddress, msig } = await getNamedAccounts(); 51 | // Give deployer role to our deployer address 52 | const admin = await wallet.impersonate(msig); 53 | await wallet.setBalance({ account: admin._address, balance: constants.MaxUint256 }); 54 | const deterministicFactory = await ethers.getContractAt( 55 | DeterministicFactory__factory.abi, 56 | DETERMINISTIC_FACTORY_ADDRESS 57 | ); 58 | await deterministicFactory.connect(admin).grantRole(await deterministicFactory.DEPLOYER_ROLE(), deployerAddress); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /test/unit/adapters/api3-chainlink-adapter/api3-chainlink-adapter-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { given, then, when } from '@utils/bdd'; 4 | import { API3ChainlinkAdapter__factory, API3ChainlinkAdapterFactory__factory, API3ChainlinkAdapterFactory } from '@typechained'; 5 | import { TransactionResponse } from 'ethers/node_modules/@ethersproject/providers'; 6 | import { snapshot } from '@utils/evm'; 7 | 8 | describe('API3ChainlinkAdapterFactory', () => { 9 | const PROXY = '0x0000000000000000000000000000000000000001'; 10 | const DECIMALS = 8; 11 | const DESCRIPTION = 'TOKEN/USD'; 12 | 13 | let factory: API3ChainlinkAdapterFactory; 14 | let snapshotId: string; 15 | 16 | before(async () => { 17 | const factoryFactory: API3ChainlinkAdapterFactory__factory = await ethers.getContractFactory('API3ChainlinkAdapterFactory'); 18 | factory = await factoryFactory.deploy(); 19 | snapshotId = await snapshot.take(); 20 | }); 21 | 22 | beforeEach(async () => { 23 | await snapshot.revert(snapshotId); 24 | }); 25 | 26 | describe('createAdapter', () => { 27 | when('adapter is created', () => { 28 | let expectedAddress: string; 29 | let tx: TransactionResponse; 30 | given(async () => { 31 | expectedAddress = await factory.computeAdapterAddress(PROXY, DECIMALS, DESCRIPTION); 32 | tx = await factory.createAdapter(PROXY, DECIMALS, DESCRIPTION); 33 | }); 34 | then('event is emitted', async () => { 35 | await expect(tx).to.emit(factory, 'AdapterCreated').withArgs(expectedAddress); 36 | }); 37 | then('contract was deployed correctly', async () => { 38 | const adapter = API3ChainlinkAdapter__factory.connect(expectedAddress, ethers.provider); 39 | expect(await adapter.API3_PROXY()).to.equal(PROXY); 40 | expect(await adapter.decimals()).to.equal(DECIMALS); 41 | expect(await adapter.description()).to.equal(DESCRIPTION); 42 | }); 43 | }); 44 | when('adapter is created twice', () => { 45 | given(async () => { 46 | await factory.createAdapter(PROXY, DECIMALS, DESCRIPTION); 47 | }); 48 | then('the second time reverts', async () => { 49 | const tx = factory.createAdapter(PROXY, DECIMALS, DESCRIPTION); 50 | await expect(tx).to.have.reverted; 51 | }); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/unit/adapters/api3-chainlink-adapter/api3-chainlink-adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { BigNumber, utils } from 'ethers'; 4 | import { behaviours } from '@utils'; 5 | import { given, then, when } from '@utils/bdd'; 6 | import { IProxy, API3ChainlinkAdapter, API3ChainlinkAdapter__factory } from '@typechained'; 7 | import { snapshot } from '@utils/evm'; 8 | import { smock, FakeContract } from '@defi-wonderland/smock'; 9 | 10 | chai.use(smock.matchers); 11 | 12 | describe('API3ChainlinkAdapter', () => { 13 | const DECIMALS = 8; 14 | const DESCRIPTION = 'TOKEN/USD'; 15 | const VALUE_WITH_18_DECIMALS = utils.parseEther('1.2345'); 16 | const VALUE_WITH_8_DECIMALS = utils.parseUnits('1.2345', 8); 17 | const TIMESTAMP = 678910; 18 | const LATEST_ROUND = 0; 19 | 20 | let adapter: API3ChainlinkAdapter; 21 | let api3Proxy: FakeContract; 22 | let snapshotId: string; 23 | 24 | before(async () => { 25 | api3Proxy = await smock.fake('IProxy'); 26 | const factory: API3ChainlinkAdapter__factory = await ethers.getContractFactory('API3ChainlinkAdapter'); 27 | adapter = await factory.deploy(api3Proxy.address, DECIMALS, DESCRIPTION); 28 | snapshotId = await snapshot.take(); 29 | }); 30 | 31 | beforeEach(async () => { 32 | await snapshot.revert(snapshotId); 33 | api3Proxy.read.reset(); 34 | api3Proxy.read.returns([VALUE_WITH_18_DECIMALS, TIMESTAMP]); 35 | }); 36 | 37 | describe('constructor', () => { 38 | when('deployed', () => { 39 | then('description is set correctly', async () => { 40 | const description = await adapter.description(); 41 | expect(description).to.equal(DESCRIPTION); 42 | }); 43 | then('decimals is set correctly', async () => { 44 | expect(await adapter.decimals()).to.equal(DECIMALS); 45 | }); 46 | then('API3 proxy is set correctly', async () => { 47 | expect(await adapter.API3_PROXY()).to.equal(api3Proxy.address); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('version', () => { 53 | when('called', () => { 54 | then('value is returned correctly', async () => { 55 | expect(await adapter.version()).to.equal(4); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('getRoundData', () => { 61 | when('called with an invalid round', () => { 62 | then('reverts with message', async () => { 63 | await behaviours.txShouldRevertWithMessage({ 64 | contract: adapter, 65 | func: 'getRoundData', 66 | args: [1], 67 | message: 'OnlyLatestRoundIsAvailable', 68 | }); 69 | }); 70 | }); 71 | when('called with the latest round', () => { 72 | let _roundId: BigNumber, _answer: BigNumber, _startedAt: BigNumber, _updatedAt: BigNumber, _answeredInRound: BigNumber; 73 | 74 | given(async () => { 75 | ({ _roundId, _answer, _startedAt, _updatedAt, _answeredInRound } = await adapter.getRoundData(LATEST_ROUND)); 76 | }); 77 | then('proxy is called correctly', () => { 78 | expect(api3Proxy.read).to.have.been.calledOnce; 79 | }); 80 | then('value is returned correctly', () => { 81 | expect(_roundId).to.equal(LATEST_ROUND); 82 | expect(_answer).to.equal(VALUE_WITH_8_DECIMALS); 83 | expect(_startedAt).to.equal(TIMESTAMP); 84 | expect(_updatedAt).to.equal(TIMESTAMP); 85 | expect(_answeredInRound).to.equal(LATEST_ROUND); 86 | }); 87 | }); 88 | }); 89 | 90 | describe('latestRoundData', () => { 91 | when('called', () => { 92 | let _roundId: BigNumber, _answer: BigNumber, _startedAt: BigNumber, _updatedAt: BigNumber, _answeredInRound: BigNumber; 93 | 94 | given(async () => { 95 | ({ _roundId, _answer, _startedAt, _updatedAt, _answeredInRound } = await adapter.latestRoundData()); 96 | }); 97 | then('proxy is called correctly', () => { 98 | expect(api3Proxy.read).to.have.been.calledOnce; 99 | }); 100 | then('value is returned correctly', () => { 101 | expect(_roundId).to.equal(LATEST_ROUND); 102 | expect(_answer).to.equal(VALUE_WITH_8_DECIMALS); 103 | expect(_startedAt).to.equal(TIMESTAMP); 104 | expect(_updatedAt).to.equal(TIMESTAMP); 105 | expect(_answeredInRound).to.equal(LATEST_ROUND); 106 | }); 107 | }); 108 | }); 109 | 110 | describe('latestAnswer', () => { 111 | when('called', () => { 112 | let answer: BigNumber; 113 | 114 | given(async () => { 115 | answer = await adapter.latestAnswer(); 116 | }); 117 | then('proxy is called correctly', () => { 118 | expect(api3Proxy.read).to.have.been.calledOnce; 119 | }); 120 | then('value is returned correctly', () => { 121 | expect(answer).to.equal(VALUE_WITH_8_DECIMALS); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('latestTimestamp', () => { 127 | when('called', () => { 128 | let timestamp: BigNumber; 129 | 130 | given(async () => { 131 | timestamp = await adapter.latestTimestamp(); 132 | }); 133 | then('proxy is called correctly', () => { 134 | expect(api3Proxy.read).to.have.been.calledOnce; 135 | }); 136 | then('value is returned correctly', () => { 137 | expect(timestamp).to.equal(TIMESTAMP); 138 | }); 139 | }); 140 | }); 141 | 142 | describe('latestRound', () => { 143 | when('called', () => { 144 | then('value is returned correctly', async () => { 145 | expect(await adapter.latestRound()).to.equal(LATEST_ROUND); 146 | }); 147 | }); 148 | }); 149 | 150 | describe('getAnswer', () => { 151 | when('called with an invalid round', () => { 152 | then('reverts with message', async () => { 153 | await behaviours.txShouldRevertWithMessage({ 154 | contract: adapter, 155 | func: 'getAnswer', 156 | args: [1], 157 | message: 'OnlyLatestRoundIsAvailable', 158 | }); 159 | }); 160 | }); 161 | when('called with the latest round', () => { 162 | let answer: BigNumber; 163 | 164 | given(async () => { 165 | answer = await adapter.getAnswer(LATEST_ROUND); 166 | }); 167 | then('proxy is called correctly', () => { 168 | expect(api3Proxy.read).to.have.been.calledOnce; 169 | }); 170 | then('value is returned correctly', () => { 171 | expect(answer).to.equal(VALUE_WITH_8_DECIMALS); 172 | }); 173 | }); 174 | }); 175 | 176 | describe('getTimestamp', () => { 177 | when('called with an invalid round', () => { 178 | then('reverts with message', async () => { 179 | await behaviours.txShouldRevertWithMessage({ 180 | contract: adapter, 181 | func: 'getTimestamp', 182 | args: [1], 183 | message: 'OnlyLatestRoundIsAvailable', 184 | }); 185 | }); 186 | }); 187 | when('called with the latest round', () => { 188 | let timestamp: BigNumber; 189 | 190 | given(async () => { 191 | timestamp = await adapter.getTimestamp(LATEST_ROUND); 192 | }); 193 | then('proxy is called correctly', () => { 194 | expect(api3Proxy.read).to.have.been.calledOnce; 195 | }); 196 | then('value is returned correctly', () => { 197 | expect(timestamp).to.equal(TIMESTAMP); 198 | }); 199 | }); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /test/unit/adapters/dia-chainlink-adapter/dia-chainlink-adapter-factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { given, then, when } from '@utils/bdd'; 4 | import { DIAChainlinkAdapter__factory, DIAChainlinkAdapterFactory__factory, DIAChainlinkAdapterFactory } from '@typechained'; 5 | import { TransactionResponse } from 'ethers/node_modules/@ethersproject/providers'; 6 | import { snapshot } from '@utils/evm'; 7 | 8 | describe('DIAChainlinkAdapterFactory', () => { 9 | const ORACLE_ADDRESS = '0xa93546947f3015c986695750b8bbEa8e26D65856'; 10 | const ORACLE_DECIMALS = 8; 11 | const DECIMALS = 8; 12 | const DESCRIPTION = 'ETH/USD'; 13 | 14 | let factory: DIAChainlinkAdapterFactory; 15 | let snapshotId: string; 16 | 17 | before(async () => { 18 | const factoryFactory: DIAChainlinkAdapterFactory__factory = await ethers.getContractFactory('DIAChainlinkAdapterFactory'); 19 | factory = await factoryFactory.deploy(); 20 | snapshotId = await snapshot.take(); 21 | }); 22 | 23 | beforeEach(async () => { 24 | await snapshot.revert(snapshotId); 25 | }); 26 | 27 | describe('createAdapter', () => { 28 | when('adapter is created', () => { 29 | let expectedAddress: string; 30 | let tx: TransactionResponse; 31 | given(async () => { 32 | expectedAddress = await factory.computeAdapterAddress(ORACLE_ADDRESS, ORACLE_DECIMALS, DECIMALS, DESCRIPTION); 33 | tx = await factory.createAdapter(ORACLE_ADDRESS, ORACLE_DECIMALS, DECIMALS, DESCRIPTION); 34 | }); 35 | then('event is emitted', async () => { 36 | await expect(tx).to.emit(factory, 'AdapterCreated').withArgs(expectedAddress); 37 | }); 38 | then('contract was deployed correctly', async () => { 39 | const adapter = DIAChainlinkAdapter__factory.connect(expectedAddress, ethers.provider); 40 | expect(await adapter.DIA_ORACLE()).to.equal(ORACLE_ADDRESS); 41 | expect(await adapter.decimals()).to.equal(DECIMALS); 42 | expect(await adapter.description()).to.equal(DESCRIPTION); 43 | }); 44 | }); 45 | when('adapter is created twice', () => { 46 | given(async () => { 47 | await factory.createAdapter(ORACLE_ADDRESS, ORACLE_DECIMALS, DECIMALS, DESCRIPTION); 48 | }); 49 | then('the second time reverts', async () => { 50 | const tx = factory.createAdapter(ORACLE_ADDRESS, ORACLE_DECIMALS, DECIMALS, DESCRIPTION); 51 | await expect(tx).to.have.reverted; 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/unit/adapters/dia-chainlink-adapter/dia-chainlink-adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { BigNumber, utils } from 'ethers'; 4 | import { behaviours } from '@utils'; 5 | import { given, then, when } from '@utils/bdd'; 6 | import { IDIAOracleV2, DIAChainlinkAdapter, DIAChainlinkAdapter__factory } from '@typechained'; 7 | import { snapshot } from '@utils/evm'; 8 | import { smock, FakeContract } from '@defi-wonderland/smock'; 9 | 10 | chai.use(smock.matchers); 11 | 12 | describe('DIAChainlinkAdapter', () => { 13 | const ORACLE_DECIMALS = 8; 14 | const FEED_DECIMALS = 8; 15 | const DESCRIPTION = 'ETH/USD'; 16 | const VALUE_WITH_8_DECIMALS = utils.parseUnits('1.2345', 8); 17 | const TIMESTAMP = 678910; 18 | const LATEST_ROUND = 0; 19 | 20 | let adapter: DIAChainlinkAdapter; 21 | let diaOracle: FakeContract; 22 | let snapshotId: string; 23 | 24 | before(async () => { 25 | diaOracle = await smock.fake('IDIAOracleV2'); 26 | const factory: DIAChainlinkAdapter__factory = await ethers.getContractFactory('DIAChainlinkAdapter'); 27 | adapter = await factory.deploy(diaOracle.address, ORACLE_DECIMALS, FEED_DECIMALS, DESCRIPTION); 28 | snapshotId = await snapshot.take(); 29 | }); 30 | 31 | beforeEach(async () => { 32 | await snapshot.revert(snapshotId); 33 | diaOracle.getValue.reset(); 34 | diaOracle.getValue.returns([VALUE_WITH_8_DECIMALS, TIMESTAMP]); 35 | }); 36 | 37 | describe('constructor', () => { 38 | when('deployed', () => { 39 | then('description is set correctly', async () => { 40 | const description = await adapter.description(); 41 | expect(description).to.equal(DESCRIPTION); 42 | }); 43 | then('decimals is set correctly', async () => { 44 | expect(await adapter.decimals()).to.equal(FEED_DECIMALS); 45 | }); 46 | then('DIA oracle is set correctly', async () => { 47 | expect(await adapter.DIA_ORACLE()).to.equal(diaOracle.address); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('version', () => { 53 | when('called', () => { 54 | then('value is returned correctly', async () => { 55 | expect(await adapter.version()).to.equal(4); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('getRoundData', () => { 61 | when('called with an invalid round', () => { 62 | then('reverts with message', async () => { 63 | await behaviours.txShouldRevertWithMessage({ 64 | contract: adapter, 65 | func: 'getRoundData', 66 | args: [1], 67 | message: 'OnlyLatestRoundIsAvailable', 68 | }); 69 | }); 70 | }); 71 | when('called with the latest round', () => { 72 | let _roundId: BigNumber, _answer: BigNumber, _startedAt: BigNumber, _updatedAt: BigNumber, _answeredInRound: BigNumber; 73 | 74 | given(async () => { 75 | ({ _roundId, _answer, _startedAt, _updatedAt, _answeredInRound } = await adapter.getRoundData(LATEST_ROUND)); 76 | }); 77 | then('oracle is called correctly', () => { 78 | expect(diaOracle.getValue).to.have.been.calledOnce; 79 | }); 80 | then('value is returned correctly', () => { 81 | expect(_roundId).to.equal(LATEST_ROUND); 82 | expect(_answer).to.equal(VALUE_WITH_8_DECIMALS); 83 | expect(_startedAt).to.equal(TIMESTAMP); 84 | expect(_updatedAt).to.equal(TIMESTAMP); 85 | expect(_answeredInRound).to.equal(LATEST_ROUND); 86 | }); 87 | }); 88 | }); 89 | 90 | describe('latestRoundData', () => { 91 | when('called', () => { 92 | let _roundId: BigNumber, _answer: BigNumber, _startedAt: BigNumber, _updatedAt: BigNumber, _answeredInRound: BigNumber; 93 | 94 | given(async () => { 95 | ({ _roundId, _answer, _startedAt, _updatedAt, _answeredInRound } = await adapter.latestRoundData()); 96 | }); 97 | then('oracle is called correctly', () => { 98 | expect(diaOracle.getValue).to.have.been.calledOnce; 99 | }); 100 | then('value is returned correctly', () => { 101 | expect(_roundId).to.equal(LATEST_ROUND); 102 | expect(_answer).to.equal(VALUE_WITH_8_DECIMALS); 103 | expect(_startedAt).to.equal(TIMESTAMP); 104 | expect(_updatedAt).to.equal(TIMESTAMP); 105 | expect(_answeredInRound).to.equal(LATEST_ROUND); 106 | }); 107 | }); 108 | }); 109 | 110 | describe('latestAnswer', () => { 111 | when('called', () => { 112 | let answer: BigNumber; 113 | 114 | given(async () => { 115 | answer = await adapter.latestAnswer(); 116 | }); 117 | then('oracle is called correctly', () => { 118 | expect(diaOracle.getValue).to.have.been.calledOnce; 119 | }); 120 | then('value is returned correctly', () => { 121 | expect(answer).to.equal(VALUE_WITH_8_DECIMALS); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('latestTimestamp', () => { 127 | when('called', () => { 128 | let timestamp: BigNumber; 129 | 130 | given(async () => { 131 | timestamp = await adapter.latestTimestamp(); 132 | }); 133 | then('oracle is called correctly', () => { 134 | expect(diaOracle.getValue).to.have.been.calledOnce; 135 | }); 136 | then('value is returned correctly', () => { 137 | expect(timestamp).to.equal(TIMESTAMP); 138 | }); 139 | }); 140 | }); 141 | 142 | describe('latestRound', () => { 143 | when('called', () => { 144 | then('value is returned correctly', async () => { 145 | expect(await adapter.latestRound()).to.equal(LATEST_ROUND); 146 | }); 147 | }); 148 | }); 149 | 150 | describe('getAnswer', () => { 151 | when('called with an invalid round', () => { 152 | then('reverts with message', async () => { 153 | await behaviours.txShouldRevertWithMessage({ 154 | contract: adapter, 155 | func: 'getAnswer', 156 | args: [1], 157 | message: 'OnlyLatestRoundIsAvailable', 158 | }); 159 | }); 160 | }); 161 | when('called with the latest round', () => { 162 | let answer: BigNumber; 163 | 164 | given(async () => { 165 | answer = await adapter.getAnswer(LATEST_ROUND); 166 | }); 167 | then('oracle is called correctly', () => { 168 | expect(diaOracle.getValue).to.have.been.calledOnce; 169 | }); 170 | then('value is returned correctly', () => { 171 | expect(answer).to.equal(VALUE_WITH_8_DECIMALS); 172 | }); 173 | }); 174 | }); 175 | 176 | describe('getTimestamp', () => { 177 | when('called with an invalid round', () => { 178 | then('reverts with message', async () => { 179 | await behaviours.txShouldRevertWithMessage({ 180 | contract: adapter, 181 | func: 'getTimestamp', 182 | args: [1], 183 | message: 'OnlyLatestRoundIsAvailable', 184 | }); 185 | }); 186 | }); 187 | when('called with the latest round', () => { 188 | let timestamp: BigNumber; 189 | 190 | given(async () => { 191 | timestamp = await adapter.getTimestamp(LATEST_ROUND); 192 | }); 193 | then('oracle is called correctly', () => { 194 | expect(diaOracle.getValue).to.have.been.calledOnce; 195 | }); 196 | then('value is returned correctly', () => { 197 | expect(timestamp).to.equal(TIMESTAMP); 198 | }); 199 | }); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /test/unit/base/simple-oracle.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { BigNumber, constants } from 'ethers'; 4 | import { behaviours } from '@utils'; 5 | import { given, then, when } from '@utils/bdd'; 6 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 7 | import { SimpleOracleMock, SimpleOracleMock__factory, ITokenPriceOracle } from '@typechained'; 8 | import { snapshot } from '@utils/evm'; 9 | import { smock, FakeContract } from '@defi-wonderland/smock'; 10 | import { shouldBeExecutableOnlyByRole } from '@utils/behaviours'; 11 | import { TransactionResponse } from 'ethers/node_modules/@ethersproject/providers'; 12 | 13 | chai.use(smock.matchers); 14 | 15 | describe('SimpleOracle', () => { 16 | const TOKEN_A = '0x0000000000000000000000000000000000000001'; 17 | const TOKEN_B = '0x0000000000000000000000000000000000000002'; 18 | const BYTES = '0xf2c047db4a7cf81f935c'; // Some random bytes 19 | let oracle: SimpleOracleMock; 20 | let snapshotId: string; 21 | 22 | before('Setup accounts and contracts', async () => { 23 | const factory: SimpleOracleMock__factory = await ethers.getContractFactory('solidity/contracts/test/base/SimpleOracle.sol:SimpleOracleMock'); 24 | oracle = await factory.deploy(); 25 | snapshotId = await snapshot.take(); 26 | }); 27 | 28 | beforeEach('Deploy and configure', async () => { 29 | await snapshot.revert(snapshotId); 30 | }); 31 | 32 | describe('addOrModifySupportForPair', () => { 33 | when('function is called', () => { 34 | given(async () => { 35 | await oracle.addOrModifySupportForPair(TOKEN_A, TOKEN_B, BYTES); 36 | }); 37 | then('internal version is called directly', async () => { 38 | const lastCall = await oracle.lastCall(); 39 | expect(lastCall.tokenA).to.equal(TOKEN_A); 40 | expect(lastCall.tokenB).to.equal(TOKEN_B); 41 | expect(lastCall.data).to.equal(BYTES); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('addSupportForPairIfNeeded', () => { 47 | when('pair is already supported', () => { 48 | given(async () => { 49 | await oracle.setPairAlreadySupported(TOKEN_A, TOKEN_B); 50 | await oracle.addSupportForPairIfNeeded(TOKEN_A, TOKEN_B, BYTES); 51 | }); 52 | then('internal version is not called', async () => { 53 | const lastCall = await oracle.lastCall(); 54 | expect(lastCall.tokenA).to.equal(constants.AddressZero); 55 | }); 56 | }); 57 | when('pair is not supported yet', () => { 58 | given(async () => { 59 | await oracle.addSupportForPairIfNeeded(TOKEN_A, TOKEN_B, BYTES); 60 | }); 61 | then('internal version is called directly', async () => { 62 | const lastCall = await oracle.lastCall(); 63 | expect(lastCall.tokenA).to.equal(TOKEN_A); 64 | expect(lastCall.tokenB).to.equal(TOKEN_B); 65 | expect(lastCall.data).to.equal(BYTES); 66 | }); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/unit/identity-oracle.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ethers } from 'hardhat'; 3 | import { behaviours } from '@utils'; 4 | import { then, when } from '@utils/bdd'; 5 | import { IdentityOracle, IdentityOracle__factory } from '@typechained'; 6 | import { snapshot } from '@utils/evm'; 7 | 8 | describe('IdentityOracle', () => { 9 | const TOKEN_A = '0x0000000000000000000000000000000000000001'; 10 | const TOKEN_B = '0x0000000000000000000000000000000000000002'; 11 | const BYTES = '0xf2c047db4a7cf81f935c'; // Some random bytes 12 | let oracle: IdentityOracle; 13 | let snapshotId: string; 14 | 15 | before('Setup accounts and contracts', async () => { 16 | const factory = await ethers.getContractFactory('solidity/contracts/IdentityOracle.sol:IdentityOracle'); 17 | oracle = await factory.deploy(); 18 | snapshotId = await snapshot.take(); 19 | }); 20 | 21 | beforeEach(async () => { 22 | await snapshot.revert(snapshotId); 23 | }); 24 | 25 | describe('canSupportPair', () => { 26 | when('asking for the same tokens', () => { 27 | then('pair can be supported', async () => { 28 | expect(await oracle.canSupportPair(TOKEN_A, TOKEN_A)).to.be.true; 29 | }); 30 | }); 31 | when('asking for different tokens', () => { 32 | then('pair cannot be supported', async () => { 33 | expect(await oracle.canSupportPair(TOKEN_A, TOKEN_B)).to.be.false; 34 | }); 35 | }); 36 | }); 37 | 38 | describe('isPairAlreadySupported', () => { 39 | when('asking for the same tokens', () => { 40 | then('pair is already supported', async () => { 41 | expect(await oracle.isPairAlreadySupported(TOKEN_A, TOKEN_A)).to.be.true; 42 | }); 43 | }); 44 | when('asking for different tokens', () => { 45 | then('pair is not already supported', async () => { 46 | expect(await oracle.isPairAlreadySupported(TOKEN_A, TOKEN_B)).to.be.false; 47 | }); 48 | }); 49 | }); 50 | 51 | describe('quote', () => { 52 | when('quoting for different tokens', () => { 53 | then('tx is reverted with reason', async () => { 54 | await behaviours.txShouldRevertWithMessage({ 55 | contract: oracle, 56 | func: 'quote', 57 | args: [TOKEN_A, 1000, TOKEN_B, BYTES], 58 | message: `PairNotSupportedYet`, 59 | }); 60 | }); 61 | }); 62 | when('quoting for the same tokens', () => { 63 | then('result is the same as amount in', async () => { 64 | const amountOut = await oracle.quote(TOKEN_A, 1000, TOKEN_A, BYTES); 65 | expect(amountOut).to.equal(1000); 66 | }); 67 | }); 68 | }); 69 | 70 | addSupportForPairTest('addOrModifySupportForPair'); 71 | 72 | addSupportForPairTest('addSupportForPairIfNeeded'); 73 | 74 | function addSupportForPairTest(method: 'addOrModifySupportForPair' | 'addSupportForPairIfNeeded') { 75 | describe(method, () => { 76 | when('trying to support different tokens', () => { 77 | then('tx is reverted with reason', async () => { 78 | await behaviours.txShouldRevertWithMessage({ 79 | contract: oracle, 80 | func: method, 81 | args: [TOKEN_A, TOKEN_B, BYTES], 82 | message: `PairCannotBeSupported`, 83 | }); 84 | }); 85 | }); 86 | when('trying to support a pair with the same tokens', () => { 87 | then('tx does not fail', async () => { 88 | await oracle[method](TOKEN_A, TOKEN_A, BYTES); 89 | }); 90 | }); 91 | }); 92 | } 93 | }); 94 | -------------------------------------------------------------------------------- /test/unit/stateful-chainlink-oracle.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { TransactionResponse } from '@ethersproject/abstract-provider'; 3 | import { ethers } from 'hardhat'; 4 | import { behaviours, wallet } from '@utils'; 5 | import { given, then, when } from '@utils/bdd'; 6 | import { 7 | FeedRegistryInterface, 8 | IAccessControl__factory, 9 | IERC165__factory, 10 | IERC20__factory, 11 | IStatefulChainlinkOracle__factory, 12 | ITokenPriceOracle__factory, 13 | Multicall__factory, 14 | StatefulChainlinkOracleMock, 15 | StatefulChainlinkOracleMock__factory, 16 | } from '@typechained'; 17 | import { snapshot } from '@utils/evm'; 18 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 19 | import { FakeContract, smock } from '@defi-wonderland/smock'; 20 | import moment from 'moment'; 21 | import { constants } from 'ethers'; 22 | 23 | describe('StatefulChainlinkOracle', () => { 24 | const ONE_DAY = moment.duration('24', 'hours').asSeconds(); 25 | const TOKEN_A = '0x0000000000000000000000000000000000000001'; 26 | const TOKEN_B = '0x0000000000000000000000000000000000000002'; 27 | const NO_PLAN = 0; 28 | const A_PLAN = 1; 29 | 30 | let superAdmin: SignerWithAddress, admin: SignerWithAddress; 31 | let feedRegistry: FakeContract; 32 | let chainlinkOracleFactory: StatefulChainlinkOracleMock__factory; 33 | let chainlinkOracle: StatefulChainlinkOracleMock; 34 | let superAdminRole: string, adminRole: string; 35 | let snapshotId: string; 36 | 37 | before('Setup accounts and contracts', async () => { 38 | [, superAdmin, admin] = await ethers.getSigners(); 39 | chainlinkOracleFactory = await ethers.getContractFactory('StatefulChainlinkOracleMock'); 40 | feedRegistry = await smock.fake('FeedRegistryInterface'); 41 | chainlinkOracle = await chainlinkOracleFactory.deploy(feedRegistry.address, superAdmin.address, [admin.address]); 42 | superAdminRole = await chainlinkOracle.SUPER_ADMIN_ROLE(); 43 | adminRole = await chainlinkOracle.ADMIN_ROLE(); 44 | snapshotId = await snapshot.take(); 45 | }); 46 | 47 | beforeEach('Deploy and configure', async () => { 48 | await snapshot.revert(snapshotId); 49 | feedRegistry.latestRoundData.reset(); 50 | }); 51 | 52 | describe('constructor', () => { 53 | when('feed registry is zero address', () => { 54 | then('tx is reverted with reason error', async () => { 55 | await behaviours.deployShouldRevertWithMessage({ 56 | contract: chainlinkOracleFactory, 57 | args: [constants.AddressZero, superAdmin.address, [admin.address]], 58 | message: 'ZeroAddress', 59 | }); 60 | }); 61 | }); 62 | when('super admin is zero address', () => { 63 | then('tx is reverted with reason error', async () => { 64 | await behaviours.deployShouldRevertWithMessage({ 65 | contract: chainlinkOracleFactory, 66 | args: [feedRegistry.address, constants.AddressZero, [admin.address]], 67 | message: 'ZeroAddress', 68 | }); 69 | }); 70 | }); 71 | when('all arguments are valid', () => { 72 | then('registry is set correctly', async () => { 73 | const registry = await chainlinkOracle.registry(); 74 | expect(registry).to.eql(feedRegistry.address); 75 | }); 76 | then('max delay is set correctly', async () => { 77 | const maxDelay = await chainlinkOracle.MAX_DELAY(); 78 | expect(maxDelay).to.eql(ONE_DAY); 79 | }); 80 | then('super admin is set correctly', async () => { 81 | const hasRole = await chainlinkOracle.hasRole(superAdminRole, superAdmin.address); 82 | expect(hasRole).to.be.true; 83 | }); 84 | then('initial admins are set correctly', async () => { 85 | const hasRole = await chainlinkOracle.hasRole(adminRole, admin.address); 86 | expect(hasRole).to.be.true; 87 | }); 88 | then('super admin role is set as super admin role', async () => { 89 | const admin = await chainlinkOracle.getRoleAdmin(superAdminRole); 90 | expect(admin).to.equal(superAdminRole); 91 | }); 92 | then('super admin role is set as admin role', async () => { 93 | const admin = await chainlinkOracle.getRoleAdmin(adminRole); 94 | expect(admin).to.equal(superAdminRole); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('canSupportPair', () => { 100 | when('no plan can be found for pair', () => { 101 | then('pair is not supported', async () => { 102 | expect(await chainlinkOracle.canSupportPair(TOKEN_A, TOKEN_B)).to.be.false; 103 | }); 104 | }); 105 | when('a plan can be found for a pair', () => { 106 | given(async () => { 107 | await chainlinkOracle.determinePricingPlan(TOKEN_A, TOKEN_B, A_PLAN); 108 | }); 109 | then('pair is supported', async () => { 110 | expect(await chainlinkOracle.canSupportPair(TOKEN_A, TOKEN_B)).to.be.true; 111 | }); 112 | then('pair is supported even when tokens are reversed', async () => { 113 | expect(await chainlinkOracle.canSupportPair(TOKEN_B, TOKEN_A)).to.be.true; 114 | }); 115 | }); 116 | when('tokens are the same', () => { 117 | then('pair is supported', async () => { 118 | expect(await chainlinkOracle.canSupportPair(TOKEN_A, TOKEN_A)).to.be.true; 119 | }); 120 | }); 121 | }); 122 | 123 | describe('isPairAlreadySupported', () => { 124 | when('there is no pricing plan', () => { 125 | let isAlreadySupported: boolean; 126 | given(async () => { 127 | await chainlinkOracle.determinePricingPlan(TOKEN_A, TOKEN_B, NO_PLAN); 128 | isAlreadySupported = await chainlinkOracle.isPairAlreadySupported(TOKEN_A, TOKEN_B); 129 | }); 130 | then('pair is not already supported', async () => { 131 | expect(isAlreadySupported).to.be.false; 132 | }); 133 | }); 134 | when('there is a pricing plan', () => { 135 | let isAlreadySupported: boolean; 136 | given(async () => { 137 | await chainlinkOracle.setPlanForPair(TOKEN_A, TOKEN_B, A_PLAN); 138 | isAlreadySupported = await chainlinkOracle.isPairAlreadySupported(TOKEN_A, TOKEN_B); 139 | }); 140 | then('pair is already supported', async () => { 141 | expect(isAlreadySupported).to.be.true; 142 | }); 143 | }); 144 | when('sending the tokens in inverse order', () => { 145 | let isAlreadySupported: boolean; 146 | given(async () => { 147 | await chainlinkOracle.setPlanForPair(TOKEN_A, TOKEN_B, A_PLAN); 148 | isAlreadySupported = await chainlinkOracle.isPairAlreadySupported(TOKEN_B, TOKEN_A); 149 | }); 150 | then('pair is already supported', async () => { 151 | expect(isAlreadySupported).to.be.true; 152 | }); 153 | }); 154 | }); 155 | 156 | describe('internalAddSupportForPair', () => { 157 | when('no plan can be found for pair', () => { 158 | given(async () => { 159 | await chainlinkOracle.determinePricingPlan(TOKEN_A, TOKEN_B, NO_PLAN); 160 | }); 161 | then('tx is reverted with reason', async () => { 162 | await behaviours.txShouldRevertWithMessage({ 163 | contract: chainlinkOracle, 164 | func: 'internalAddOrModifySupportForPair', 165 | args: [TOKEN_A, TOKEN_B, []], 166 | message: 'PairCannotBeSupported', 167 | }); 168 | }); 169 | }); 170 | when('a plan can be calculated for the pair', () => { 171 | const SOME_OTHER_PLAN = 2; 172 | let tx: TransactionResponse; 173 | given(async () => { 174 | await chainlinkOracle.determinePricingPlan(TOKEN_A, TOKEN_B, SOME_OTHER_PLAN); 175 | tx = await chainlinkOracle.internalAddOrModifySupportForPair(TOKEN_A, TOKEN_B, []); 176 | }); 177 | then(`it is marked as the new plan`, async () => { 178 | expect(await chainlinkOracle.planForPair(TOKEN_A, TOKEN_B)).to.eql(SOME_OTHER_PLAN); 179 | }); 180 | 181 | then('event is emitted', async () => { 182 | await expect(tx).to.emit(chainlinkOracle, 'UpdatedPlanForPair').withArgs(TOKEN_A, TOKEN_B, SOME_OTHER_PLAN); 183 | }); 184 | when('a pair loses support', () => { 185 | let tx: TransactionResponse; 186 | given(async () => { 187 | await chainlinkOracle.setPlanForPair(TOKEN_A, TOKEN_B, A_PLAN); 188 | await chainlinkOracle.determinePricingPlan(TOKEN_A, TOKEN_B, NO_PLAN); 189 | tx = await chainlinkOracle.internalAddOrModifySupportForPair(TOKEN_A, TOKEN_B, []); 190 | }); 191 | then('pair is left with no plan', async () => { 192 | expect(await chainlinkOracle.planForPair(TOKEN_A, TOKEN_B)).to.eql(NO_PLAN); 193 | }); 194 | 195 | then('event is emitted', async () => { 196 | await expect(tx).to.emit(chainlinkOracle, 'UpdatedPlanForPair').withArgs(TOKEN_A, TOKEN_B, NO_PLAN); 197 | }); 198 | }); 199 | }); 200 | }); 201 | 202 | describe('addMappings', () => { 203 | when('input sizes do not match', () => { 204 | then('tx is reverted with reason', async () => { 205 | await behaviours.txShouldRevertWithMessage({ 206 | contract: chainlinkOracle.connect(admin), 207 | func: 'addMappings', 208 | args: [[TOKEN_A], [TOKEN_A, TOKEN_B]], 209 | message: 'InvalidMappingsInput', 210 | }); 211 | }); 212 | }); 213 | when('function is called by admin', () => { 214 | const TOKEN_ADDRESS = wallet.generateRandomAddress(); 215 | let tx: TransactionResponse; 216 | given(async () => { 217 | tx = await chainlinkOracle.connect(admin).addMappings([TOKEN_A], [TOKEN_ADDRESS]); 218 | }); 219 | then('mapping is registered', async () => { 220 | expect(await chainlinkOracle.mappedToken(TOKEN_A)).to.equal(TOKEN_ADDRESS); 221 | }); 222 | then('event is emmitted', async () => { 223 | await expect(tx).to.emit(chainlinkOracle, 'MappingsAdded').withArgs([TOKEN_A], [TOKEN_ADDRESS]); 224 | }); 225 | }); 226 | behaviours.shouldBeExecutableOnlyByRole({ 227 | contract: () => chainlinkOracle, 228 | funcAndSignature: 'addMappings(address[],address[])', 229 | params: [[TOKEN_A], [wallet.generateRandomAddress()]], 230 | role: () => adminRole, 231 | addressWithRole: () => admin, 232 | }); 233 | }); 234 | describe('supportsInterface', () => { 235 | behaviours.shouldSupportInterface({ 236 | contract: () => chainlinkOracle, 237 | interfaceName: 'IERC165', 238 | interface: IERC165__factory.createInterface(), 239 | }); 240 | behaviours.shouldSupportInterface({ 241 | contract: () => chainlinkOracle, 242 | interfaceName: 'Multicall', 243 | interface: Multicall__factory.createInterface(), 244 | }); 245 | behaviours.shouldSupportInterface({ 246 | contract: () => chainlinkOracle, 247 | interfaceName: 'ITokenPriceOracle', 248 | interface: ITokenPriceOracle__factory.createInterface(), 249 | }); 250 | behaviours.shouldSupportInterface({ 251 | contract: () => chainlinkOracle, 252 | interfaceName: 'ITransformerOracle', 253 | interface: { 254 | actual: IStatefulChainlinkOracle__factory.createInterface(), 255 | inheritedFrom: [ITokenPriceOracle__factory.createInterface()], 256 | }, 257 | }); 258 | behaviours.shouldSupportInterface({ 259 | contract: () => chainlinkOracle, 260 | interfaceName: 'IAccessControl', 261 | interface: IAccessControl__factory.createInterface(), 262 | }); 263 | behaviours.shouldNotSupportInterface({ 264 | contract: () => chainlinkOracle, 265 | interfaceName: 'IERC20', 266 | interface: IERC20__factory.createInterface(), 267 | }); 268 | }); 269 | 270 | describe('intercalCallRegistry', () => { 271 | when('price is negative', () => { 272 | given(() => makeRegistryReturn({ price: -1 })); 273 | thenRegistryCallRevertsWithReason('InvalidPrice'); 274 | }); 275 | when('price is zero', () => { 276 | given(() => makeRegistryReturn({ price: 0 })); 277 | thenRegistryCallRevertsWithReason('InvalidPrice'); 278 | }); 279 | when('last update was > 24hs ago', () => { 280 | const LAST_UPDATE_AGO = moment.duration('24', 'hours').as('seconds') + moment.duration('15', 'minutes').as('seconds'); 281 | given(() => makeRegistryReturn({ lastUpdate: moment().unix() - LAST_UPDATE_AGO })); 282 | thenRegistryCallRevertsWithReason('LastUpdateIsTooOld'); 283 | }); 284 | when('call to the registry reverts', () => { 285 | const NO_REASON = ''; 286 | given(() => feedRegistry.latestRoundData.reverts(NO_REASON)); 287 | thenRegistryCallRevertsWithReason(NO_REASON); 288 | }); 289 | function makeRegistryReturn({ price, lastUpdate }: { price?: number; lastUpdate?: number }) { 290 | feedRegistry.latestRoundData.returns([0, price ?? 1, 0, lastUpdate ?? moment().unix(), 0]); 291 | } 292 | async function thenRegistryCallRevertsWithReason(reason: string) { 293 | then('_callRegistry reverts with reason', async () => { 294 | await behaviours.txShouldRevertWithMessage({ 295 | contract: chainlinkOracle, 296 | func: 'intercalCallRegistry', 297 | args: [TOKEN_A, TOKEN_B], 298 | message: reason, 299 | }); 300 | }); 301 | } 302 | }); 303 | }); 304 | -------------------------------------------------------------------------------- /test/utils/bdd.ts: -------------------------------------------------------------------------------- 1 | import { Suite, SuiteFunction } from 'mocha'; 2 | 3 | export const then = it; 4 | export const given = beforeEach; 5 | export const when: SuiteFunction = function (title: string, fn: (this: Suite) => void) { 6 | context('when ' + title, fn); 7 | }; 8 | when.only = (title: string, fn?: (this: Suite) => void) => context.only('when ' + title, fn!); 9 | when.skip = (title: string, fn: (this: Suite) => void) => context.skip('when ' + title, fn); 10 | 11 | export const contract = describe; 12 | -------------------------------------------------------------------------------- /test/utils/behaviours.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import chai from 'chai'; 4 | import { Contract, ContractFactory, ContractInterface, Signer, utils, Wallet } from 'ethers'; 5 | import { TransactionResponse } from '@ethersproject/abstract-provider'; 6 | import { Provider } from '@ethersproject/providers'; 7 | import { getStatic } from 'ethers/lib/utils'; 8 | import { wallet } from '.'; 9 | import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; 10 | import { given, then, when } from './bdd'; 11 | import { ERC615Interface, getInterfaceId } from './erc165'; 12 | 13 | chai.use(chaiAsPromised); 14 | 15 | type Impersonator = Signer | Provider | string; 16 | 17 | export const checkTxRevertedWithMessage = async ({ 18 | tx, 19 | message, 20 | }: { 21 | tx: Promise; 22 | message: RegExp | string; 23 | }): Promise => { 24 | await expect(tx).to.be.reverted; 25 | if (message instanceof RegExp) { 26 | await expect(tx).eventually.rejected.have.property('message').match(message); 27 | } else { 28 | await expect(tx).to.be.revertedWith(message); 29 | } 30 | }; 31 | 32 | export const checkTxRevertedWithZeroAddress = async (tx: Promise): Promise => { 33 | await checkTxRevertedWithMessage({ 34 | tx, 35 | message: /zero\saddress/, 36 | }); 37 | }; 38 | 39 | export const deployShouldRevertWithZeroAddress = async ({ contract, args }: { contract: ContractFactory; args: any[] }): Promise => { 40 | const deployContractTx = await contract.getDeployTransaction(...args); 41 | const tx = contract.signer.sendTransaction(deployContractTx); 42 | await checkTxRevertedWithZeroAddress(tx); 43 | }; 44 | 45 | export const deployShouldRevertWithMessage = async ({ 46 | contract, 47 | args, 48 | message, 49 | }: { 50 | contract: ContractFactory; 51 | args: any[]; 52 | message: string; 53 | }): Promise => { 54 | const deployContractTx = await contract.getDeployTransaction(...args); 55 | const tx = contract.signer.sendTransaction(deployContractTx); 56 | await checkTxRevertedWithMessage({ tx, message }); 57 | }; 58 | 59 | export const txShouldRevertWithZeroAddress = async ({ 60 | contract, 61 | func, 62 | args, 63 | }: { 64 | contract: Contract; 65 | func: string; 66 | args: any[]; 67 | tx?: Promise; 68 | }): Promise => { 69 | const tx = contract[func](...args); 70 | await checkTxRevertedWithZeroAddress(tx); 71 | }; 72 | 73 | export const txShouldRevertWithMessage = async ({ 74 | contract, 75 | func, 76 | args, 77 | message, 78 | }: { 79 | contract: Contract; 80 | func: string; 81 | args: any[]; 82 | message: string; 83 | }): Promise => { 84 | const tx = contract[func](...args); 85 | await checkTxRevertedWithMessage({ tx, message }); 86 | }; 87 | 88 | export const checkTxEmittedEvents = async ({ 89 | contract, 90 | tx, 91 | events, 92 | }: { 93 | contract: Contract; 94 | tx: TransactionResponse; 95 | events: { name: string; args: any[] }[]; 96 | }): Promise => { 97 | for (let i = 0; i < events.length; i++) { 98 | await expect(tx) 99 | .to.emit(contract, events[i].name) 100 | .withArgs(...events[i].args); 101 | } 102 | }; 103 | 104 | export const deployShouldSetVariablesAndEmitEvents = async ({ 105 | contract, 106 | args, 107 | settersGettersVariablesAndEvents, 108 | }: { 109 | contract: ContractFactory; 110 | args: any[]; 111 | settersGettersVariablesAndEvents: { 112 | getterFunc: string; 113 | variable: any; 114 | eventEmitted: string; 115 | }[]; 116 | }): Promise => { 117 | const deployContractTx = await contract.getDeployTransaction(...args); 118 | const tx = await contract.signer.sendTransaction(deployContractTx); 119 | const address = getStatic<(tx: TransactionResponse) => string>(contract.constructor, 'getContractAddress')(tx); 120 | const deployedContract = getStatic<(address: string, contractInterface: ContractInterface, signer?: Signer) => Contract>( 121 | contract.constructor, 122 | 'getContract' 123 | )(address, contract.interface, contract.signer); 124 | await txShouldHaveSetVariablesAndEmitEvents({ 125 | contract: deployedContract, 126 | tx, 127 | settersGettersVariablesAndEvents, 128 | }); 129 | }; 130 | 131 | export const txShouldHaveSetVariablesAndEmitEvents = async ({ 132 | contract, 133 | tx, 134 | settersGettersVariablesAndEvents, 135 | }: { 136 | contract: Contract; 137 | tx: TransactionResponse; 138 | settersGettersVariablesAndEvents: { 139 | getterFunc: string; 140 | variable: any; 141 | eventEmitted: string; 142 | }[]; 143 | }): Promise => { 144 | for (let i = 0; i < settersGettersVariablesAndEvents.length; i++) { 145 | await checkTxEmittedEvents({ 146 | contract, 147 | tx, 148 | events: [ 149 | { 150 | name: settersGettersVariablesAndEvents[i].eventEmitted, 151 | args: [settersGettersVariablesAndEvents[i].variable], 152 | }, 153 | ], 154 | }); 155 | expect(await contract[settersGettersVariablesAndEvents[i].getterFunc]()).to.eq(settersGettersVariablesAndEvents[i].variable); 156 | } 157 | }; 158 | 159 | export const txShouldSetVariableAndEmitEvent = async ({ 160 | contract, 161 | setterFunc, 162 | getterFunc, 163 | variable, 164 | eventEmitted, 165 | }: { 166 | contract: Contract; 167 | setterFunc: string; 168 | getterFunc: string; 169 | variable: any; 170 | eventEmitted: string; 171 | }): Promise => { 172 | expect(await contract[getterFunc]()).to.not.eq(variable); 173 | const tx = contract[setterFunc](variable); 174 | await txShouldHaveSetVariablesAndEmitEvents({ 175 | contract, 176 | tx, 177 | settersGettersVariablesAndEvents: [ 178 | { 179 | getterFunc, 180 | variable, 181 | eventEmitted, 182 | }, 183 | ], 184 | }); 185 | }; 186 | 187 | export const fnShouldOnlyBeCallableByGovernance = ( 188 | delayedContract: () => Contract, 189 | fnName: string, 190 | governance: Impersonator, 191 | args: unknown[] | (() => unknown[]) 192 | ): void => { 193 | it('should be callable by governance', () => { 194 | return expect(callFunction(governance)).not.to.be.revertedWith('OnlyGovernance()'); 195 | }); 196 | 197 | it('should not be callable by any address', async () => { 198 | return expect(callFunction(await wallet.generateRandom())).to.be.revertedWith('OnlyGovernance()'); 199 | }); 200 | 201 | function callFunction(impersonator: Impersonator) { 202 | const argsArray: unknown[] = typeof args === 'function' ? args() : args; 203 | const fn = delayedContract().connect(impersonator)[fnName] as (...args: unknown[]) => unknown; 204 | return fn(...argsArray); 205 | } 206 | }; 207 | 208 | export const shouldSupportInterface = ({ 209 | contract, 210 | interfaceName, 211 | interface: interface_, 212 | }: { 213 | contract: () => Contract; 214 | interfaceName: string; 215 | interface: ERC615Interface; 216 | }) => { 217 | when(`asked if ${interfaceName} is supported`, () => { 218 | then('result is true', async () => { 219 | const interfaceId = getInterfaceId(interface_); 220 | expect(await contract().supportsInterface(interfaceId)).to.be.true; 221 | }); 222 | }); 223 | }; 224 | 225 | export const shouldNotSupportInterface = ({ 226 | contract, 227 | interfaceName, 228 | interface: interface_, 229 | }: { 230 | contract: () => Contract; 231 | interfaceName: string; 232 | interface: utils.Interface; 233 | }) => { 234 | when(`asked if ${interfaceName} is supported`, () => { 235 | then('result is false', async () => { 236 | const interfaceId = getInterfaceId(interface_); 237 | expect(await contract().supportsInterface(interfaceId)).to.be.false; 238 | }); 239 | }); 240 | }; 241 | 242 | export const shouldBeExecutableOnlyByRole = ({ 243 | contract, 244 | funcAndSignature, 245 | params, 246 | addressWithRole, 247 | role, 248 | }: { 249 | contract: () => Contract; 250 | funcAndSignature: string; 251 | params?: any[]; 252 | addressWithRole: () => SignerWithAddress; 253 | role: () => string; 254 | }) => { 255 | params = params ?? []; 256 | when('called from address without role', () => { 257 | let tx: Promise; 258 | let walletWithoutRole: Wallet; 259 | given(async () => { 260 | walletWithoutRole = await wallet.generateRandom(); 261 | tx = contract() 262 | .connect(walletWithoutRole) 263 | [funcAndSignature](...params!); 264 | }); 265 | then('tx is reverted with reason', async () => { 266 | await expect(tx).to.be.revertedWith(`AccessControl: account ${walletWithoutRole.address.toLowerCase()} is missing role ${role()}`); 267 | }); 268 | }); 269 | when('called from address with role', () => { 270 | let tx: Promise; 271 | given(async () => { 272 | tx = contract() 273 | .connect(addressWithRole()) 274 | [funcAndSignature](...params!); 275 | }); 276 | then('tx is not reverted or not reverted with reason missing role', async () => { 277 | await expect(tx).to.not.be.revertedWith(`AccessControl: account ${addressWithRole().address.toLowerCase()} is missing role ${role()}`); 278 | }); 279 | }); 280 | }; 281 | -------------------------------------------------------------------------------- /test/utils/bn.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, utils } from 'ethers'; 2 | import { expect } from 'chai'; 3 | 4 | export const expectToEqualWithThreshold = ({ 5 | value, 6 | to, 7 | threshold, 8 | }: { 9 | value: BigNumber | number | string; 10 | to: BigNumber | number | string; 11 | threshold: BigNumber | number | string; 12 | }): void => { 13 | value = toBN(value); 14 | to = toBN(to); 15 | threshold = toBN(threshold); 16 | expect( 17 | to.sub(threshold).lte(value) && to.add(threshold).gte(value), 18 | `Expected ${value.toString()} to be between ${to.sub(threshold).toString()} and ${to.add(threshold).toString()}` 19 | ).to.be.true; 20 | }; 21 | 22 | export const toBN = (value: string | number | BigNumber): BigNumber => { 23 | return BigNumber.isBigNumber(value) ? value : BigNumber.from(value); 24 | }; 25 | 26 | export const toUnit = (value: number): BigNumber => { 27 | return utils.parseUnits(value.toString()); 28 | }; 29 | -------------------------------------------------------------------------------- /test/utils/contracts.ts: -------------------------------------------------------------------------------- 1 | import { Contract, ContractFactory } from '@ethersproject/contracts'; 2 | import { TransactionResponse } from '@ethersproject/abstract-provider'; 3 | import { ContractInterface, Signer } from 'ethers'; 4 | import { getAddress, getStatic, ParamType, solidityKeccak256 } from 'ethers/lib/utils'; 5 | import { ethers } from 'hardhat'; 6 | 7 | export const deploy = async (contract: ContractFactory, args: any[]): Promise<{ tx: TransactionResponse; contract: Contract }> => { 8 | const deploymentTransactionRequest = await contract.getDeployTransaction(...args); 9 | const deploymentTx = await contract.signer.sendTransaction(deploymentTransactionRequest); 10 | const contractAddress = getStatic<(deploymentTx: TransactionResponse) => string>(contract.constructor, 'getContractAddress')(deploymentTx); 11 | const deployedContract = getStatic<(contractAddress: string, contractInterface: ContractInterface, signer?: Signer) => Contract>( 12 | contract.constructor, 13 | 'getContract' 14 | )(contractAddress, contract.interface, contract.signer); 15 | return { 16 | tx: deploymentTx, 17 | contract: deployedContract, 18 | }; 19 | }; 20 | 21 | export const getCreationCode = ({ 22 | bytecode, 23 | constructorArgs, 24 | }: { 25 | bytecode: string; 26 | constructorArgs: { types: string[] | ParamType[]; values: any[] }; 27 | }): string => { 28 | return `${bytecode}${ethers.utils.defaultAbiCoder.encode(constructorArgs.types, constructorArgs.values).slice(2)}`; 29 | }; 30 | 31 | export const getCreate2Address = (create2DeployerAddress: string, salt: string, bytecode: string): string => { 32 | return getAddress( 33 | '0x' + 34 | solidityKeccak256( 35 | ['bytes'], 36 | [`0xff${create2DeployerAddress.slice(2)}${salt.slice(2)}${solidityKeccak256(['bytes'], [bytecode]).slice(2)}`] 37 | ).slice(-40) 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /test/utils/defillama.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { BigNumber, utils } from 'ethers'; 3 | 4 | export const getLastPrice = async (network: string, coin: string): Promise => { 5 | return await getPrice(network, coin); 6 | }; 7 | 8 | export const getPrice = async (network: string, coin: string, timestamp?: number): Promise => { 9 | const { price } = await getTokenData(network, coin, timestamp); 10 | return price; 11 | }; 12 | 13 | export const getTokenData = async (network: string, coin: string, timestamp?: number): Promise<{ price: number; decimals: number }> => { 14 | const coinId = `${network}:${coin.toLowerCase()}`; 15 | const url = timestamp ? `https://coins.llama.fi/prices/historical/${timestamp}/${coinId}` : `https://coins.llama.fi/prices/current/${coinId}`; 16 | const response = await axios.get(url); 17 | const { coins } = response.data; 18 | return coins[coinId]; 19 | }; 20 | 21 | export const convertPriceToBigNumberWithDecimals = (price: number, decimals: number): BigNumber => { 22 | return utils.parseUnits(price.toFixed(decimals), decimals); 23 | }; 24 | 25 | export const convertPriceToNumberWithDecimals = (price: number, decimals: number): number => { 26 | return convertPriceToBigNumberWithDecimals(price, decimals).toNumber(); 27 | }; 28 | -------------------------------------------------------------------------------- /test/utils/erc165.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers'; 2 | 3 | const { makeInterfaceId } = require('@openzeppelin/test-helpers'); 4 | 5 | export type ERC615Interface = utils.Interface | { actual: utils.Interface; inheritedFrom: utils.Interface[] }; 6 | 7 | export function getInterfaceId(interface_: ERC615Interface) { 8 | let functions: string[]; 9 | if ('actual' in interface_) { 10 | const allInheritedFunctions = interface_.inheritedFrom.flatMap((int) => Object.keys(int.functions)); 11 | functions = Object.keys(interface_.actual.functions).filter((func) => !allInheritedFunctions.includes(func)); 12 | } else { 13 | functions = Object.keys(interface_.functions); 14 | } 15 | return makeInterfaceId.ERC165(functions); 16 | } 17 | -------------------------------------------------------------------------------- /test/utils/event-utils.ts: -------------------------------------------------------------------------------- 1 | import { TransactionResponse, TransactionReceipt } from '@ethersproject/abstract-provider'; 2 | import { expect } from 'chai'; 3 | 4 | export async function expectNoEventWithName(response: TransactionResponse, eventName: string) { 5 | const receipt = await response.wait(); 6 | for (const event of getEvents(receipt)) { 7 | expect(event.event).not.to.equal(eventName); 8 | } 9 | } 10 | 11 | export async function readArgFromEvent(response: TransactionResponse, eventName: string, paramName: string): Promise { 12 | const receipt = await response.wait(); 13 | for (const event of getEvents(receipt)) { 14 | if (event.event === eventName) { 15 | return event.args[paramName]; 16 | } 17 | } 18 | } 19 | 20 | export async function readArgFromEventOrFail(response: TransactionResponse, eventName: string, paramName: string): Promise { 21 | const result = await readArgFromEvent(response, eventName, paramName); 22 | if (result) { 23 | return result; 24 | } 25 | throw new Error(`Failed to find event with name ${eventName}`); 26 | } 27 | 28 | function getEvents(receipt: TransactionReceipt): Event[] { 29 | // @ts-ignore 30 | return receipt.events; 31 | } 32 | 33 | type Event = { 34 | event: string; // Event name 35 | args: any; 36 | }; 37 | -------------------------------------------------------------------------------- /test/utils/evm.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from 'ethers'; 2 | import hre, { network } from 'hardhat'; 3 | import { getNodeUrl } from 'utils/env'; 4 | 5 | export const advanceTimeAndBlock = async (time: number): Promise => { 6 | await advanceTime(time); 7 | await advanceBlocks(1); 8 | }; 9 | 10 | export const advanceToTimeAndBlock = async (time: number): Promise => { 11 | await advanceToTime(time); 12 | await advanceBlocks(1); 13 | }; 14 | 15 | export const advanceTime = async (time: number): Promise => { 16 | await network.provider.request({ 17 | method: 'evm_increaseTime', 18 | params: [time], 19 | }); 20 | }; 21 | 22 | export const advanceToTime = async (time: number): Promise => { 23 | await network.provider.request({ 24 | method: 'evm_setNextBlockTimestamp', 25 | params: [time], 26 | }); 27 | }; 28 | 29 | export const advanceBlocks = async (blocks: BigNumberish) => { 30 | blocks = !BigNumber.isBigNumber(blocks) ? BigNumber.from(`${blocks}`) : blocks; 31 | await network.provider.request({ 32 | method: 'hardhat_mine', 33 | params: [blocks.toHexString().replace('0x0', '0x')], 34 | }); 35 | }; 36 | 37 | type ForkConfig = { network: string; skipHardhatDeployFork?: boolean } & Record; 38 | export const reset = async ({ network: networkName, ...forkingConfig }: ForkConfig) => { 39 | if (!forkingConfig.skipHardhatDeployFork) { 40 | process.env.HARDHAT_DEPLOY_FORK = networkName; 41 | } 42 | const params = [ 43 | { 44 | forking: { 45 | ...forkingConfig, 46 | jsonRpcUrl: getNodeUrl(networkName), 47 | }, 48 | }, 49 | ]; 50 | await network.provider.request({ 51 | method: 'hardhat_reset', 52 | params, 53 | }); 54 | }; 55 | 56 | class SnapshotManager { 57 | snapshots: { [id: string]: string } = {}; 58 | 59 | async take(): Promise { 60 | const id = await this.takeSnapshot(); 61 | this.snapshots[id] = id; 62 | return id; 63 | } 64 | 65 | async revert(id: string): Promise { 66 | await this.revertSnapshot(this.snapshots[id]); 67 | this.snapshots[id] = await this.takeSnapshot(); 68 | } 69 | 70 | private async takeSnapshot(): Promise { 71 | return (await network.provider.request({ 72 | method: 'evm_snapshot', 73 | params: [], 74 | })) as string; 75 | } 76 | 77 | private async revertSnapshot(id: string) { 78 | await network.provider.request({ 79 | method: 'evm_revert', 80 | params: [id], 81 | }); 82 | } 83 | } 84 | 85 | export const snapshot = new SnapshotManager(); 86 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as behaviours from './behaviours'; 2 | import * as contracts from './contracts'; 3 | import * as evm from './evm'; 4 | import * as bn from './bn'; 5 | import * as wallet from './wallet'; 6 | import * as erc165 from './erc165'; 7 | 8 | export { contracts, behaviours, bn, evm, wallet, erc165 }; 9 | -------------------------------------------------------------------------------- /test/utils/uniswap.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish, utils } from 'ethers'; 2 | import bn from 'bignumber.js'; 3 | 4 | export enum FeeAmount { 5 | LOW = 500, 6 | MEDIUM = 3000, 7 | HIGH = 10000, 8 | } 9 | 10 | export const TICK_SPACINGS: { [amount in FeeAmount]: number } = { 11 | [FeeAmount.LOW]: 10, 12 | [FeeAmount.MEDIUM]: 60, 13 | [FeeAmount.HIGH]: 200, 14 | }; 15 | 16 | export const getMinTick = (tickSpacing: number) => Math.ceil(-887272 / tickSpacing) * tickSpacing; 17 | export const getMaxTick = (tickSpacing: number) => Math.floor(887272 / tickSpacing) * tickSpacing; 18 | 19 | export function getCreate2Address(factoryAddress: string, [tokenA, tokenB]: [string, string], fee: number, bytecode: string): string { 20 | const [token0, token1] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB] : [tokenB, tokenA]; 21 | const constructorArgumentsEncoded = utils.defaultAbiCoder.encode(['address', 'address', 'uint24'], [token0, token1, fee]); 22 | const create2Inputs = [ 23 | '0xff', 24 | factoryAddress, 25 | // salt 26 | utils.keccak256(constructorArgumentsEncoded), 27 | // init code. bytecode + constructor arguments 28 | utils.keccak256(bytecode), 29 | ]; 30 | const sanitizedInputs = `0x${create2Inputs.map((i) => i.slice(2)).join('')}`; 31 | return utils.getAddress(`0x${utils.keccak256(sanitizedInputs).slice(-40)}`); 32 | } 33 | 34 | export function encodePriceSqrt(reserve1: BigNumberish, reserve0: BigNumberish): BigNumber { 35 | bn.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 }); 36 | return BigNumber.from(new bn(reserve1.toString()).div(reserve0.toString()).sqrt().multipliedBy(new bn(2).pow(96)).integerValue(3).toString()); 37 | } 38 | -------------------------------------------------------------------------------- /test/utils/wallet.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, constants, Wallet } from 'ethers'; 2 | import { ethers, network } from 'hardhat'; 3 | import { JsonRpcSigner } from '@ethersproject/providers'; 4 | import { getAddress } from 'ethers/lib/utils'; 5 | import { randomHex } from 'web3-utils'; 6 | 7 | export const impersonate = async (address: string): Promise => { 8 | await network.provider.request({ 9 | method: 'hardhat_impersonateAccount', 10 | params: [address], 11 | }); 12 | return ethers.provider.getSigner(address); 13 | }; 14 | 15 | export const generateRandom = async () => { 16 | const wallet = (await Wallet.createRandom()).connect(ethers.provider); 17 | await setBalance({ 18 | account: wallet.address, 19 | balance: constants.MaxUint256, 20 | }); 21 | return wallet; 22 | }; 23 | 24 | export const setBalance = async ({ account, balance }: { account: string; balance: BigNumber }): Promise => { 25 | await ethers.provider.send('hardhat_setBalance', [account, balance.toHexString().replace('0x0', '0x')]); 26 | }; 27 | 28 | export const generateRandomAddress = () => { 29 | return getAddress(randomHex(20)); 30 | }; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "outDir": "dist", 9 | "baseUrl": ".", 10 | "paths": { 11 | "@artifacts": ["artifacts/*"], 12 | "@deploy": ["deploy/*"], 13 | "@deployments": ["deployments/*"], 14 | "@typechained": ["typechained/index"], 15 | "@utils": ["test/utils/index"], 16 | "@utils/*": ["test/utils/*"], 17 | "@integration/*": ["test/integration/*"], 18 | "@unit/*": ["test/unit/*"], 19 | "@eth-sdk-types": ["node_modules/.dethcrypto/eth-sdk-client/cjs/types"] 20 | } 21 | }, 22 | "include": ["./scripts", "./deploy", "./test"], 23 | "files": ["./hardhat.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "outDir": "dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "@artifacts": ["artifacts/*"], 14 | "@deploy": ["deploy/*"], 15 | "@deployments": ["deployments/*"], 16 | "@typechained": ["typechained/index"] 17 | } 18 | }, 19 | "exclude": ["dist", "node_modules"], 20 | "include": ["./typechained"] 21 | } 22 | -------------------------------------------------------------------------------- /utils/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | import { DeployResult } from '@0xged/hardhat-deploy/dist/types'; 3 | import { HardhatNetworkUserConfig, HardhatRuntimeEnvironment } from 'hardhat/types'; 4 | 5 | let testChainId: number; 6 | 7 | export const setTestChainId = (chainId: number): void => { 8 | testChainId = chainId; 9 | }; 10 | 11 | export const getChainId = async (hre: HardhatRuntimeEnvironment): Promise => { 12 | if (!!process.env.TEST || !!process.env.REPORT_GAS) { 13 | if (!testChainId) throw new Error('Should specify chain id of test'); 14 | return testChainId; 15 | } 16 | if (!!process.env.FORK) return getRealChainIdOfFork(hre); 17 | return parseInt(await hre.getChainId()); 18 | }; 19 | 20 | export const getRealChainIdOfFork = (hre: HardhatRuntimeEnvironment): number => { 21 | const config = hre.network.config as HardhatNetworkUserConfig; 22 | if (config.forking?.url.includes('eth')) return 1; 23 | if (config.forking?.url.includes('ftm') || config.forking?.url.includes('fantom')) return 250; 24 | if (config.forking?.url.includes('polygon')) return 137; 25 | throw new Error('Should specify chain id of fork'); 26 | }; 27 | 28 | export const shouldVerifyContract = async (deploy: DeployResult): Promise => { 29 | if (process.env.REPORT_GAS || process.env.FORK || process.env.TEST) return false; 30 | if (!deploy.newlyDeployed) return false; 31 | const txReceipt = await ethers.provider.getTransaction(deploy.receipt!.transactionHash); 32 | await txReceipt.wait(10); 33 | return true; 34 | }; 35 | -------------------------------------------------------------------------------- /utils/env.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | const MAX_ACCOUNTS = 10; 4 | 5 | export function getNodeUrl(network: string): string { 6 | if (network) { 7 | const uri = process.env[`NODE_URI_${network.toUpperCase()}`]; 8 | if (uri && uri !== '') { 9 | return uri; 10 | } 11 | } 12 | console.warn(`No node uri for network ${network}`); 13 | return ''; 14 | } 15 | 16 | export function getMnemonic(network: string): string { 17 | const mnemonic = process.env[`${network.toUpperCase()}_MNEMONIC`] as string; 18 | if (!mnemonic) { 19 | console.warn(`No mnemonic for network ${network}`); 20 | return 'test test test test test test test test test test test junk'; 21 | } 22 | return mnemonic; 23 | } 24 | 25 | export function getPrivateKeys(network: string): string[] { 26 | const privateKeys = []; 27 | for (let i = 1; i <= MAX_ACCOUNTS; i++) { 28 | const privateKey = process.env[`${network.toUpperCase()}_${i}_PRIVATE_KEY`]; 29 | if (!!privateKey) privateKeys.push(privateKey); 30 | } 31 | if (privateKeys.length === 0) { 32 | console.warn(`No private keys for network ${network}`); 33 | } 34 | return privateKeys; 35 | } 36 | 37 | type ACCOUNTS_TYPE = 'MNEMONIC' | 'PRIVATE_KEYS'; 38 | 39 | export function getAccountsType(network: string): ACCOUNTS_TYPE { 40 | const accountsType = process.env[`${network.toUpperCase()}_ACCOUNTS_TYPE`]; 41 | if (!accountsType || accountsType === 'PRIVATE_KEYS') return 'PRIVATE_KEYS'; 42 | if (accountsType != 'MNEMONIC' && accountsType != 'PRIVATE_KEYS') { 43 | console.warn(`Accounts type incorrect for network ${network} using fallback`); 44 | return 'PRIVATE_KEYS'; 45 | } 46 | return 'MNEMONIC'; 47 | } 48 | 49 | export function getAccounts(network: string): { mnemonic: string } | string[] { 50 | if (getAccountsType(network) == 'PRIVATE_KEYS') { 51 | return getPrivateKeys(network); 52 | } 53 | return { 54 | mnemonic: getMnemonic(network), 55 | }; 56 | } 57 | 58 | export function getEtherscanAPIKeys(networks: string[]): { [network: string]: string } { 59 | const apiKeys: { [network: string]: string } = {}; 60 | networks.forEach((network) => { 61 | const networkApiKey = process.env[`${network.toUpperCase()}_ETHERSCAN_API_KEY`]; 62 | if (!networkApiKey) { 63 | console.warn(`No etherscan api key for ${network}`); 64 | } else { 65 | switch (network) { 66 | case 'ethereum-ropsten': 67 | apiKeys['ropsten'] = networkApiKey; 68 | break; 69 | case 'ethereum-rinkeby': 70 | apiKeys['rinkeby'] = networkApiKey; 71 | break; 72 | case 'ethereum-goerli': 73 | apiKeys['goerli'] = networkApiKey; 74 | break; 75 | case 'ethereum-kovan': 76 | apiKeys['kovan'] = networkApiKey; 77 | break; 78 | case 'ethereum': 79 | apiKeys['mainnet'] = networkApiKey; 80 | break; 81 | case 'optimism': 82 | apiKeys['optimisticEthereum'] = networkApiKey; 83 | break; 84 | case 'optimism-kovan': 85 | apiKeys['optimisticKovan'] = networkApiKey; 86 | break; 87 | case 'arbitrum': 88 | apiKeys['arbitrumOne'] = networkApiKey; 89 | break; 90 | case 'arbitrum-rinkeby': 91 | apiKeys['arbitrumTestnet'] = networkApiKey; 92 | break; 93 | case 'polygon-mumbai': 94 | apiKeys['polygonMumbai'] = networkApiKey; 95 | break; 96 | default: 97 | apiKeys[network] = networkApiKey; 98 | break; 99 | } 100 | } 101 | }); 102 | return apiKeys; 103 | } 104 | 105 | export function isTesting(): boolean { 106 | return !!process.env.TEST; 107 | } 108 | 109 | export function isHardhatCompile(): boolean { 110 | return process.argv[process.argv.length - 1] == 'compile'; 111 | } 112 | 113 | export function isHardhatClean(): boolean { 114 | return process.argv[process.argv.length - 1] == 'clean'; 115 | } 116 | -------------------------------------------------------------------------------- /workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "eslint.workingDirectories": [{ "mode": "auto" }], 9 | "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, 10 | "solidity.packageDefaultDependenciesContractsDirectory": "", 11 | "solidity.packageDefaultDependenciesDirectory": "node_modules" 12 | }, 13 | "extensions": { 14 | "recommendations": [ 15 | "juanblanco.solidity", 16 | "esbenp.prettier-vscode", 17 | "dbaeumer.vscode-eslint", 18 | "mikestead.dotenv" 19 | ] 20 | } 21 | } 22 | --------------------------------------------------------------------------------