├── .github └── workflows │ ├── CI.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .solhint.json ├── LICENSE ├── README.md ├── foundry.toml ├── package.json ├── pyproject.toml ├── remappings.txt ├── script └── Counter.s.sol ├── src ├── ChainlinkPriceOracle.sol ├── CompoundV3YieldOracle.sol ├── UniswapV3VolatilityOracle.sol ├── interfaces │ ├── IAdmin.sol │ ├── IBlackScholes.sol │ ├── IChainlinkPriceOracleAdmin.sol │ ├── IComet.sol │ ├── ICompoundV3YieldOracle.sol │ ├── IERC20.sol │ ├── IKeep3rV2Job.sol │ ├── IOracleAdmin.sol │ ├── IPriceOracle.sol │ ├── IUniswapV3VolatilityOracle.sol │ ├── IVolatilityOracle.sol │ └── IYieldOracle.sol ├── libraries │ ├── FixedPoint96.sol │ ├── FullMath.sol │ ├── Oracle.sol │ ├── TickMath.sol │ └── Volatility.sol └── utils │ ├── Admin.sol │ └── Keep3rV2Job.sol └── test ├── ChainlinkPriceOracle.t.sol ├── CompoundV3YieldOracle.t.sol ├── UniswapV3VolatilityOracle.t.sol └── VolatilityOracle.t.sol /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | run-ci: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: actions/setup-node@v2 15 | 16 | - name: Install dev dependencies 17 | run: npm install 18 | 19 | - name: Set up python 20 | id: setup-python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: 3.9 24 | 25 | - name: Install Poetry 26 | uses: snok/install-poetry@v1 27 | 28 | - name: Load cached venv 29 | id: cached-poetry-dependencies 30 | uses: actions/cache@v2 31 | with: 32 | path: .venv 33 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 34 | 35 | - name: Install dependencies 36 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 37 | run: poetry install --no-interaction --no-root 38 | 39 | - name: Install library 40 | run: poetry install --no-interaction 41 | 42 | - name: Install Foundry 43 | uses: onbjerg/foundry-toolchain@v1 44 | with: 45 | version: nightly 46 | 47 | - name: Pull Submodules 48 | run: forge update 49 | 50 | - name: Run tests 51 | run: forge test --optimize --fork-url https://eth-mainnet.alchemyapi.io/v2/SzNcOCE77s6nmGIal7MxtuzF--q2HZgx --fork-block-number 15441384 -vv 52 | 53 | - name: Run lint check 54 | run: npm run lint:check 55 | 56 | - name: Coverage 57 | run: | 58 | forge coverage --report lcov 59 | id: coverage 60 | 61 | - uses: codecov/codecov-action@v2 62 | 63 | # Too slow to run regularly 64 | #- name: Run audit 65 | # run: poetry run slither --solc-remaps @openzeppelin=lib/openzeppelin-contracts --solc-args optimize src/ 66 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | cache/ 3 | node_modules/ 4 | package-lock.json 5 | .env 6 | .idea 7 | 8 | .vscode/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/ds-test"] 5 | path = lib/ds-test 6 | url = https://github.com/dapphub/ds-test 7 | [submodule "lib/solmate"] 8 | path = lib/solmate 9 | url = https://github.com/Rari-Capital/solmate 10 | [submodule "lib/v3-core"] 11 | path = lib/v3-core 12 | url = https://github.com/Uniswap/v3-core 13 | [submodule "lib/valorem-core"] 14 | path = lib/valorem-core 15 | url = https://github.com/valorem-labs-inc/valorem-core.git 16 | [submodule "lib/keep3r-network-v2"] 17 | path = lib/keep3r-network-v2 18 | url = https://github.com/keep3r-network/keep3r-network-v2.git 19 | [submodule "lib/chainlink"] 20 | path = lib/chainlink 21 | url = https://github.com/smartcontractkit/chainlink.git 22 | [submodule "lib/v3-periphery"] 23 | path = lib/v3-periphery 24 | url = https://github.com/Uniswap/v3-periphery.git 25 | [submodule "lib/comet"] 26 | path = lib/comet 27 | url = https://github.com/compound-finance/comet.git 28 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", ">=0.8.0"], 5 | "no-inline-assembly": ["off", ">=0.8.0"], 6 | "avoid-low-level-calls": ["off", ">=0.8.0"], 7 | "not-rely-on-time": ["off", ">=0.8.0"], 8 | "func-visibility": ["off",{"ignoreConstructors":true}] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. 4 | "Business Source License" is a trademark of MariaDB Corporation Ab. 5 | 6 | ----------------------------------------------------------------------------- 7 | 8 | Parameters 9 | 10 | Licensor: Alcibiades Capital LLC 11 | 12 | Licensed Work: Valorem Options V1 Core 13 | The Licensed Work is (c) 2021 Alcibiades Capital LLC 14 | 15 | Additional Use Grant: Any uses listed and defined at 16 | v1-options-core-license-grants.valorem.xyz 17 | 18 | Change Date: The earlier of 2026-02-01 or a date specified at 19 | v1-options-core-license-date.valorem.xyz 20 | 21 | Change License: GNU General Public License v2.0 or later 22 | 23 | ----------------------------------------------------------------------------- 24 | 25 | Terms 26 | 27 | The Licensor hereby grants you the right to copy, modify, create derivative 28 | works, redistribute, and make non-production use of the Licensed Work. The 29 | Licensor may make an Additional Use Grant, above, permitting limited 30 | production use. 31 | 32 | Effective on the Change Date, or the fourth anniversary of the first publicly 33 | available distribution of a specific version of the Licensed Work under this 34 | License, whichever comes first, the Licensor hereby grants you rights under 35 | the terms of the Change License, and the rights granted in the paragraph 36 | above terminate. 37 | 38 | If your use of the Licensed Work does not comply with the requirements 39 | currently in effect as described in this License, you must purchase a 40 | commercial license from the Licensor, its affiliated entities, or authorized 41 | resellers, or you must refrain from using the Licensed Work. 42 | 43 | All copies of the original and modified Licensed Work, and derivative works 44 | of the Licensed Work, are subject to this License. This License applies 45 | separately for each version of the Licensed Work and the Change Date may vary 46 | for each version of the Licensed Work released by Licensor. 47 | 48 | You must conspicuously display this License on each original or modified copy 49 | of the Licensed Work. If you receive the Licensed Work in original or 50 | modified form from a third party, the terms and conditions set forth in this 51 | License apply to your use of that work. 52 | 53 | Any use of the Licensed Work in violation of this License will automatically 54 | terminate your rights under this License for the current and all other 55 | versions of the Licensed Work. 56 | 57 | This License does not grant you any right in any trademark or logo of 58 | Licensor or its affiliates (provided that you may use a trademark or logo of 59 | Licensor as expressly required by this License). 60 | 61 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 62 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 63 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 64 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 65 | TITLE. 66 | 67 | MariaDB hereby grants you permission to use this License’s text to license 68 | your works, and to refer to it using the trademark "Business Source License", 69 | as long as you comply with the Covenants of Licensor below. 70 | 71 | ----------------------------------------------------------------------------- 72 | 73 | Covenants of Licensor 74 | 75 | In consideration of the right to use this License’s text and the "Business 76 | Source License" name and trademark, Licensor covenants to MariaDB, and to all 77 | other recipients of the licensed work to be provided by Licensor: 78 | 79 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 80 | or a license that is compatible with GPL Version 2.0 or a later version, 81 | where "compatible" means that software provided under the Change License can 82 | be included in a program with software provided under GPL Version 2.0 or a 83 | later version. Licensor may specify additional Change Licenses without 84 | limitation. 85 | 86 | 2. To either: (a) specify an additional grant of rights to use that does not 87 | impose any additional restriction on the right granted in this License, as 88 | the Additional Use Grant; or (b) insert the text "None". 89 | 90 | 3. To specify a Change Date. 91 | 92 | 4. Not to modify this License in any other way. 93 | 94 | ----------------------------------------------------------------------------- 95 | 96 | Notice 97 | 98 | The Business Source License (this document, or the "License") is not an Open 99 | Source license. However, the Licensed Work will eventually be made available 100 | under an Open Source License, as stated in this License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # valorem-oracles 2 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "valoremoracles", 3 | "author": "Alcibiades", 4 | "version": "1.0.0", 5 | "description": "", 6 | "homepage": "https://valorem.xyz", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/valorem-labs-inc/valorem-oracles.git" 10 | }, 11 | "scripts": { 12 | "fmt": "forge fmt", 13 | "fmt:check": "forge fmt --check", 14 | "solhint": "solhint --config ./.solhint.json 'src/**/*.sol' --fix", 15 | "solhint:check": "solhint --config ./.solhint.json 'src/**/*.sol'", 16 | "lint": "npm run fmt && npm run solhint", 17 | "lint:check": "npm run fmt:check && npm run solhint:check" 18 | }, 19 | "devDependencies": { 20 | "solhint": "^3.3.7" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "valoremoracles" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Alcibiades "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | slither-analyzer = "^0.8.2" 10 | 11 | [tool.poetry.dev-dependencies] 12 | 13 | [build-system] 14 | requires = ["poetry-core>=1.0.0"] 15 | build-backend = "poetry.core.masonry.api" 16 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | solmate/=lib/solmate/src/ 4 | v3-core/=lib/v3-core/ 5 | v3-periphery/=lib/v3-periphery/ 6 | valorem-core/=lib/valorem-core/src 7 | chainlink/=lib/chainlink 8 | keep3r/=lib/keep3r-network-v2 9 | comet/=lib/comet 10 | -------------------------------------------------------------------------------- /script/Counter.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | contract CounterScript is Script { 7 | function setUp() public {} 8 | 9 | function run() public { 10 | vm.broadcast(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ChainlinkPriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "./interfaces/IPriceOracle.sol"; 5 | import "./interfaces/IChainlinkPriceOracleAdmin.sol"; 6 | import "./interfaces/IERC20.sol"; 7 | 8 | import "./utils/Admin.sol"; 9 | 10 | import "chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; 11 | 12 | /** 13 | * @notice This contract adapts the chainlink price oracle. It stores a mapping from 14 | * ERC20 contract address to a chainlink price feed. 15 | */ 16 | contract ChainlinkPriceOracle is IPriceOracle, IChainlinkPriceOracleAdmin, Admin { 17 | /** 18 | * //////////// STATE ///////////// 19 | */ 20 | 21 | mapping(IERC20 => AggregatorV3Interface) public tokenToUSDPriceFeed; 22 | 23 | constructor() { 24 | admin = msg.sender; 25 | } 26 | 27 | /** 28 | * ///////////// IPriceOracle //////////// 29 | */ 30 | 31 | /// @inheritdoc IPriceOracle 32 | function getPriceUSD(IERC20 token) external view returns (uint256 price, uint8 scale) { 33 | AggregatorV3Interface aggregator = _getAggregator(token); 34 | (int256 rawPrice, uint8 _scale) = _getPrice(aggregator); 35 | price = uint256(rawPrice); 36 | scale = _scale; 37 | } 38 | 39 | /** 40 | * ///////////// IChainlinkPriceOracleAdmin //////////// 41 | */ 42 | 43 | /// @inheritdoc IChainlinkPriceOracleAdmin 44 | function setPriceFeed(IERC20 token, AggregatorV3Interface priceFeed) 45 | external 46 | requiresAdmin 47 | returns (address, address) 48 | { 49 | // todo: validate token and price feed 50 | tokenToUSDPriceFeed[token] = priceFeed; 51 | emit PriceFeedSet(address(token), address(priceFeed)); 52 | return (address(token), address(priceFeed)); 53 | } 54 | 55 | /** 56 | * /////////// INTERNAL //////////// 57 | */ 58 | 59 | function _getPrice(AggregatorV3Interface aggregator) internal view returns (int256 price, uint8 decimals) { 60 | /// @dev N.b. this is not a spot price from a particular dex. Using chainlink 61 | /// aggregators imparts resistance to flash price movement attacks. 62 | (, price,,,) = aggregator.latestRoundData(); 63 | decimals = aggregator.decimals(); 64 | return (price, decimals); 65 | } 66 | 67 | function _getAggregator(IERC20 token) internal view returns (AggregatorV3Interface aggregator) { 68 | return tokenToUSDPriceFeed[token]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/CompoundV3YieldOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "./interfaces/ICompoundV3YieldOracle.sol"; 5 | import "./interfaces/IComet.sol"; 6 | import "./interfaces/IERC20.sol"; 7 | import "./utils/Keep3rV2Job.sol"; 8 | 9 | contract CompoundV3YieldOracle is ICompoundV3YieldOracle, Keep3rV2Job { 10 | /** 11 | * /////////// CONSTANTS /////////////// 12 | */ 13 | address public constant COMET_USDC_ADDRESS = 0xc3d688B66703497DAA19211EEdff47f25384cdc3; 14 | address public constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; 15 | uint16 public constant DEFAULT_SNAPSHOT_ARRAY_SIZE = 5; 16 | uint16 public constant MAXIMUM_SNAPSHOT_ARRAY_SIZE = 3 * 5; 17 | 18 | /** 19 | * ///////////// STRUCTS ///////////// 20 | */ 21 | /// @dev Used to allow cumulative accounting of the time weighted rate 22 | struct YieldInfo { 23 | uint256 prevRate; 24 | uint256 prevTs; 25 | uint256 totalDelta; 26 | uint256 weightedRateAcc; 27 | } 28 | 29 | /** 30 | * ///////////// STATE /////////////// 31 | */ 32 | 33 | // token to array index mapping 34 | mapping(IERC20 => uint16) public tokenToSnapshotWriteIndex; 35 | 36 | mapping(IERC20 => SupplyRateSnapshot[]) public tokenToSnapshotArray; 37 | 38 | mapping(IERC20 => IComet) public tokenAddressToComet; 39 | 40 | IERC20[] public tokenRefreshList; 41 | 42 | mapping(address => uint16) private tokenToTokenRefreshListIndex; 43 | 44 | constructor(address _keep3r) { 45 | admin = msg.sender; 46 | setCometAddress(USDC_ADDRESS, COMET_USDC_ADDRESS); 47 | keep3r = _keep3r; 48 | } 49 | 50 | /** 51 | * ////////// IYieldOracle ////////// 52 | */ 53 | 54 | /// @notice Computed using a time weighted rate of return 55 | /// @dev compound III / comet is currently deployed/implemented only on USDC 56 | /// @inheritdoc IYieldOracle 57 | function getTokenYield(address token) public view returns (uint256 yield) { 58 | IERC20 _token = IERC20(token); 59 | IComet comet = tokenAddressToComet[(_token)]; 60 | if (address(comet) == address(0)) { 61 | revert CometAddressNotSpecifiedForToken(token); 62 | } 63 | 64 | SupplyRateSnapshot[] memory snapshots = tokenToSnapshotArray[_token]; 65 | /// write idx will always point at eldest element 66 | uint16 writeIdx = tokenToSnapshotWriteIndex[_token]; 67 | YieldInfo memory yieldInfo = YieldInfo(snapshots[writeIdx].supplyRate, snapshots[writeIdx].timestamp, 0, 0); 68 | 69 | /// go from writeIdx to end of initialized array 70 | for (uint256 i = writeIdx; i < snapshots.length; i++) { 71 | SupplyRateSnapshot memory snapshot = snapshots[i]; 72 | /// break from loop if snapshot is not initialized 73 | if (snapshot.timestamp == 0) { 74 | break; 75 | } 76 | _updateYieldInfo(yieldInfo, snapshot); 77 | } 78 | 79 | /// go from 0 to writeIdx - 1 80 | for (uint256 i = 0; i < writeIdx; i++) { 81 | SupplyRateSnapshot memory snapshot = snapshots[i]; 82 | _updateYieldInfo(yieldInfo, snapshot); 83 | } 84 | 85 | return yieldInfo.weightedRateAcc / yieldInfo.totalDelta; 86 | } 87 | 88 | /// @inheritdoc IYieldOracle 89 | function scale() public pure returns (uint8) { 90 | return 18; 91 | } 92 | 93 | /** 94 | * //////////// Keep3r //////////// 95 | */ 96 | 97 | function work() external validateAndPayKeeper(msg.sender) { 98 | _latchYieldForRefreshTokens(); 99 | } 100 | 101 | /** 102 | * //////////// ICompoundV3YieldOracle ////////////// 103 | */ 104 | 105 | /// @inheritdoc ICompoundV3YieldOracle 106 | function setCometAddress(address baseAssetErc20, address comet) public requiresAdmin returns (address, address) { 107 | if (baseAssetErc20 == address(0)) { 108 | revert InvalidTokenAddress(); 109 | } 110 | if (comet == address(0)) { 111 | revert InvalidCometAddress(); 112 | } 113 | 114 | _updateTokenRefreshList(baseAssetErc20); 115 | 116 | tokenAddressToComet[IERC20(baseAssetErc20)] = IComet(comet); 117 | _setCometSnapShotBufferSize(baseAssetErc20, DEFAULT_SNAPSHOT_ARRAY_SIZE); 118 | 119 | emit CometSet(baseAssetErc20, comet); 120 | return (baseAssetErc20, comet); 121 | } 122 | 123 | /// @inheritdoc ICompoundV3YieldOracle 124 | function latchCometRate(address token) external requiresAdmin returns (uint256) { 125 | return _latchSupplyRate(token); 126 | } 127 | 128 | /// @inheritdoc ICompoundV3YieldOracle 129 | function latchRatesForRegisteredTokens() external requiresAdmin { 130 | _latchYieldForRefreshTokens(); 131 | } 132 | 133 | /// @inheritdoc ICompoundV3YieldOracle 134 | function getCometSnapshots(address token) public view returns (uint16 idx, SupplyRateSnapshot[] memory snapshots) { 135 | IERC20 _token = IERC20(token); 136 | idx = tokenToSnapshotWriteIndex[_token]; 137 | snapshots = tokenToSnapshotArray[_token]; 138 | } 139 | 140 | /// @inheritdoc ICompoundV3YieldOracle 141 | function setCometSnapshotBufferSize(address token, uint16 newSize) external requiresAdmin returns (uint16) { 142 | return _setCometSnapShotBufferSize(token, newSize); 143 | } 144 | 145 | /** 146 | * /////////// Internal /////////// 147 | */ 148 | 149 | function _setCometSnapShotBufferSize(address token, uint16 newSize) internal returns (uint16) { 150 | if (newSize > MAXIMUM_SNAPSHOT_ARRAY_SIZE) { 151 | revert SnapshotArraySizeTooLarge(); 152 | } 153 | 154 | SupplyRateSnapshot[] storage snapshots = tokenToSnapshotArray[IERC20(token)]; 155 | if (newSize <= snapshots.length) { 156 | return uint16(snapshots.length); 157 | } 158 | 159 | uint16 currentSz = uint16(snapshots.length); 160 | // increase array size 161 | for (uint16 i = 0; i < newSize - currentSz; i++) { 162 | // add uninitialized snapshot to extend length of array 163 | snapshots.push(SupplyRateSnapshot(0, 0)); 164 | } 165 | 166 | emit CometSnapshotArraySizeSet(token, newSize); 167 | return newSize; 168 | } 169 | 170 | function _latchYieldForRefreshTokens() internal { 171 | for (uint256 i = 0; i < tokenRefreshList.length; i++) { 172 | _latchSupplyRate(address(tokenRefreshList[i])); 173 | } 174 | } 175 | 176 | function _latchSupplyRate(address token) internal returns (uint256 supplyRate) { 177 | IComet comet; 178 | IERC20 _token = IERC20(token); 179 | uint16 idx = tokenToSnapshotWriteIndex[_token]; 180 | SupplyRateSnapshot[] storage snapshots = tokenToSnapshotArray[_token]; 181 | uint16 idxNext = (idx + 1) % uint16(snapshots.length); 182 | 183 | (supplyRate, comet) = _getSupplyRateYieldForUnderlyingAsset(token); 184 | 185 | // update the cached rate 186 | snapshots[idx].timestamp = block.timestamp; 187 | snapshots[idx].supplyRate = supplyRate; 188 | tokenToSnapshotWriteIndex[_token] = idxNext; 189 | 190 | emit CometRateLatched(token, address(comet), supplyRate); 191 | } 192 | 193 | function _getSupplyRateYieldForUnderlyingAsset(address token) internal view returns (uint256 yield, IComet comet) { 194 | comet = tokenAddressToComet[IERC20(token)]; 195 | if (address(comet) == address(0)) { 196 | revert CometAddressNotSpecifiedForToken(token); 197 | } 198 | 199 | uint256 utilization = comet.getUtilization(); 200 | uint64 supplyRate = comet.getSupplyRate(utilization); 201 | yield = uint256(supplyRate); 202 | } 203 | 204 | function _getWeightedPeriodWeightAndTimeDelta(uint256 prevTs, uint256 prevRate, SupplyRateSnapshot memory snapshot) 205 | internal 206 | pure 207 | returns (uint256 tsDelta, uint256 weightedPeriodRate) 208 | { 209 | uint256 curTs = snapshot.timestamp; 210 | uint256 curRate = snapshot.supplyRate; 211 | tsDelta = curTs - prevTs; 212 | 213 | uint256 periodRate = (curRate + prevRate) / 2; 214 | weightedPeriodRate = periodRate * tsDelta; 215 | } 216 | 217 | function _updateTokenRefreshList(address token) internal { 218 | // append token if not present in refresh list 219 | uint16 index = tokenToTokenRefreshListIndex[token]; 220 | 221 | // uninitialized 222 | if (index == 0) { 223 | tokenRefreshList.push(IERC20(token)); 224 | tokenToTokenRefreshListIndex[token] = uint16(tokenRefreshList.length); 225 | } 226 | } 227 | 228 | function _updateYieldInfo(YieldInfo memory yieldInfo, SupplyRateSnapshot memory snapshot) internal pure { 229 | (uint256 tsDelta, uint256 weightedPeriodRate) = 230 | _getWeightedPeriodWeightAndTimeDelta(yieldInfo.prevTs, yieldInfo.prevRate, snapshot); 231 | 232 | yieldInfo.totalDelta += tsDelta; 233 | yieldInfo.weightedRateAcc += weightedPeriodRate; 234 | 235 | yieldInfo.prevTs = snapshot.timestamp; 236 | yieldInfo.prevRate = snapshot.supplyRate; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/UniswapV3VolatilityOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "./interfaces/IUniswapV3VolatilityOracle.sol"; 5 | 6 | import "v3-core/contracts/interfaces/IUniswapV3Factory.sol"; 7 | import "v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 8 | 9 | import "./utils/Keep3rV2Job.sol"; 10 | import "./interfaces/IVolatilityOracle.sol"; 11 | 12 | import "./libraries/Volatility.sol"; 13 | import "./libraries/Oracle.sol"; 14 | 15 | contract UniswapV3VolatilityOracle is IUniswapV3VolatilityOracle, Keep3rV2Job { 16 | /** 17 | * /////////// CONSTANTS //////////// 18 | */ 19 | uint24 private constant POINT_ZERO_ONE_PCT_FEE = 100; 20 | uint24 private constant POINT_THREE_PCT_FEE = 3_000; 21 | uint24 private constant POINT_ZERO_FIVE_PCT_FEE = 500; 22 | 23 | address private constant UNISWAP_FACTORY_ADDRESS = 0x1F98431c8aD98523631AE4a59f267346ea31F984; 24 | 25 | /** 26 | * /////////// STATE //////////// 27 | */ 28 | 29 | struct Indices { 30 | uint8 read; 31 | uint8 write; 32 | } 33 | 34 | mapping(bytes32 => UniswapV3FeeTier) private tokenPairHashToDefaultFeeTier; 35 | 36 | /// @inheritdoc IUniswapV3VolatilityOracle 37 | mapping(IUniswapV3Pool => Volatility.PoolMetadata) public cachedPoolMetadata; 38 | 39 | /// @inheritdoc IUniswapV3VolatilityOracle 40 | mapping(IUniswapV3Pool => Volatility.FeeGrowthGlobals[25]) public feeGrowthGlobals; 41 | 42 | /// @inheritdoc IUniswapV3VolatilityOracle 43 | mapping(IUniswapV3Pool => Indices) public feeGrowthGlobalsIndices; 44 | 45 | IUniswapV3Factory private uniswapV3Factory; 46 | 47 | IUniswapV3VolatilityOracle.UniswapV3PoolInfo[] private tokenFeeTierList; 48 | 49 | constructor(address _keep3r) { 50 | admin = msg.sender; 51 | uniswapV3Factory = IUniswapV3Factory(UNISWAP_FACTORY_ADDRESS); 52 | keep3r = _keep3r; 53 | } 54 | 55 | /** 56 | * /////////// IVolatilityOracle ////////// 57 | */ 58 | 59 | /// @inheritdoc IVolatilityOracle 60 | function getHistoricalVolatility(address) external pure returns (uint256) { 61 | revert("not implemented"); 62 | } 63 | 64 | /// @inheritdoc IVolatilityOracle 65 | function getImpliedVolatility(address tokenA, address tokenB) external view returns (uint256 impliedVolatility) { 66 | UniswapV3FeeTier tier = getDefaultFeeTierForTokenPair(tokenA, tokenB); 67 | return getImpliedVolatility(tokenA, tokenB, tier); 68 | } 69 | 70 | /// @inheritdoc IVolatilityOracle 71 | function scale() external pure returns (uint8) { 72 | return 18; 73 | } 74 | 75 | /** 76 | * /////////// IUniswapV3VolatilityOracle ////////// 77 | */ 78 | 79 | /// @inheritdoc IUniswapV3VolatilityOracle 80 | function getImpliedVolatility(address tokenA, address tokenB, UniswapV3FeeTier tier) 81 | public 82 | view 83 | returns (uint256 impliedVolatility) 84 | { 85 | if (tier == UniswapV3FeeTier.RESERVED) { 86 | revert NoFeeTierSpecifiedForTokenPair(); 87 | } 88 | 89 | uint24 fee = getUniswapV3FeeInHundredthsOfBip(tier); 90 | IUniswapV3Pool pool = getV3PoolForTokensAndFee(tokenA, tokenB, fee); 91 | uint256[25] memory loadedLens = lens(pool); 92 | Indices memory idxs = feeGrowthGlobalsIndices[pool]; 93 | return loadedLens[idxs.read]; 94 | } 95 | 96 | /// @inheritdoc IUniswapV3VolatilityOracle 97 | function getV3PoolForTokensAndFee(address tokenA, address tokenB, uint24 fee) 98 | public 99 | view 100 | returns (IUniswapV3Pool pool) 101 | { 102 | pool = IUniswapV3Pool(uniswapV3Factory.getPool(tokenA, tokenB, fee)); 103 | } 104 | 105 | /// @inheritdoc IUniswapV3VolatilityOracle 106 | function getDefaultFeeTierForTokenPair(address tokenA, address tokenB) public view returns (UniswapV3FeeTier) { 107 | bytes32 tokenPairHash = keccak256(abi.encodePacked(tokenA, tokenB)); 108 | UniswapV3FeeTier tier = tokenPairHashToDefaultFeeTier[tokenPairHash]; 109 | return tier; 110 | } 111 | 112 | /// @inheritdoc IUniswapV3VolatilityOracle 113 | function refreshVolatilityCache() public returns (uint256) { 114 | return _refreshVolatilityCache(); 115 | } 116 | 117 | /// @inheritdoc IUniswapV3VolatilityOracle 118 | function refreshVolatilityCacheAndMetadataForPool(UniswapV3PoolInfo calldata info) 119 | public 120 | requiresAdmin 121 | returns (uint256) 122 | { 123 | _refreshPoolMetadata(info); 124 | (, uint256 timestamp) = _refreshTokenVolatility(info.tokenA, info.tokenB, info.feeTier); 125 | return timestamp; 126 | } 127 | 128 | /// @inheritdoc IUniswapV3VolatilityOracle 129 | function setDefaultFeeTierForTokenPair(address tokenA, address tokenB, UniswapV3FeeTier tier) 130 | external 131 | requiresAdmin 132 | returns (address, address, UniswapV3FeeTier) 133 | { 134 | if (tokenA == address(0) || tokenB == address(0)) { 135 | revert InvalidToken(); 136 | } 137 | if (tier == UniswapV3FeeTier.RESERVED) { 138 | revert InvalidFeeTier(); 139 | } 140 | 141 | bytes32 tokenPairHash = keccak256(abi.encodePacked(tokenA, tokenB)); 142 | tokenPairHashToDefaultFeeTier[tokenPairHash] = tier; 143 | return (tokenA, tokenB, tier); 144 | } 145 | 146 | /// @inheritdoc IUniswapV3VolatilityOracle 147 | function cacheMetadataFor(IUniswapV3Pool pool) public requiresAdmin { 148 | _cacheMetadataFor(pool); 149 | } 150 | 151 | /// @inheritdoc IUniswapV3VolatilityOracle 152 | function lens(IUniswapV3Pool pool) public view returns (uint256[25] memory impliedVolatility) { 153 | (uint160 sqrtPriceX96, int24 tick,,,,,) = pool.slot0(); 154 | Volatility.FeeGrowthGlobals[25] memory feeGrowthGlobal = feeGrowthGlobals[pool]; 155 | 156 | for (uint8 i = 0; i < 25; i++) { 157 | (impliedVolatility[i],) = _estimate24H(pool, sqrtPriceX96, tick, feeGrowthGlobal[i]); 158 | } 159 | } 160 | 161 | /// @inheritdoc IUniswapV3VolatilityOracle 162 | function estimate24H(IUniswapV3Pool pool) public returns (uint256 impliedVolatility) { 163 | (uint160 sqrtPriceX96, int24 tick,,,,,) = pool.slot0(); 164 | 165 | Volatility.FeeGrowthGlobals[25] storage feeGrowthGlobal = feeGrowthGlobals[pool]; 166 | Indices memory idxs = _loadIndicesAndSelectRead(pool, feeGrowthGlobal); 167 | 168 | Volatility.FeeGrowthGlobals memory current; 169 | (impliedVolatility, current) = _estimate24H(pool, sqrtPriceX96, tick, feeGrowthGlobal[idxs.read]); 170 | 171 | // Write to storage 172 | if (current.timestamp - 1 hours > feeGrowthGlobal[idxs.write].timestamp) { 173 | idxs.write = (idxs.write + 1) % 25; 174 | feeGrowthGlobals[pool][idxs.write] = current; 175 | } 176 | feeGrowthGlobalsIndices[pool] = idxs; 177 | } 178 | 179 | /** 180 | * ////////////// KEEP3R /////////////// 181 | */ 182 | 183 | function work() external validateAndPayKeeper(msg.sender) { 184 | _refreshVolatilityCache(); 185 | } 186 | 187 | /** 188 | * ////////////// TOKEN REFRESH LIST /////////////// 189 | */ 190 | 191 | /// @inheritdoc IUniswapV3VolatilityOracle 192 | function setTokenFeeTierRefreshList(UniswapV3PoolInfo[] calldata list) 193 | external 194 | requiresAdmin 195 | returns (UniswapV3PoolInfo[] memory) 196 | { 197 | delete tokenFeeTierList; 198 | for (uint256 i = 0; i < list.length; i++) { 199 | UniswapV3PoolInfo memory info = UniswapV3PoolInfo(list[i].tokenA, list[i].tokenB, list[i].feeTier); 200 | // refresh pool metadata cache on first add 201 | _refreshPoolMetadata(info); 202 | tokenFeeTierList.push(info); 203 | } 204 | emit TokenRefreshListSet(); 205 | return list; 206 | } 207 | 208 | /// @inheritdoc IUniswapV3VolatilityOracle 209 | function getTokenFeeTierRefreshList() public view returns (UniswapV3PoolInfo[] memory) { 210 | return tokenFeeTierList; 211 | } 212 | 213 | /** 214 | * /////////////// ADMIN FUNCTIONS /////////////// 215 | */ 216 | 217 | /// @inheritdoc IUniswapV3VolatilityOracle 218 | function getUniswapV3FeeInHundredthsOfBip(UniswapV3FeeTier tier) public pure returns (uint24) { 219 | if (tier == UniswapV3FeeTier.PCT_POINT_01) { 220 | return 1 * 100; 221 | } 222 | if (tier == UniswapV3FeeTier.PCT_POINT_05) { 223 | return 5 * 100; 224 | } 225 | if (tier == UniswapV3FeeTier.PCT_POINT_3) { 226 | return 3 * 100 * 10; 227 | } 228 | if (tier == UniswapV3FeeTier.PCT_1) { 229 | return 100 * 100; 230 | } 231 | revert("unimplemented fee tier"); 232 | } 233 | 234 | /** 235 | * ///////// INTERNAL /////////// 236 | */ 237 | 238 | function _refreshPoolMetadata(UniswapV3PoolInfo memory info) internal { 239 | uint24 fee = getUniswapV3FeeInHundredthsOfBip(info.feeTier); 240 | IUniswapV3Pool pool = getV3PoolForTokensAndFee(info.tokenA, info.tokenB, fee); 241 | _cacheMetadataFor(pool); 242 | } 243 | 244 | function _cacheMetadataFor(IUniswapV3Pool pool) internal { 245 | Volatility.PoolMetadata memory poolMetadata; 246 | 247 | (,, uint16 observationIndex, uint16 observationCardinality,, uint8 feeProtocol,) = pool.slot0(); 248 | poolMetadata.maxSecondsAgo = (Oracle.getMaxSecondsAgo(pool, observationIndex, observationCardinality) * 3) / 5; 249 | 250 | uint24 fee = pool.fee(); 251 | poolMetadata.gamma0 = fee; 252 | poolMetadata.gamma1 = fee; 253 | if (feeProtocol % 16 != 0) { 254 | poolMetadata.gamma0 -= fee / (feeProtocol % 16); 255 | } 256 | if (feeProtocol >> 4 != 0) { 257 | poolMetadata.gamma1 -= fee / (feeProtocol >> 4); 258 | } 259 | 260 | poolMetadata.tickSpacing = pool.tickSpacing(); 261 | 262 | cachedPoolMetadata[pool] = poolMetadata; 263 | } 264 | 265 | function _refreshVolatilityCache() internal returns (uint256) { 266 | for (uint256 i = 0; i < tokenFeeTierList.length; i++) { 267 | address tokenA = tokenFeeTierList[i].tokenA; 268 | address tokenB = tokenFeeTierList[i].tokenB; 269 | UniswapV3FeeTier feeTier = tokenFeeTierList[i].feeTier; 270 | _refreshTokenVolatility(tokenA, tokenB, feeTier); 271 | } 272 | 273 | emit VolatilityOracleCacheUpdated(block.timestamp); 274 | return block.timestamp; 275 | } 276 | 277 | function _refreshTokenVolatility(address tokenA, address tokenB, UniswapV3FeeTier feeTier) 278 | internal 279 | returns (uint256 volatility, uint256 timestamp) 280 | { 281 | uint24 fee = getUniswapV3FeeInHundredthsOfBip(feeTier); 282 | IUniswapV3Pool pool = getV3PoolForTokensAndFee(tokenA, tokenB, fee); 283 | 284 | // refresh metadata only if observation is older than xx 285 | // in certain cases, aloe won't have sufficient data to run estimate24h, since 286 | // the oldest observation for the pool oracle is under an hour. for now, 287 | // we're only refreshing the pool metadata cache when the token is added to the 288 | // refresh list, and when a manual call to refresh a token is made. 289 | uint256 impliedVolatility = estimate24H(pool); 290 | emit TokenVolatilityUpdated(tokenA, tokenB, fee, impliedVolatility, block.timestamp); 291 | return (impliedVolatility, block.timestamp); 292 | } 293 | 294 | function _estimate24H( 295 | IUniswapV3Pool _pool, 296 | uint160 _sqrtPriceX96, 297 | int24 _tick, 298 | Volatility.FeeGrowthGlobals memory _previous 299 | ) private view returns (uint256 impliedVolatility, Volatility.FeeGrowthGlobals memory current) { 300 | Volatility.PoolMetadata memory poolMetadata = cachedPoolMetadata[_pool]; 301 | 302 | uint32 secondsAgo = poolMetadata.maxSecondsAgo; 303 | require(secondsAgo >= 1 hours, "IV Oracle: need more data"); 304 | if (secondsAgo > 1 days) { 305 | secondsAgo = 1 days; 306 | } 307 | // Throws if secondsAgo == 0 308 | (int24 arithmeticMeanTick, uint160 secondsPerLiquidityX128) = Oracle.consult(_pool, secondsAgo); 309 | 310 | current = Volatility.FeeGrowthGlobals( 311 | _pool.feeGrowthGlobal0X128(), _pool.feeGrowthGlobal1X128(), uint32(block.timestamp) 312 | ); 313 | impliedVolatility = Volatility.estimate24H( 314 | poolMetadata, 315 | Volatility.PoolData( 316 | _sqrtPriceX96, _tick, arithmeticMeanTick, secondsPerLiquidityX128, secondsAgo, _pool.liquidity() 317 | ), 318 | _previous, 319 | current 320 | ); 321 | } 322 | 323 | function _loadIndicesAndSelectRead(IUniswapV3Pool _pool, Volatility.FeeGrowthGlobals[25] storage _feeGrowthGlobal) 324 | private 325 | view 326 | returns (Indices memory) 327 | { 328 | Indices memory idxs = feeGrowthGlobalsIndices[_pool]; 329 | uint32 timingError = _timingError(block.timestamp - _feeGrowthGlobal[idxs.read].timestamp); 330 | 331 | for (uint8 counter = idxs.read + 1; counter < idxs.read + 25; counter++) { 332 | uint8 newReadIndex = counter % 25; 333 | uint32 newTimingError = _timingError(block.timestamp - _feeGrowthGlobal[newReadIndex].timestamp); 334 | 335 | if (newTimingError < timingError) { 336 | idxs.read = newReadIndex; 337 | timingError = newTimingError; 338 | } else { 339 | break; 340 | } 341 | } 342 | 343 | return idxs; 344 | } 345 | 346 | function _timingError(uint256 _age) private pure returns (uint32) { 347 | return uint32(_age < 24 hours ? 24 hours - _age : _age - 24 hours); 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/interfaces/IAdmin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | interface IAdmin { 5 | /** 6 | * @notice Emitted when a new admin address is set for the contract. 7 | * @param admin The new admin address. 8 | */ 9 | event AdminSet(address indexed admin); 10 | 11 | /** 12 | * /////////////// ADMIN FUNCTIONS /////////////// 13 | */ 14 | 15 | /** 16 | * @notice Sets the admin address for this contract. 17 | * @param _admin The new admin address for this contract. Cannot be 0x0. 18 | */ 19 | function setAdmin(address _admin) external; 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/IBlackScholes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "./IVolatilityOracle.sol"; 5 | import "./IPriceOracle.sol"; 6 | import "./IYieldOracle.sol"; 7 | 8 | import "valorem-core/interfaces/IOptionSettlementEngine.sol"; 9 | 10 | /** 11 | * @notice Interface for pricing strategies via Black Scholes method. Volatility 12 | * is derived from the Uniswap pool. 13 | */ 14 | interface IBlackScholes is IAdmin { 15 | /** 16 | * @notice Returns the long call premium for the supplied valorem optionId 17 | */ 18 | function getLongCallPremium(uint256 optionId) external view returns (uint256 callPremium); 19 | 20 | /** 21 | * @notice Returns the long call premium for the supplied valorem optionId 22 | */ 23 | function getShortCallPremium(uint256 optionId) external view returns (uint256 callPremium); 24 | 25 | function getLongCallPremiumEx( 26 | uint256 optionId, 27 | IVolatilityOracle volatilityOracle, 28 | IPriceOracle priceOracle, 29 | IYieldOracle yieldOracle, 30 | IOptionSettlementEngine engine 31 | ) external view returns (uint256 callPremium); 32 | 33 | function getShortCallPremiumEx( 34 | uint256 optionId, 35 | IVolatilityOracle volatilityOracle, 36 | IPriceOracle priceOracle, 37 | IYieldOracle yieldOracle, 38 | IOptionSettlementEngine engine 39 | ) external view returns (uint256 callPremium); 40 | 41 | /** 42 | * @notice sets the oracle from which to retrieve historical or implied volatility 43 | */ 44 | function setVolatilityOracle(IVolatilityOracle oracle) external; 45 | 46 | /** 47 | * @notice sets the oracle from which to retrieve the underlying asset price 48 | */ 49 | function setPriceOracle(IPriceOracle oracle) external; 50 | 51 | /** 52 | * @notice sets the yield oracle for the risk free rate 53 | */ 54 | function setYieldOracle(IYieldOracle oracle) external; 55 | 56 | /** 57 | * @notice sets the Valorem engine for retrieving options 58 | */ 59 | function setValoremOptionSettlementEngine(IOptionSettlementEngine engine) external; 60 | } 61 | -------------------------------------------------------------------------------- /src/interfaces/IChainlinkPriceOracleAdmin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "./IERC20.sol"; 5 | import "chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; 6 | 7 | interface IChainlinkPriceOracleAdmin { 8 | /** 9 | * @notice Emitted when a price feed for an ERC20 token is set. 10 | * @param token The contract address for the ERC20. 11 | * @param priceFeed The price feed address. 12 | */ 13 | event PriceFeedSet(address indexed token, address indexed priceFeed); 14 | 15 | /** 16 | * /////////////// ADMIN FUNCTIONS /////////////// 17 | */ 18 | 19 | /** 20 | * @notice Sets the oracle contract address. 21 | * @param token The contract address for the erc20 contract. 22 | * @param priceFeed The contract address for the chainlink price feed. 23 | * @return The set token and price feed. 24 | */ 25 | function setPriceFeed(IERC20 token, AggregatorV3Interface priceFeed) external returns (address, address); 26 | } 27 | -------------------------------------------------------------------------------- /src/interfaces/IComet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.13; 3 | 4 | /// @dev Taken from https://github.com/compound-developers/compound-3-developer-faq/blob/master/contracts/MyContract.sol 5 | 6 | interface IComet { 7 | struct AssetInfo { 8 | uint8 offset; 9 | address asset; 10 | address priceFeed; 11 | uint64 scale; 12 | uint64 borrowCollateralFactor; 13 | uint64 liquidateCollateralFactor; 14 | uint64 liquidationFactor; 15 | uint128 supplyCap; 16 | } 17 | 18 | struct UserBasic { 19 | int104 principal; 20 | uint64 baseTrackingIndex; 21 | uint64 baseTrackingAccrued; 22 | uint16 assetsIn; 23 | uint8 _reserved; 24 | } 25 | 26 | struct TotalsBasic { 27 | uint64 baseSupplyIndex; 28 | uint64 baseBorrowIndex; 29 | uint64 trackingSupplyIndex; 30 | uint64 trackingBorrowIndex; 31 | uint104 totalSupplyBase; 32 | uint104 totalBorrowBase; 33 | uint40 lastAccrualTime; 34 | uint8 pauseFlags; 35 | } 36 | 37 | struct UserCollateral { 38 | uint128 balance; 39 | uint128 _reserved; 40 | } 41 | 42 | struct RewardOwed { 43 | address token; 44 | uint256 owed; 45 | } 46 | 47 | struct TotalsCollateral { 48 | uint128 totalSupplyAsset; 49 | uint128 _reserved; 50 | } 51 | 52 | function supply(address asset, uint256 amount) external; 53 | function supplyTo(address dst, address asset, uint256 amount) external; 54 | function supplyFrom(address from, address dst, address asset, uint256 amount) external; 55 | 56 | function transfer(address dst, uint256 amount) external returns (bool); 57 | function transferFrom(address src, address dst, uint256 amount) external returns (bool); 58 | 59 | function transferAsset(address dst, address asset, uint256 amount) external; 60 | function transferAssetFrom(address src, address dst, address asset, uint256 amount) external; 61 | 62 | function withdraw(address asset, uint256 amount) external; 63 | function withdrawTo(address to, address asset, uint256 amount) external; 64 | function withdrawFrom(address src, address to, address asset, uint256 amount) external; 65 | 66 | function approveThis(address manager, address asset, uint256 amount) external; 67 | function withdrawReserves(address to, uint256 amount) external; 68 | 69 | function absorb(address absorber, address[] calldata accounts) external; 70 | function buyCollateral(address asset, uint256 minAmount, uint256 baseAmount, address recipient) external; 71 | function quoteCollateral(address asset, uint256 baseAmount) external view returns (uint256); 72 | 73 | function getAssetInfo(uint8 i) external view returns (AssetInfo memory); 74 | function getAssetInfoByAddress(address asset) external view returns (AssetInfo memory); 75 | function getReserves() external view returns (int256); 76 | function getPrice(address priceFeed) external view returns (uint256); 77 | 78 | function isBorrowCollateralized(address account) external view returns (bool); 79 | function isLiquidatable(address account) external view returns (bool); 80 | 81 | function totalSupply() external view returns (uint256); 82 | function totalBorrow() external view returns (uint256); 83 | function balanceOf(address owner) external view returns (uint256); 84 | function borrowBalanceOf(address account) external view returns (uint256); 85 | 86 | function pause(bool supplyPaused, bool transferPaused, bool withdrawPaused, bool absorbPaused, bool buyPaused) 87 | external; 88 | function isSupplyPaused() external view returns (bool); 89 | function isTransferPaused() external view returns (bool); 90 | function isWithdrawPaused() external view returns (bool); 91 | function isAbsorbPaused() external view returns (bool); 92 | function isBuyPaused() external view returns (bool); 93 | 94 | function accrueAccount(address account) external; 95 | function getSupplyRate(uint256 utilization) external view returns (uint64); 96 | function getBorrowRate(uint256 utilization) external view returns (uint64); 97 | function getUtilization() external view returns (uint256); 98 | 99 | function governor() external view returns (address); 100 | function pauseGuardian() external view returns (address); 101 | function baseToken() external view returns (address); 102 | function baseTokenPriceFeed() external view returns (address); 103 | function extensionDelegate() external view returns (address); 104 | 105 | function totalsCollateral(address addr) external view returns (TotalsCollateral memory); 106 | } 107 | -------------------------------------------------------------------------------- /src/interfaces/ICompoundV3YieldOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "./IYieldOracle.sol"; 5 | 6 | interface ICompoundV3YieldOracle is IYieldOracle { 7 | /** 8 | * /////////// STRUCTS ///////////// 9 | */ 10 | 11 | struct SupplyRateSnapshot { 12 | uint256 timestamp; 13 | uint256 supplyRate; 14 | } 15 | 16 | /** 17 | * /////////// EVENTS ////////////// 18 | */ 19 | 20 | /** 21 | * @notice Emitted when the comet contract address is set. 22 | * @param token The token address of the base asset for the comet contract. 23 | * @param comet The address of the set comet contract. 24 | */ 25 | event CometSet(address indexed token, address indexed comet); 26 | 27 | /** 28 | * @notice Emitted when the supply rate is latched in the given comet pool. 29 | * @param token The token address of the base asset for the comet contract. 30 | * @param comet The address of the comet contract. 31 | * @param supplyRate The latched supply rate of the comet contract. 32 | */ 33 | event CometRateLatched(address indexed token, address indexed comet, uint256 supplyRate); 34 | 35 | /** 36 | * @notice Emitted when the comet snapshot array size is increased for a given token. 37 | * @param token The token address of the base asset for which we're increasing the comet 38 | * snapshot array size. 39 | * @param newSize The new size of the array. 40 | */ 41 | event CometSnapshotArraySizeSet(address indexed token, uint16 newSize); 42 | 43 | /** 44 | * //////////// ERRORS //////////// 45 | */ 46 | 47 | /** 48 | * @notice Emitted when the token supplied from getTokenYield had not 49 | * been previously registered with setCometAddresss. 50 | * @param token The token address requested. 51 | */ 52 | error CometAddressNotSpecifiedForToken(address token); 53 | 54 | /// @notice Emitted for invalid token addresses supplied to setCometAddress 55 | error InvalidTokenAddress(); 56 | 57 | /// @notice Emitted for invalid comet addresses supplied to setCometAddress 58 | error InvalidCometAddress(); 59 | 60 | /// @notice Emitted if the supplied size for the snapshot array exceeds the max 61 | /// limit 62 | error SnapshotArraySizeTooLarge(); 63 | 64 | /** 65 | * ///////// ADMIN ///////// 66 | */ 67 | 68 | /** 69 | * @notice Adds the base asset erc20 address, comet contract pair for getting yield on the 70 | * base asset. Must be called before IYieldOracle.getTokenYield. 71 | * @param baseAssetErc20 The address of the underlying ERC20 contract 72 | * @param comet The address of the compound III/comet contract 73 | * @return The base asset's erc 20 address, the set comet address 74 | */ 75 | function setCometAddress(address baseAssetErc20, address comet) external returns (address, address); 76 | 77 | /** 78 | * @notice Latches the current supply rate for the provided erc20 base asset. 79 | * @dev Reverts if setCometAddress was not preiously called with the supplied token. 80 | * @param token The address of the erc20 base asset. 81 | * @return The latched supply rate. 82 | */ 83 | function latchCometRate(address token) external returns (uint256); 84 | 85 | /** 86 | * @notice Refreshes the cached/latched supply rate of every registered ERC20 87 | * underying asset. 88 | */ 89 | function latchRatesForRegisteredTokens() external; 90 | 91 | /** 92 | * @notice Gets the current list of snapshots of compound v3 supply rates 93 | * @param token The address of the erc20 base asset for which to return the supply 94 | * rate snapshots. 95 | * @return The snapshots currently stored in the oracle, along with associated 'next' index. 96 | */ 97 | function getCometSnapshots(address token) external view returns (uint16, SupplyRateSnapshot[] memory); 98 | 99 | /** 100 | * @notice Increases the size of the supply rate buffer. Caller must pay the associated 101 | * gas costs for increasing the size of the array. 102 | * @dev Reverts if newSize is less than current size. Max size is 2^16/~65k 103 | * @param token The erc20 underlying asset for which to increase the size of the comet buffer. 104 | * @param newSize The new size of the array. 105 | * @return New size of the array. 106 | */ 107 | function setCometSnapshotBufferSize(address token, uint16 newSize) external returns (uint16); 108 | } 109 | -------------------------------------------------------------------------------- /src/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.8.0; 4 | 5 | /** 6 | * @dev Interface of the ERC20 standard as defined in the EIP. 7 | */ 8 | interface IERC20 { 9 | /** 10 | * @dev Emitted when `value` tokens are moved from one account (`from`) to 11 | * another (`to`). 12 | * 13 | * Note that `value` may be zero. 14 | */ 15 | event Transfer(address indexed from, address indexed to, uint256 value); 16 | 17 | /** 18 | * @dev Emitted when the allowance of a `spender` for an `owner` is set by 19 | * a call to {approve}. `value` is the new allowance. 20 | */ 21 | event Approval(address indexed owner, address indexed spender, uint256 value); 22 | 23 | /** 24 | * @dev Returns the amount of tokens in existence. 25 | */ 26 | function totalSupply() external view returns (uint256); 27 | 28 | /** 29 | * @dev Returns the amount of tokens owned by `account`. 30 | */ 31 | function balanceOf(address account) external view returns (uint256); 32 | 33 | /** 34 | * @dev Moves `amount` tokens from the caller's account to `to`. 35 | * 36 | * Returns a boolean value indicating whether the operation succeeded. 37 | * 38 | * Emits a {Transfer} event. 39 | */ 40 | function transfer(address to, uint256 amount) external returns (bool); 41 | 42 | /** 43 | * @dev Returns the remaining number of tokens that `spender` will be 44 | * allowed to spend on behalf of `owner` through {transferFrom}. This is 45 | * zero by default. 46 | * 47 | * This value changes when {approve} or {transferFrom} are called. 48 | */ 49 | function allowance(address owner, address spender) external view returns (uint256); 50 | 51 | /** 52 | * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. 53 | * 54 | * Returns a boolean value indicating whether the operation succeeded. 55 | * 56 | * IMPORTANT: Beware that changing an allowance with this method brings the risk 57 | * that someone may use both the old and the new allowance by unfortunate 58 | * transaction ordering. One possible solution to mitigate this race 59 | * condition is to first reduce the spender's allowance to 0 and set the 60 | * desired value afterwards: 61 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 62 | * 63 | * Emits an {Approval} event. 64 | */ 65 | function approve(address spender, uint256 amount) external returns (bool); 66 | 67 | /** 68 | * @dev Moves `amount` tokens from `from` to `to` using the 69 | * allowance mechanism. `amount` is then deducted from the caller's 70 | * allowance. 71 | * 72 | * Returns a boolean value indicating whether the operation succeeded. 73 | * 74 | * Emits a {Transfer} event. 75 | */ 76 | function transferFrom(address from, address to, uint256 amount) external returns (bool); 77 | } 78 | -------------------------------------------------------------------------------- /src/interfaces/IKeep3rV2Job.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | interface IKeep3rV2Job { 5 | event Keep3rSet(address keep3rAddress); 6 | 7 | error InvalidKeeper(); 8 | 9 | function getKeep3r() external view returns (address); 10 | 11 | function setKeep3r(address _keep3rAddress) external; 12 | 13 | function work() external; 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/IOracleAdmin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | interface IOracleAdmin { 5 | /** 6 | * @notice Emitted when the oracle contract address is set. 7 | * @param oracle The contract address for the oracle. 8 | */ 9 | event OracleSet(address indexed oracle); 10 | 11 | /** 12 | * /////////////// ADMIN FUNCTIONS /////////////// 13 | */ 14 | 15 | /** 16 | * @notice Sets the oracle contract address. 17 | * @param oracle The contract address for the oracle adapter. 18 | * @return The contract address for the oracle adapter. 19 | */ 20 | function setOracle(address oracle) external returns (address); 21 | } 22 | -------------------------------------------------------------------------------- /src/interfaces/IPriceOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "./IERC20.sol"; 5 | 6 | /** 7 | * @notice This is an interface for contracts providing the price of a token, in 8 | * USD. 9 | * This is used internally in order to provide a uniform way of interacting with 10 | * various price oracles. An external price oracle can be used seamlessly 11 | * by being wrapped in a contract implementing this interface. 12 | */ 13 | interface IPriceOracle { 14 | /** 15 | * @notice Returns the price against USD for a specific ERC20, sourced from chainlink. 16 | * @param token The ERC20 token to retrieve the USD price for 17 | * @return price The price of the token in USD, scale The power of 10 by which the return is scaled 18 | */ 19 | function getPriceUSD(IERC20 token) external view returns (uint256 price, uint8 scale); 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/IUniswapV3VolatilityOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 5 | import "./IKeep3rV2Job.sol"; 6 | import "./IAdmin.sol"; 7 | import "./IVolatilityOracle.sol"; 8 | 9 | interface IUniswapV3VolatilityOracle is IKeep3rV2Job, IAdmin, IVolatilityOracle { 10 | /** 11 | * ////////// STRUCTS ///////////// 12 | */ 13 | struct UniswapV3PoolInfo { 14 | address tokenA; 15 | address tokenB; 16 | UniswapV3FeeTier feeTier; 17 | } 18 | 19 | enum UniswapV3FeeTier { 20 | RESERVED, 21 | PCT_POINT_01, 22 | PCT_POINT_05, 23 | PCT_POINT_3, 24 | PCT_1 25 | } 26 | 27 | /** 28 | * /////////// EVENTS ///////////// 29 | */ 30 | 31 | /** 32 | * @notice Emitted when the implied volatility cache is updated. 33 | * @param timestamp The timestamp of when the cache is updated. 34 | */ 35 | event VolatilityOracleCacheUpdated(uint256 timestamp); 36 | 37 | /** 38 | * @notice Emitted when the implied volatility for a given token is updated. 39 | * @param tokenA The ERC20 contract address of the token. 40 | * @param tokenB The ERC20 contract address of the token. 41 | * @param feeTier The UniswapV3 fee tier. 42 | * @param volatility The implied volatility of the token. 43 | * @param timestamp The timestamp of the refresh. 44 | */ 45 | event TokenVolatilityUpdated( 46 | address indexed tokenA, address indexed tokenB, uint24 feeTier, uint256 volatility, uint256 timestamp 47 | ); 48 | 49 | /// @notice Emitted when the token refresh list is set. 50 | event TokenRefreshListSet(); 51 | 52 | /** 53 | * @notice Emitted when the default token fee tier for the supplied token pair is set 54 | * @param tokenA The contract address of the ERC20 for which to retrieve the v3 pool. 55 | * @param tokenB The contract address of the ERC20 for which to retrieve the v3 pool. 56 | * @param feeTier The UniswapV3 fee tier. 57 | */ 58 | event DefaultTokenPairFeeTierSet(address indexed tokenA, address indexed tokenB, uint24 feeTier); 59 | 60 | /// @notice Thrown if an invalid (== 0x0) token address is passed. 61 | error InvalidToken(); 62 | 63 | /// @notice Thrown if an invalid (== RESERVED) fee tier is passed. 64 | error InvalidFeeTier(); 65 | 66 | /// @notice Thrown when the passed v3 factory address is invalid. 67 | error InvalidUniswapV3Factory(); 68 | 69 | /// @notice Thrown when invalid parameters are passed to setUniswapV3Pool. 70 | error InvalidUniswapV3Pool(); 71 | 72 | /// @notice Thrown when the passed volatility oracle address is invalid. 73 | error InvalidVolatilityOracle(); 74 | 75 | /// @notice Thrown when no fee tier was specified via setDefaultFeeTierForTokenPair. 76 | error NoFeeTierSpecifiedForTokenPair(); 77 | 78 | /** 79 | * ////////// HELPERS /////////// 80 | */ 81 | 82 | /** 83 | * @notice Retrieves the implied volatility of a ERC20 token. 84 | * @param tokenA The ERC20 token for which to retrieve historical volatility. 85 | * @param tokenB The ERC20 token for which to retrieve historical volatility. 86 | * @param tier The Uniswap fee tier for the desired pool on which to derive a 87 | * volatility measurement. 88 | * @return impliedVolatility The implied volatility of the token, scaled by 1e18 89 | */ 90 | function getImpliedVolatility(address tokenA, address tokenB, UniswapV3FeeTier tier) 91 | external 92 | view 93 | returns (uint256 impliedVolatility); 94 | 95 | /** 96 | * @notice Retrieves the uniswap v3 pool for passed ERC20 address plus arguments 97 | * from setUniswapV3Pool. 98 | * @param tokenA The contract address of the ERC20 for which to retrieve the v3 pool. 99 | * @param tokenB The contract address of the ERC20 for which to retrieve the v3 pool. 100 | * @param fee The fee tier for the pool in 1/100ths of a bip. 101 | * @return pool The uniswap v3 pool for the supplied token. 102 | */ 103 | function getV3PoolForTokensAndFee(address tokenA, address tokenB, uint24 fee) 104 | external 105 | view 106 | returns (IUniswapV3Pool pool); 107 | 108 | /** 109 | * @notice Retrieves the uniswap fee in 1/100ths of a bip. 110 | * @param tier The fee tier enum. 111 | * @return The fee in 1/100ths of a bip. 112 | */ 113 | function getUniswapV3FeeInHundredthsOfBip(UniswapV3FeeTier tier) external pure returns (uint24); 114 | 115 | /** 116 | * @notice Updates the cached implied volatility for the tokens in the refresh list. 117 | * @return timestamp The timestamp of the cache refresh. 118 | */ 119 | function refreshVolatilityCache() external returns (uint256 timestamp); 120 | 121 | /** 122 | * @notice Returns the fee tier associated with a particular token pair. 123 | * @param tokenA Token for which to retrieve the fee tier. 124 | * @param tokenB Token for which to retrieve the fee tier. 125 | * @return The fee tier associated with the pair. 126 | */ 127 | function getDefaultFeeTierForTokenPair(address tokenA, address tokenB) external view returns (UniswapV3FeeTier); 128 | 129 | /** 130 | * @notice Updates the cached implied volatility for the supplied pool info. 131 | * @param info The UniswapV3Pool info corresponding to the pool to refresh. 132 | * @return timestamp The timestamp of the cache refresh. 133 | */ 134 | function refreshVolatilityCacheAndMetadataForPool(UniswapV3PoolInfo calldata info) 135 | external 136 | returns (uint256 timestamp); 137 | 138 | /** 139 | * @notice Sets the default uniswap v3 fee tier for the token pair. Used when retrieving 140 | * the uniswap v3 pool for a given pair. 141 | * @param tokenA The contract address of the ERC20 for which to retrieve the v3 pool. 142 | * @param tokenB The contract address of the ERC20 for which to retrieve the v3 pool. 143 | * @return The set values 144 | */ 145 | function setDefaultFeeTierForTokenPair(address tokenA, address tokenB, UniswapV3FeeTier tier) 146 | external 147 | returns (address, address, UniswapV3FeeTier); 148 | 149 | /** 150 | * ////////// TOKEN REFRESH LIST ////////// 151 | */ 152 | 153 | /** 154 | * @notice Sets the list of tokens and fees to periodically refresh for implied volatility. 155 | * @param list The token refresh list. 156 | * @return The token refresh list. 157 | */ 158 | function setTokenFeeTierRefreshList(UniswapV3PoolInfo[] calldata list) 159 | external 160 | returns (UniswapV3PoolInfo[] memory); 161 | 162 | /** 163 | * @notice Gets the list of tokens and fees to periodically refresh for implied volatility. 164 | * @return The token refresh list. 165 | */ 166 | function getTokenFeeTierRefreshList() external view returns (UniswapV3PoolInfo[] memory); 167 | 168 | // The below was heavily inspired by Aloe Finance's Blend 169 | 170 | /** 171 | * @notice Accesses the most recently stored metadata for a given Uniswap pool 172 | * @dev These values may or may not have been initialized and may or may not be 173 | * up to date. `tickSpacing` will be non-zero if they've been initialized. 174 | * @param pool The Uniswap pool for which metadata should be retrieved 175 | * @return maxSecondsAgo The age of the oldest observation in the pool's oracle 176 | * @return gamma0 The pool fee minus the protocol fee on token0, scaled by 1e6 177 | * @return gamma1 The pool fee minus the protocol fee on token1, scaled by 1e6 178 | * @return tickSpacing The pool's tick spacing 179 | */ 180 | function cachedPoolMetadata(IUniswapV3Pool pool) 181 | external 182 | view 183 | returns (uint32 maxSecondsAgo, uint24 gamma0, uint24 gamma1, int24 tickSpacing); 184 | 185 | /** 186 | * @notice Accesses any of the 25 most recently stored fee growth structs 187 | * @dev The full array (idx=0,1,2...24) has data that spans *at least* 24 hours 188 | * @param pool The Uniswap pool for which fee growth should be retrieved 189 | * @param idx The index into the storage array 190 | * @return feeGrowthGlobal0X128 Total pool revenue in token0, as of timestamp 191 | * @return feeGrowthGlobal1X128 Total pool revenue in token1, as of timestamp 192 | * @return timestamp The time at which snapshot was taken and stored 193 | */ 194 | function feeGrowthGlobals(IUniswapV3Pool pool, uint256 idx) 195 | external 196 | view 197 | returns (uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128, uint32 timestamp); 198 | 199 | /** 200 | * @notice Returns indices that the contract will use to access `feeGrowthGlobals` 201 | * @param pool The Uniswap pool for which array indices should be fetched 202 | * @return read The index that was closest to 24 hours old last time `estimate24H` was called 203 | * @return write The index that was written to last time `estimate24H` was called 204 | */ 205 | function feeGrowthGlobalsIndices(IUniswapV3Pool pool) external view returns (uint8 read, uint8 write); 206 | 207 | /** 208 | * @notice Updates cached metadata for a Uniswap pool. Must be called at least once 209 | * in order for volatility to be determined. Should also be called whenever 210 | * protocol fee changes 211 | * @param pool The Uniswap pool to poke 212 | */ 213 | function cacheMetadataFor(IUniswapV3Pool pool) external; 214 | 215 | /** 216 | * @notice Provides multiple estimates of IV using all stored `feeGrowthGlobals` entries for `pool` 217 | * @dev This is not meant to be used on-chain, and it doesn't contribute to the oracle's knowledge. 218 | * Please use `estimate24H` instead. 219 | * @param pool The pool to use for volatility estimate 220 | * @return impliedVolatility The array of volatility estimates, scaled by 1e18 221 | */ 222 | function lens(IUniswapV3Pool pool) external view returns (uint256[25] memory impliedVolatility); 223 | 224 | /** 225 | * @notice Estimates 24-hour implied volatility for a Uniswap pool. 226 | * @param pool The pool to use for volatility estimate 227 | * @return impliedVolatility The estimated volatility, scaled by 1e18 228 | */ 229 | function estimate24H(IUniswapV3Pool pool) external returns (uint256 impliedVolatility); 230 | } 231 | -------------------------------------------------------------------------------- /src/interfaces/IVolatilityOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | /** 5 | * @notice This is an interface for contracts providing historical volatility, 6 | * implied volatility, or both. 7 | * This is used internally in order to provide a uniform way of interacting with 8 | * various volatility oracles. An external volatility oracle can be used seamlessly 9 | * by being wrapped in a contract implementing this interface. 10 | */ 11 | interface IVolatilityOracle { 12 | /** 13 | * @notice Retrieves the historical volatility of a ERC20 token. 14 | * @param token The ERC20 token for which to retrieve historical volatility. 15 | * @return historicalVolatility The historical volatility of the token, scaled by 1e18 16 | */ 17 | function getHistoricalVolatility(address token) external view returns (uint256 historicalVolatility); 18 | 19 | /** 20 | * @notice Retrieves the implied volatility of a ERC20 token. 21 | * @param tokenA The ERC20 token for which to retrieve historical volatility. 22 | * @param tokenB The ERC20 token for which to retrieve historical volatility. 23 | * volatility measurement. 24 | * @return impliedVolatility The implied volatility of the token, scaled by scale() 25 | */ 26 | function getImpliedVolatility(address tokenA, address tokenB) external view returns (uint256 impliedVolatility); 27 | 28 | /** 29 | * @notice Returns the scaling factor for the volatility 30 | * @return scale The power of 10 by which the return is scaled 31 | */ 32 | function scale() external view returns (uint8 scale); 33 | } 34 | -------------------------------------------------------------------------------- /src/interfaces/IYieldOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "./IAdmin.sol"; 5 | 6 | /** 7 | * @notice This is an interface for contracts providing token yields. 8 | * This is used internally in order to provide a uniform way of interacting with 9 | * various yield oracles. An external yield oracle can be used seamlessly 10 | * by being wrapped in a contract implementing this interface. 11 | */ 12 | interface IYieldOracle { 13 | /** 14 | * @notice Retrieves the yield of a given token address 15 | * @param token The ERC20 token address for which to retrieve yield 16 | * @return tokenYield The per-second yield for this given token 17 | */ 18 | function getTokenYield(address token) external view returns (uint256 tokenYield); 19 | 20 | /** 21 | * @notice Returns the scaling factor for the yield 22 | * @return The power of 10 by which the return is scaled 23 | */ 24 | function scale() external view returns (uint8); 25 | } 26 | -------------------------------------------------------------------------------- /src/libraries/FixedPoint96.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.13; 3 | 4 | /// @title FixedPoint96 5 | /// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) 6 | /// From https://github.com/aloelabs/aloe-blend 7 | library FixedPoint96 { 8 | uint8 internal constant RESOLUTION = 96; 9 | uint256 internal constant Q96 = 0x1000000000000000000000000; 10 | } 11 | -------------------------------------------------------------------------------- /src/libraries/FullMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | /// @title Contains 512-bit math functions 5 | /// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision 6 | /// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits 7 | /// From https://github.com/aloelabs/aloe-blend 8 | library FullMath { 9 | /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 10 | /// @param a The multiplicand 11 | /// @param b The multiplier 12 | /// @param denominator The divisor 13 | /// @return result The 256-bit result 14 | /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv 15 | function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) { 16 | // Handle division by zero 17 | require(denominator != 0, "mulDiv: divide by zero"); 18 | 19 | // 512-bit multiply [prod1 prod0] = a * b 20 | // Compute the product mod 2**256 and mod 2**256 - 1 21 | // then use the Chinese Remainder Theorem to reconstruct 22 | // the 512 bit result. The result is stored in two 256 23 | // variables such that product = prod1 * 2**256 + prod0 24 | uint256 prod0; // Least significant 256 bits of the product 25 | uint256 prod1; // Most significant 256 bits of the product 26 | assembly { 27 | let mm := mulmod(a, b, not(0)) 28 | prod0 := mul(a, b) 29 | prod1 := sub(sub(mm, prod0), lt(mm, prod0)) 30 | } 31 | 32 | // Short circuit 256 by 256 division 33 | // This saves gas when a * b is small, at the cost of making the 34 | // large case a bit more expensive. Depending on your use case you 35 | // may want to remove this short circuit and always go through the 36 | // 512 bit path. 37 | if (prod1 == 0) { 38 | assembly { 39 | result := div(prod0, denominator) 40 | } 41 | return result; 42 | } 43 | 44 | /////////////////////////////////////////////// 45 | // 512 by 256 division. 46 | /////////////////////////////////////////////// 47 | 48 | // Handle overflow, the result must be < 2**256 49 | require(prod1 < denominator, "mulDiv: overflow"); 50 | 51 | // Make division exact by subtracting the remainder from [prod1 prod0] 52 | // Compute remainder using mulmod 53 | // Note mulmod(_, _, 0) == 0 54 | uint256 remainder; 55 | assembly { 56 | remainder := mulmod(a, b, denominator) 57 | } 58 | // Subtract 256 bit number from 512 bit number 59 | assembly { 60 | prod1 := sub(prod1, gt(remainder, prod0)) 61 | prod0 := sub(prod0, remainder) 62 | } 63 | 64 | // Factor powers of two out of denominator 65 | // Compute largest power of two divisor of denominator. 66 | // Always >= 1. 67 | unchecked { 68 | // https://ethereum.stackexchange.com/a/96646 69 | uint256 twos = (type(uint256).max - denominator + 1) & denominator; 70 | // Divide denominator by power of two 71 | assembly { 72 | denominator := div(denominator, twos) 73 | } 74 | 75 | // Divide [prod1 prod0] by the factors of two 76 | assembly { 77 | prod0 := div(prod0, twos) 78 | } 79 | // Shift in bits from prod1 into prod0. For this we need 80 | // to flip `twos` such that it is 2**256 / twos. 81 | // If twos is zero, then it becomes one 82 | assembly { 83 | twos := add(div(sub(0, twos), twos), 1) 84 | } 85 | prod0 |= prod1 * twos; 86 | 87 | // Invert denominator mod 2**256 88 | // Now that denominator is an odd number, it has an inverse 89 | // modulo 2**256 such that denominator * inv = 1 mod 2**256. 90 | // Compute the inverse by starting with a seed that is correct 91 | // correct for four bits. That is, denominator * inv = 1 mod 2**4 92 | // If denominator is zero the inverse starts with 2 93 | uint256 inv = (3 * denominator) ^ 2; 94 | // Now use Newton-Raphson iteration to improve the precision. 95 | // Thanks to Hensel's lifting lemma, this also works in modular 96 | // arithmetic, doubling the correct bits in each step. 97 | inv *= 2 - denominator * inv; // inverse mod 2**8 98 | inv *= 2 - denominator * inv; // inverse mod 2**16 99 | inv *= 2 - denominator * inv; // inverse mod 2**32 100 | inv *= 2 - denominator * inv; // inverse mod 2**64 101 | inv *= 2 - denominator * inv; // inverse mod 2**128 102 | inv *= 2 - denominator * inv; // inverse mod 2**256 103 | // If denominator is zero, inv is now 128 104 | 105 | // Because the division is now exact we can divide by multiplying 106 | // with the modular inverse of denominator. This will give us the 107 | // correct result modulo 2**256. Since the precoditions guarantee 108 | // that the outcome is less than 2**256, this is the final result. 109 | // We don't need to compute the high bits of the result and prod1 110 | // is no longer required. 111 | result = prod0 * inv; 112 | return result; 113 | } 114 | } 115 | 116 | /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 117 | /// @param a The multiplicand 118 | /// @param b The multiplier 119 | /// @param denominator The divisor 120 | /// @return result The 256-bit result 121 | function mulDivRoundingUp(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) { 122 | result = mulDiv(a, b, denominator); 123 | if (mulmod(a, b, denominator) > 0) { 124 | require(result < type(uint256).max, "mulDivRoundingUp: overflow"); 125 | result++; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/libraries/Oracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.13; 3 | 4 | import "v3-core/contracts/interfaces/IUniswapV3Pool.sol"; 5 | 6 | import "./FullMath.sol"; 7 | import "./TickMath.sol"; 8 | 9 | /// @title Oracle 10 | /// @notice Provides functions to integrate with V3 pool oracle 11 | /// From https://github.com/aloelabs/aloe-blend 12 | library Oracle { 13 | /** 14 | * @notice Calculates time-weighted means of tick and liquidity for a given Uniswap V3 pool 15 | * @param pool Address of the pool that we want to observe 16 | * @param secondsAgo Number of seconds in the past from which to calculate the time-weighted means 17 | * @return arithmeticMeanTick The arithmetic mean tick from (block.timestamp - secondsAgo) to block.timestamp 18 | * @return secondsPerLiquidityX128 The change in seconds per liquidity from (block.timestamp - secondsAgo) 19 | * to block.timestamp 20 | */ 21 | function consult(IUniswapV3Pool pool, uint32 secondsAgo) 22 | internal 23 | view 24 | returns (int24 arithmeticMeanTick, uint160 secondsPerLiquidityX128) 25 | { 26 | require(secondsAgo != 0, "BP"); 27 | 28 | uint32[] memory secondsAgos = new uint32[](2); 29 | secondsAgos[0] = secondsAgo; 30 | secondsAgos[1] = 0; 31 | 32 | (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) = 33 | pool.observe(secondsAgos); 34 | 35 | int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0]; 36 | arithmeticMeanTick = int24(tickCumulativesDelta / int32(secondsAgo)); 37 | // Always round to negative infinity 38 | if (tickCumulativesDelta < 0 && (tickCumulativesDelta % int32(secondsAgo) != 0)) { 39 | arithmeticMeanTick--; 40 | } 41 | 42 | secondsPerLiquidityX128 = secondsPerLiquidityCumulativeX128s[1] - secondsPerLiquidityCumulativeX128s[0]; 43 | } 44 | 45 | /** 46 | * @notice Given a pool, it returns the number of seconds ago of the oldest stored observation 47 | * @param pool Address of Uniswap V3 pool that we want to observe 48 | * @param observationIndex The observation index from pool.slot0() 49 | * @param observationCardinality The observationCardinality from pool.slot0() 50 | * @dev (, , uint16 observationIndex, uint16 observationCardinality, , , ) = pool.slot0(); 51 | * @return secondsAgo The number of seconds ago that the oldest observation was stored 52 | */ 53 | function getMaxSecondsAgo(IUniswapV3Pool pool, uint16 observationIndex, uint16 observationCardinality) 54 | internal 55 | view 56 | returns (uint32 secondsAgo) 57 | { 58 | require(observationCardinality != 0, "NI"); 59 | 60 | unchecked { 61 | (uint32 observationTimestamp,,, bool initialized) = 62 | pool.observations((observationIndex + 1) % observationCardinality); 63 | 64 | // The next index might not be initialized if the cardinality is in the process of increasing 65 | // In this case the oldest observation is always in index 0 66 | if (!initialized) { 67 | (observationTimestamp,,,) = pool.observations(0); 68 | } 69 | 70 | secondsAgo = uint32(block.timestamp) - observationTimestamp; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/libraries/TickMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.13; 3 | 4 | /// @title Math library for computing sqrt prices from ticks and vice versa 5 | /// @notice Computes sqrt price for ticks of size 1.0001, i.e. sqrt(1.0001^tick) as fixed point Q64.96 numbers. Supports 6 | /// prices between 2**-128 and 2**128 7 | /// From https://github.com/aloelabs/aloe-blend 8 | library TickMath { 9 | /// @dev The minimum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**-128 10 | int24 internal constant MIN_TICK = -887272; 11 | /// @dev The maximum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**128 12 | int24 internal constant MAX_TICK = -MIN_TICK; 13 | 14 | /// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) 15 | uint160 internal constant MIN_SQRT_RATIO = 4295128739; 16 | /// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) 17 | uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; 18 | 19 | /// @notice Calculates sqrt(1.0001^tick) * 2^96 20 | /// @dev Throws if |tick| > max tick 21 | /// @param tick The input tick for the above formula 22 | /// @return sqrtPriceX96 A Fixed point Q64.96 number representing the sqrt of the ratio of the two assets (token1/token0) 23 | /// at the given tick 24 | function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { 25 | uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); 26 | require(absTick <= uint256(uint24(MAX_TICK)), "T"); 27 | 28 | uint256 ratio = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000; 29 | unchecked { 30 | if (absTick & 0x2 != 0) { 31 | ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; 32 | } 33 | if (absTick & 0x4 != 0) { 34 | ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; 35 | } 36 | if (absTick & 0x8 != 0) { 37 | ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; 38 | } 39 | if (absTick & 0x10 != 0) { 40 | ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; 41 | } 42 | if (absTick & 0x20 != 0) { 43 | ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; 44 | } 45 | if (absTick & 0x40 != 0) { 46 | ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; 47 | } 48 | if (absTick & 0x80 != 0) { 49 | ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; 50 | } 51 | if (absTick & 0x100 != 0) { 52 | ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; 53 | } 54 | if (absTick & 0x200 != 0) { 55 | ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; 56 | } 57 | if (absTick & 0x400 != 0) { 58 | ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; 59 | } 60 | if (absTick & 0x800 != 0) { 61 | ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; 62 | } 63 | if (absTick & 0x1000 != 0) { 64 | ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; 65 | } 66 | if (absTick & 0x2000 != 0) { 67 | ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; 68 | } 69 | if (absTick & 0x4000 != 0) { 70 | ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; 71 | } 72 | if (absTick & 0x8000 != 0) { 73 | ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; 74 | } 75 | if (absTick & 0x10000 != 0) { 76 | ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; 77 | } 78 | if (absTick & 0x20000 != 0) { 79 | ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; 80 | } 81 | if (absTick & 0x40000 != 0) { 82 | ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; 83 | } 84 | if (absTick & 0x80000 != 0) { 85 | ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; 86 | } 87 | 88 | if (tick > 0) { 89 | ratio = type(uint256).max / ratio; 90 | } 91 | 92 | // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. 93 | // we then downcast because we know the result always fits within 160 bits due to our tick input constraint 94 | // we round up in the division so getTickAtSqrtRatio of the output price is always consistent 95 | sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1)); 96 | } 97 | } 98 | 99 | /// @notice Calculates the greatest tick value such that getRatioAtTick(tick) <= ratio 100 | /// @dev Throws in case sqrtPriceX96 < MIN_SQRT_RATIO, as MIN_SQRT_RATIO is the lowest value getRatioAtTick may 101 | /// ever return. 102 | /// @param sqrtPriceX96 The sqrt ratio for which to compute the tick as a Q64.96 103 | /// @return tick The greatest tick for which the ratio is less than or equal to the input ratio 104 | function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24 tick) { 105 | // second inequality must be < because the price can never reach the price at the max tick 106 | require(sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO, "R"); 107 | uint256 ratio = uint256(sqrtPriceX96) << 32; 108 | 109 | uint256 r = ratio; 110 | uint256 msb = 0; 111 | 112 | assembly { 113 | let f := shl(7, gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) 114 | msb := or(msb, f) 115 | r := shr(f, r) 116 | } 117 | assembly { 118 | let f := shl(6, gt(r, 0xFFFFFFFFFFFFFFFF)) 119 | msb := or(msb, f) 120 | r := shr(f, r) 121 | } 122 | assembly { 123 | let f := shl(5, gt(r, 0xFFFFFFFF)) 124 | msb := or(msb, f) 125 | r := shr(f, r) 126 | } 127 | assembly { 128 | let f := shl(4, gt(r, 0xFFFF)) 129 | msb := or(msb, f) 130 | r := shr(f, r) 131 | } 132 | assembly { 133 | let f := shl(3, gt(r, 0xFF)) 134 | msb := or(msb, f) 135 | r := shr(f, r) 136 | } 137 | assembly { 138 | let f := shl(2, gt(r, 0xF)) 139 | msb := or(msb, f) 140 | r := shr(f, r) 141 | } 142 | assembly { 143 | let f := shl(1, gt(r, 0x3)) 144 | msb := or(msb, f) 145 | r := shr(f, r) 146 | } 147 | assembly { 148 | let f := gt(r, 0x1) 149 | msb := or(msb, f) 150 | } 151 | 152 | if (msb >= 128) { 153 | r = ratio >> (msb - 127); 154 | } else { 155 | r = ratio << (127 - msb); 156 | } 157 | 158 | int256 lg2 = (int256(msb) - 128) << 64; 159 | 160 | assembly { 161 | r := shr(127, mul(r, r)) 162 | let f := shr(128, r) 163 | lg2 := or(lg2, shl(63, f)) 164 | r := shr(f, r) 165 | } 166 | assembly { 167 | r := shr(127, mul(r, r)) 168 | let f := shr(128, r) 169 | lg2 := or(lg2, shl(62, f)) 170 | r := shr(f, r) 171 | } 172 | assembly { 173 | r := shr(127, mul(r, r)) 174 | let f := shr(128, r) 175 | lg2 := or(lg2, shl(61, f)) 176 | r := shr(f, r) 177 | } 178 | assembly { 179 | r := shr(127, mul(r, r)) 180 | let f := shr(128, r) 181 | lg2 := or(lg2, shl(60, f)) 182 | r := shr(f, r) 183 | } 184 | assembly { 185 | r := shr(127, mul(r, r)) 186 | let f := shr(128, r) 187 | lg2 := or(lg2, shl(59, f)) 188 | r := shr(f, r) 189 | } 190 | assembly { 191 | r := shr(127, mul(r, r)) 192 | let f := shr(128, r) 193 | lg2 := or(lg2, shl(58, f)) 194 | r := shr(f, r) 195 | } 196 | assembly { 197 | r := shr(127, mul(r, r)) 198 | let f := shr(128, r) 199 | lg2 := or(lg2, shl(57, f)) 200 | r := shr(f, r) 201 | } 202 | assembly { 203 | r := shr(127, mul(r, r)) 204 | let f := shr(128, r) 205 | lg2 := or(lg2, shl(56, f)) 206 | r := shr(f, r) 207 | } 208 | assembly { 209 | r := shr(127, mul(r, r)) 210 | let f := shr(128, r) 211 | lg2 := or(lg2, shl(55, f)) 212 | r := shr(f, r) 213 | } 214 | assembly { 215 | r := shr(127, mul(r, r)) 216 | let f := shr(128, r) 217 | lg2 := or(lg2, shl(54, f)) 218 | r := shr(f, r) 219 | } 220 | assembly { 221 | r := shr(127, mul(r, r)) 222 | let f := shr(128, r) 223 | lg2 := or(lg2, shl(53, f)) 224 | r := shr(f, r) 225 | } 226 | assembly { 227 | r := shr(127, mul(r, r)) 228 | let f := shr(128, r) 229 | lg2 := or(lg2, shl(52, f)) 230 | r := shr(f, r) 231 | } 232 | assembly { 233 | r := shr(127, mul(r, r)) 234 | let f := shr(128, r) 235 | lg2 := or(lg2, shl(51, f)) 236 | r := shr(f, r) 237 | } 238 | assembly { 239 | r := shr(127, mul(r, r)) 240 | let f := shr(128, r) 241 | lg2 := or(lg2, shl(50, f)) 242 | } 243 | 244 | int256 logSqrt10001 = lg2 * 255738958999603826347141; // 128.128 number 245 | 246 | int24 tickLow = int24((logSqrt10001 - 3402992956809132418596140100660247210) >> 128); 247 | int24 tickHi = int24((logSqrt10001 + 291339464771989622907027621153398088495) >> 128); 248 | 249 | tick = tickLow == tickHi ? tickLow : getSqrtRatioAtTick(tickHi) <= sqrtPriceX96 ? tickHi : tickLow; 250 | } 251 | 252 | /// @notice Rounds down to the nearest tick where tick % tickSpacing == 0 253 | /// @param tick The tick to round 254 | /// @param tickSpacing The tick spacing to round to 255 | /// @return the floored tick 256 | /// @dev Ensure tick +/- tickSpacing does not overflow or underflow int24 257 | function floor(int24 tick, int24 tickSpacing) internal pure returns (int24) { 258 | int24 mod = tick % tickSpacing; 259 | 260 | unchecked { 261 | if (mod >= 0) { 262 | return tick - mod; 263 | } 264 | return tick - mod - tickSpacing; 265 | } 266 | } 267 | 268 | /// @notice Rounds up to the nearest tick where tick % tickSpacing == 0 269 | /// @param tick The tick to round 270 | /// @param tickSpacing The tick spacing to round to 271 | /// @return the ceiled tick 272 | /// @dev Ensure tick +/- tickSpacing does not overflow or underflow int24 273 | function ceil(int24 tick, int24 tickSpacing) internal pure returns (int24) { 274 | int24 mod = tick % tickSpacing; 275 | 276 | unchecked { 277 | if (mod > 0) { 278 | return tick - mod + tickSpacing; 279 | } 280 | return tick - mod; 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/libraries/Volatility.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity ^0.8.13; 3 | 4 | import "solmate/utils/FixedPointMathLib.sol"; 5 | 6 | import "./FixedPoint96.sol"; 7 | import "./FullMath.sol"; 8 | import "./TickMath.sol"; 9 | 10 | /// @title Volatility 11 | /// @notice Provides functions that use Uniswap v3 to compute price volatility 12 | /// From https://github.com/aloelabs/aloe-blend 13 | library Volatility { 14 | struct PoolMetadata { 15 | // the oldest oracle observation that's been populated by the pool 16 | uint32 maxSecondsAgo; 17 | // the overall fee minus the protocol fee for token0, times 1e6 18 | uint24 gamma0; 19 | // the overall fee minus the protocol fee for token1, times 1e6 20 | uint24 gamma1; 21 | // the pool tick spacing 22 | int24 tickSpacing; 23 | } 24 | 25 | struct PoolData { 26 | // the current price (from pool.slot0()) 27 | uint160 sqrtPriceX96; 28 | // the current tick (from pool.slot0()) 29 | int24 currentTick; 30 | // the mean tick over some period (from OracleLibrary.consult(...)) 31 | int24 arithmeticMeanTick; 32 | // the mean liquidity over some period (from OracleLibrary.consult(...)) 33 | uint160 secondsPerLiquidityX128; 34 | // the number of seconds to look back when getting mean tick & mean liquidity 35 | uint32 oracleLookback; 36 | // the liquidity depth at currentTick (from pool.liquidity()) 37 | uint128 tickLiquidity; 38 | } 39 | 40 | struct FeeGrowthGlobals { 41 | // the fee growth as a Q128.128 fees of token0 collected per unit of liquidity for the entire life of the pool 42 | uint256 feeGrowthGlobal0X128; 43 | // the fee growth as a Q128.128 fees of token1 collected per unit of liquidity for the entire life of the pool 44 | uint256 feeGrowthGlobal1X128; 45 | // the block timestamp at which feeGrowthGlobal0X128 and feeGrowthGlobal1X128 were last updated 46 | uint32 timestamp; 47 | } 48 | 49 | /** 50 | * @notice Estimates implied volatility using https://lambert-guillaume.medium.com/on-chain-volatility-and-uniswap-v3-d031b98143d1 51 | * @param metadata The pool's metadata (may be cached) 52 | * @param data A summary of the pool's state from `pool.slot0` `pool.observe` and `pool.liquidity` 53 | * @param a The pool's cumulative feeGrowthGlobals some time in the past 54 | * @param b The pool's cumulative feeGrowthGlobals as of the current block 55 | * @return An estimate of the 24 hour implied volatility scaled by 1e18 56 | */ 57 | function estimate24H( 58 | PoolMetadata memory metadata, 59 | PoolData memory data, 60 | FeeGrowthGlobals memory a, 61 | FeeGrowthGlobals memory b 62 | ) internal pure returns (uint256) { 63 | uint256 volumeGamma0Gamma1; 64 | { 65 | uint128 revenue0Gamma1 = computeRevenueGamma( 66 | a.feeGrowthGlobal0X128, 67 | b.feeGrowthGlobal0X128, 68 | data.secondsPerLiquidityX128, 69 | data.oracleLookback, 70 | metadata.gamma1 71 | ); 72 | uint128 revenue1Gamma0 = computeRevenueGamma( 73 | a.feeGrowthGlobal1X128, 74 | b.feeGrowthGlobal1X128, 75 | data.secondsPerLiquidityX128, 76 | data.oracleLookback, 77 | metadata.gamma0 78 | ); 79 | // This is an approximation. Ideally the fees earned during each swap would be multiplied by the price 80 | // *at that swap*. But for prices simulated with GBM and swap sizes either normally or uniformly distributed, 81 | // the error you get from using geometric mean price is <1% even with high drift and volatility. 82 | volumeGamma0Gamma1 = revenue1Gamma0 + amount0ToAmount1(revenue0Gamma1, data.arithmeticMeanTick); 83 | } 84 | 85 | uint128 sqrtTickTVLX32 = uint128( 86 | FixedPointMathLib.sqrt( 87 | computeTickTVLX64(metadata.tickSpacing, data.currentTick, data.sqrtPriceX96, data.tickLiquidity) 88 | ) 89 | ); 90 | uint48 timeAdjustmentX32 = uint48(FixedPointMathLib.sqrt((uint256(1 days) << 64) / (b.timestamp - a.timestamp))); 91 | 92 | if (sqrtTickTVLX32 == 0) { 93 | return 0; 94 | } 95 | unchecked { 96 | return (uint256(2e18) * uint256(timeAdjustmentX32) * FixedPointMathLib.sqrt(volumeGamma0Gamma1)) 97 | / sqrtTickTVLX32; 98 | } 99 | } 100 | 101 | /** 102 | * @notice Computes an `amount1` that (at `tick`) is equivalent in worth to the provided `amount0` 103 | * @param amount0 The amount of token0 to convert 104 | * @param tick The tick at which the conversion should hold true 105 | * @return amount1 An equivalent amount of token1 106 | */ 107 | function amount0ToAmount1(uint128 amount0, int24 tick) internal pure returns (uint256 amount1) { 108 | uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick); 109 | uint224 priceX96 = uint224(FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, FixedPoint96.Q96)); 110 | 111 | amount1 = FullMath.mulDiv(amount0, priceX96, FixedPoint96.Q96); 112 | } 113 | 114 | /** 115 | * @notice Computes pool revenue using feeGrowthGlobal accumulators, then scales it down by a factor of gamma 116 | * @param feeGrowthGlobalAX128 The value of feeGrowthGlobal (either 0 or 1) at time A 117 | * @param feeGrowthGlobalBX128 The value of feeGrowthGlobal (either 0 or 1, but matching) at time B (B > A) 118 | * @param secondsPerLiquidityX128 The difference in the secondsPerLiquidity accumulator from `secondsAgo` seconds ago until now 119 | * @param secondsAgo The oracle lookback period that was used to find `secondsPerLiquidityX128` 120 | * @param gamma The fee factor to scale by 121 | * @return Revenue over the period from `block.timestamp - secondsAgo` to `block.timestamp`, scaled down by a factor of gamma 122 | */ 123 | function computeRevenueGamma( 124 | uint256 feeGrowthGlobalAX128, 125 | uint256 feeGrowthGlobalBX128, 126 | uint160 secondsPerLiquidityX128, 127 | uint32 secondsAgo, 128 | uint24 gamma 129 | ) internal pure returns (uint128) { 130 | unchecked { 131 | uint256 temp; 132 | 133 | if (feeGrowthGlobalBX128 >= feeGrowthGlobalAX128) { 134 | // feeGrowthGlobal has increased from time A to time B 135 | temp = feeGrowthGlobalBX128 - feeGrowthGlobalAX128; 136 | } else { 137 | // feeGrowthGlobal has overflowed between time A and time B 138 | temp = type(uint256).max - feeGrowthGlobalAX128 + feeGrowthGlobalBX128; 139 | } 140 | 141 | temp = FullMath.mulDiv(temp, secondsAgo * gamma, secondsPerLiquidityX128 * 1e6); 142 | return temp > type(uint128).max ? type(uint128).max : uint128(temp); 143 | } 144 | } 145 | 146 | /** 147 | * @notice Computes the value of liquidity available at the current tick, denominated in token1 148 | * @param tickSpacing The pool tick spacing (from pool.tickSpacing()) 149 | * @param tick The current tick (from pool.slot0()) 150 | * @param sqrtPriceX96 The current price (from pool.slot0()) 151 | * @param liquidity The liquidity depth at currentTick (from pool.liquidity()) 152 | */ 153 | function computeTickTVLX64(int24 tickSpacing, int24 tick, uint160 sqrtPriceX96, uint128 liquidity) 154 | internal 155 | pure 156 | returns (uint256 tickTVL) 157 | { 158 | tick = TickMath.floor(tick, tickSpacing); 159 | 160 | // both value0 and value1 fit in uint192 161 | (uint256 value0, uint256 value1) = _getValuesOfLiquidity( 162 | sqrtPriceX96, TickMath.getSqrtRatioAtTick(tick), TickMath.getSqrtRatioAtTick(tick + tickSpacing), liquidity 163 | ); 164 | tickTVL = (value0 + value1) << 64; 165 | } 166 | 167 | /** 168 | * @notice Computes the value of the liquidity in terms of token1 169 | * @dev Each return value can fit in a uint192 if necessary 170 | * @param sqrtRatioX96 A sqrt price representing the current pool prices 171 | * @param sqrtRatioAX96 A sqrt price representing the lower tick boundary 172 | * @param sqrtRatioBX96 A sqrt price representing the upper tick boundary 173 | * @param liquidity The liquidity being valued 174 | * @return value0 The value of amount0 underlying `liquidity`, in terms of token1 175 | * @return value1 The amount of token1 176 | */ 177 | function _getValuesOfLiquidity( 178 | uint160 sqrtRatioX96, 179 | uint160 sqrtRatioAX96, 180 | uint160 sqrtRatioBX96, 181 | uint128 liquidity 182 | ) private pure returns (uint256 value0, uint256 value1) { 183 | assert(sqrtRatioAX96 <= sqrtRatioX96 && sqrtRatioX96 <= sqrtRatioBX96); 184 | 185 | unchecked { 186 | uint224 numerator = uint224(FullMath.mulDiv(sqrtRatioX96, sqrtRatioBX96 - sqrtRatioX96, FixedPoint96.Q96)); 187 | 188 | value0 = FullMath.mulDiv(liquidity, numerator, sqrtRatioBX96); 189 | value1 = FullMath.mulDiv(liquidity, sqrtRatioX96 - sqrtRatioAX96, FixedPoint96.Q96); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/utils/Admin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "../interfaces/IAdmin.sol"; 5 | 6 | abstract contract Admin is IAdmin { 7 | address internal admin; 8 | 9 | modifier requiresAdmin() { 10 | require(msg.sender == admin, "!ADMIN"); 11 | _; 12 | } 13 | 14 | function setAdmin(address _admin) external requiresAdmin { 15 | require(_admin != address(0x0), "INVALID ADMIN"); 16 | admin = _admin; 17 | emit AdminSet(_admin); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/Keep3rV2Job.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "../interfaces/IKeep3rV2Job.sol"; 5 | import "./Admin.sol"; 6 | 7 | import "keep3r/solidity/interfaces/IKeep3r.sol"; 8 | 9 | abstract contract Keep3rV2Job is IKeep3rV2Job, Admin { 10 | address public keep3r; 11 | 12 | // taken from https://docs.keep3r.network/core/jobs#simple-keeper 13 | modifier validateAndPayKeeper(address _keeper) { 14 | _isValidKeeper(_keeper); 15 | _; 16 | IKeep3r(keep3r).worked(_keeper); 17 | } 18 | 19 | function setKeep3r(address _keep3r) public requiresAdmin { 20 | _setKeep3r(_keep3r); 21 | } 22 | 23 | function getKeep3r() public view returns (address) { 24 | return keep3r; 25 | } 26 | 27 | function _setKeep3r(address _keep3r) internal { 28 | keep3r = _keep3r; 29 | emit Keep3rSet(_keep3r); 30 | } 31 | 32 | function _isValidKeeper(address _keeper) internal { 33 | if (!IKeep3r(keep3r).isKeeper(_keeper)) { 34 | revert InvalidKeeper(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/ChainlinkPriceOracle.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "../src/ChainlinkPriceOracle.sol"; 7 | 8 | contract ChainlinkPriceOracleTest is Test { 9 | event LogString(string topic); 10 | event LogAddress(string topic, address info); 11 | event LogUint(string topic, uint256 info); 12 | event LogInt(string topic, int256 info); 13 | 14 | event AdminSet(address indexed admin); 15 | event PriceFeedSet(address indexed token, address indexed priceFeed); 16 | 17 | IERC20 private constant DAI_ADDRESS = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); 18 | IERC20 private constant USDC_ADDRESS = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); 19 | IERC20 private constant WETH_ADDRESS = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 20 | 21 | AggregatorV3Interface private constant DAI_USD_FEED = 22 | AggregatorV3Interface(0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9); 23 | 24 | ChainlinkPriceOracle public oracle; 25 | 26 | function setUp() public { 27 | oracle = new ChainlinkPriceOracle(); 28 | } 29 | 30 | function testAdmin() public { 31 | // some random addr 32 | vm.prank(address(1)); 33 | vm.expectRevert(bytes("!ADMIN")); 34 | oracle.setAdmin(address(this)); 35 | 36 | vm.expectEmit(true, false, false, false); 37 | emit AdminSet(address(1)); 38 | oracle.setAdmin(address(1)); 39 | 40 | // address changes back to this, instead of 1 41 | vm.expectRevert(bytes("!ADMIN")); 42 | oracle.setAdmin(address(this)); 43 | 44 | // test that setting price feed fails if not admin 45 | vm.expectRevert(bytes("!ADMIN")); 46 | address fakeErc20 = address(2); 47 | address fakeFeed = address(3); 48 | oracle.setPriceFeed(IERC20(fakeErc20), AggregatorV3Interface(fakeFeed)); 49 | 50 | // succeeds if admin 51 | vm.prank(address(1)); 52 | oracle.setPriceFeed(IERC20(fakeErc20), AggregatorV3Interface(fakeFeed)); 53 | } 54 | 55 | function testSetPriceFeeds() public { 56 | vm.expectEmit(true, true, false, false); 57 | emit PriceFeedSet(address(DAI_ADDRESS), address(DAI_USD_FEED)); 58 | oracle.setPriceFeed(DAI_ADDRESS, DAI_USD_FEED); 59 | 60 | // TODO: assert revert invalid feed/erc20 61 | } 62 | 63 | function testPriceFeeds() public { 64 | oracle.setPriceFeed(DAI_ADDRESS, DAI_USD_FEED); 65 | (uint256 price, uint8 scale) = oracle.getPriceUSD(DAI_ADDRESS); 66 | assertEq(price, 99983112); 67 | assertEq(scale, 8); 68 | // TODO: assert revert if feed not initialized 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/CompoundV3YieldOracle.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "../src/interfaces/ICompoundV3YieldOracle.sol"; 7 | import "../src/interfaces/IERC20.sol"; 8 | import "../src/interfaces/IComet.sol"; 9 | 10 | import "../src/CompoundV3YieldOracle.sol"; 11 | 12 | contract CompoundV3YieldOracleTest is Test { 13 | using stdStorage for StdStorage; 14 | 15 | event LogString(string topic); 16 | event LogAddress(string topic, address info); 17 | event LogUint(string topic, uint256 info); 18 | event LogInt(string topic, int256 info); 19 | 20 | event CometSet(address indexed token, address indexed comet); 21 | 22 | IComet public constant COMET_USDC = IComet(0xc3d688B66703497DAA19211EEdff47f25384cdc3); 23 | IERC20 public constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); 24 | IERC20 public constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 25 | address public constant KEEP3R_ADDRESS = 0xeb02addCfD8B773A5FFA6B9d1FE99c566f8c44CC; 26 | uint16 public constant DEFAULT_SNAPSHOT_ARRAY_SIZE = 5; 27 | uint16 public constant MAXIMUM_SNAPSHOT_ARRAY_SIZE = 3 * 5; 28 | 29 | CompoundV3YieldOracle public oracle; 30 | 31 | struct AssetInfo { 32 | uint8 offset; 33 | address asset; 34 | address priceFeed; 35 | uint64 scale; 36 | uint64 borrowCollateralFactor; 37 | uint64 liquidateCollateralFactor; 38 | uint64 liquidationFactor; 39 | uint128 supplyCap; 40 | } 41 | 42 | struct TotalsCollateral { 43 | uint128 totalSupplyAsset; 44 | uint128 _reserved; 45 | } 46 | 47 | function setUp() public { 48 | oracle = new CompoundV3YieldOracle(KEEP3R_ADDRESS); 49 | } 50 | 51 | function testConstructor() public { 52 | assertEq(address(COMET_USDC), address(oracle.tokenAddressToComet(USDC))); 53 | assertEq(address(0), address(oracle.tokenAddressToComet(IERC20(address(0))))); 54 | } 55 | 56 | function testSetComet() public { 57 | vm.expectRevert(ICompoundV3YieldOracle.InvalidTokenAddress.selector); 58 | oracle.setCometAddress(address(0), address(COMET_USDC)); 59 | 60 | vm.expectRevert(ICompoundV3YieldOracle.InvalidCometAddress.selector); 61 | oracle.setCometAddress(address(this), address(0)); 62 | 63 | // e.g. if 'this' were an ERC20 64 | vm.expectEmit(true, true, false, false); 65 | emit CometSet(address(this), address(COMET_USDC)); 66 | oracle.setCometAddress(address(this), address(COMET_USDC)); 67 | assertEq(address(COMET_USDC), address(oracle.tokenAddressToComet(IERC20(address(this))))); 68 | } 69 | 70 | function testSetSnapshotArraySize() public { 71 | (uint16 initIdx, ICompoundV3YieldOracle.SupplyRateSnapshot[] memory snapshots) = 72 | oracle.getCometSnapshots(address(USDC)); 73 | uint16 initSz = uint16(snapshots.length); 74 | assertEq(initSz, DEFAULT_SNAPSHOT_ARRAY_SIZE); 75 | assertEq(initIdx, 0); 76 | 77 | // assert that the snapshot array maintains the same size if a smaller size 78 | // is provided 79 | uint16 snapshotSz = oracle.setCometSnapshotBufferSize(address(USDC), initSz - 1); 80 | assertEq(snapshotSz, DEFAULT_SNAPSHOT_ARRAY_SIZE); 81 | 82 | // assert that the snapshot array can grow 83 | snapshotSz = oracle.setCometSnapshotBufferSize(address(USDC), initSz + 1); 84 | assertEq(snapshotSz, DEFAULT_SNAPSHOT_ARRAY_SIZE + 1); 85 | 86 | // assert a revert if the max size cap is exceeded 87 | vm.expectRevert(ICompoundV3YieldOracle.SnapshotArraySizeTooLarge.selector); 88 | oracle.setCometSnapshotBufferSize(address(USDC), MAXIMUM_SNAPSHOT_ARRAY_SIZE + 1); 89 | } 90 | 91 | function testGetSpotYield() public { 92 | vm.expectRevert( 93 | abi.encodeWithSelector(ICompoundV3YieldOracle.CometAddressNotSpecifiedForToken.selector, address(this)) 94 | ); 95 | oracle.getTokenYield(address(this)); 96 | 97 | uint256 yield = oracle.latchCometRate(address(USDC)); 98 | emit LogUint("usdc yield", yield); 99 | 100 | // blockno 15441384 101 | assertEq(yield, 723951975); 102 | } 103 | 104 | function testUninitializedSnapshots() public { 105 | (uint16 idx, ICompoundV3YieldOracle.SupplyRateSnapshot[] memory snapshots) = 106 | oracle.getCometSnapshots(address(USDC)); 107 | assertEq(idx, 0); 108 | for (uint256 i = 0; i < snapshots.length; i++) { 109 | assertEq(snapshots[i].timestamp, 0); 110 | assertEq(snapshots[i].supplyRate, 0); 111 | } 112 | } 113 | 114 | function testSnapshotUpdate() public { 115 | oracle.latchCometRate(address(USDC)); 116 | (uint16 idx, ICompoundV3YieldOracle.SupplyRateSnapshot[] memory snapshots) = 117 | oracle.getCometSnapshots(address(USDC)); 118 | assertEq(idx, 1); 119 | assertEq(snapshots[idx - 1].timestamp, block.timestamp); 120 | assertFalse(snapshots[idx - 1].supplyRate == 0); 121 | } 122 | 123 | function testMaxRateSwing() public { 124 | oracle.latchCometRate(address(USDC)); 125 | 126 | // grant a lot of ETH 127 | _writeTokenBalance(address(this), address(WETH), 1_000_000_000 ether); 128 | WETH.approve(address(COMET_USDC), 1_000_000_000 ether); 129 | 130 | (,,,, uint256 amountToSupplyCap) = _getAndLogCometInfo(); 131 | 132 | (uint256 supplyRate,) = _logAndValidateSpotYieldAgainstOracle(); 133 | COMET_USDC.supply(address(WETH), amountToSupplyCap); 134 | _getAndLogCometInfo(); 135 | (uint256 supplyRate2,) = _logAndValidateSpotYieldAgainstOracle(); 136 | 137 | assertEq(supplyRate, supplyRate2); 138 | assertTrue(COMET_USDC.isBorrowCollateralized(address(this))); 139 | 140 | uint256 toBorrow = 2_000_000; 141 | uint256 usdcScale = 10 ** 6; 142 | 143 | // flex utilization limits 144 | COMET_USDC.withdraw(address(USDC), toBorrow * usdcScale); 145 | 146 | oracle.latchCometRate(address(USDC)); 147 | (uint256 supplyRate3,) = _logAndValidateSpotYieldAgainstOracle(); 148 | assertGt(supplyRate3, supplyRate2); 149 | } 150 | 151 | function testTimeWeightedReturn() public { 152 | uint256 toBorrow = 2_000_000; 153 | uint256 usdcScale = 10 ** 6; 154 | (,,,, uint256 amountToSupplyCap) = _getAndLogCometInfo(); 155 | 156 | _writeTokenBalance(address(this), address(WETH), 1_000_000_000 ether); 157 | WETH.approve(address(COMET_USDC), 1_000_000_000 ether); 158 | USDC.approve(address(COMET_USDC), 1_000_000_000 ether); 159 | COMET_USDC.supply(address(WETH), amountToSupplyCap); 160 | 161 | // withdraw, supply loop 162 | for (uint256 i = 0; i < 10; i++) { 163 | // borrow 164 | oracle.latchCometRate(address(USDC)); 165 | COMET_USDC.withdraw(address(USDC), toBorrow * usdcScale); 166 | vm.warp(block.timestamp + i * 10_000); 167 | 168 | // repay 169 | oracle.latchCometRate(address(USDC)); 170 | COMET_USDC.supply(address(USDC), toBorrow * usdcScale); 171 | vm.warp(block.timestamp + i * 10_000); 172 | } 173 | 174 | (uint16 idx, ICompoundV3YieldOracle.SupplyRateSnapshot[] memory snapshots) = 175 | oracle.getCometSnapshots(address(USDC)); 176 | 177 | uint256 timeWeightedYield = oracle.getTokenYield(address(USDC)); 178 | assertEq(idx, 0); 179 | assertEq(snapshots.length, 5); 180 | assertEq(timeWeightedYield, 2211616289); 181 | } 182 | 183 | function testExistingTokenForNewComet() public { 184 | uint256 gasBefore = gasleft(); 185 | oracle.setCometAddress(address(this), address(1)); 186 | vm.expectRevert(); 187 | oracle.tokenRefreshList(2); 188 | 189 | // pretend we're updating the existing comet address 190 | oracle.setCometAddress(address(this), address(2)); 191 | // this should still revert since we haven't extended the array 192 | vm.expectRevert(); 193 | oracle.tokenRefreshList(2); 194 | 195 | uint256 gasLeft = gasBefore - gasleft(); 196 | // fix gas to ensure O(1) complexity isn't changed 197 | emit LogUint("gasLeft", gasLeft); 198 | assertEq(gasLeft, 139778); 199 | } 200 | 201 | function _writeTokenBalance(address who, address token, uint256 amt) internal { 202 | stdstore.target(token).sig(IERC20(token).balanceOf.selector).with_key(who).checked_write(amt); 203 | } 204 | 205 | function _getAndLogCometInfo() 206 | internal 207 | returns ( 208 | int256 reserve, 209 | uint256 totalSupply, 210 | uint128 totalSuppliedWETH, 211 | uint128 wethSupplyCap, 212 | uint256 amountToSupplyCap 213 | ) 214 | { 215 | IComet.AssetInfo memory wethInfo = COMET_USDC.getAssetInfoByAddress(address(WETH)); 216 | IComet.TotalsCollateral memory totalWeth = COMET_USDC.totalsCollateral(address(WETH)); 217 | 218 | reserve = COMET_USDC.getReserves(); 219 | totalSupply = COMET_USDC.totalSupply(); 220 | totalSuppliedWETH = totalWeth.totalSupplyAsset; 221 | wethSupplyCap = wethInfo.supplyCap; 222 | amountToSupplyCap = wethInfo.supplyCap - totalWeth.totalSupplyAsset; 223 | 224 | emit LogUint("cUSDCv3 reserves ", uint256(reserve)); 225 | emit LogUint("cUSDCv3 total supply", totalSupply); 226 | emit LogUint("supplied WETH ", uint256(totalWeth.totalSupplyAsset)); 227 | emit LogUint("WETH supply cap ", uint256(wethInfo.supplyCap)); 228 | emit LogUint("Amount to supply cap", uint256(amountToSupplyCap)); 229 | } 230 | 231 | function _getAndLogUtilization() internal returns (uint256 utilization) { 232 | utilization = COMET_USDC.getUtilization(); 233 | emit LogUint("cUSDCv3 utilization", utilization); 234 | } 235 | 236 | function _getAndLogSupplyRate() internal returns (uint256 supplyRate) { 237 | uint256 utilization = _getAndLogUtilization(); 238 | supplyRate = COMET_USDC.getSupplyRate(utilization); 239 | emit LogUint("cUSDCv3 supply rate", supplyRate); 240 | } 241 | 242 | function _getAndLogSpotYield() internal returns (uint256 yield) { 243 | (uint16 idx, ICompoundV3YieldOracle.SupplyRateSnapshot[] memory snapshots) = 244 | oracle.getCometSnapshots(address(USDC)); 245 | uint16 oldestIdx = _getMostRecentIndex(idx, snapshots); 246 | ICompoundV3YieldOracle.SupplyRateSnapshot memory oldestSnapshot = snapshots[oldestIdx]; 247 | yield = oldestSnapshot.supplyRate; 248 | emit LogUint("USDC oracle yield", yield); 249 | } 250 | 251 | function _getOldestIndex(uint16 idx, ICompoundV3YieldOracle.SupplyRateSnapshot[] memory snapshots) 252 | internal 253 | pure 254 | returns (uint16) 255 | { 256 | if (snapshots[idx].timestamp == 0) { 257 | return 0; 258 | } 259 | 260 | return uint16((idx + 1) % snapshots.length); 261 | } 262 | 263 | function _getMostRecentIndex(uint16 idx, ICompoundV3YieldOracle.SupplyRateSnapshot[] memory snapshots) 264 | internal 265 | pure 266 | returns (uint16) 267 | { 268 | if (idx == 0) { 269 | return uint16(snapshots.length - 1); 270 | } 271 | return idx - 1; 272 | } 273 | 274 | function _logAndValidateSpotYieldAgainstOracle() internal returns (uint256 supplyRate, uint256 yield) { 275 | supplyRate = _getAndLogSupplyRate(); 276 | yield = _getAndLogSpotYield(); 277 | assertEq(supplyRate, yield); 278 | } 279 | 280 | function _perSecondRateToApr(uint256 perSecondRate) internal pure returns (uint256 apr) { 281 | uint256 secondsPerYear = 60 * 60 * 24 * 365; 282 | apr = perSecondRate * secondsPerYear; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /test/UniswapV3VolatilityOracle.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL 1.1 2 | pragma solidity 0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol"; 6 | import "keep3r/solidity/interfaces/IKeep3r.sol"; 7 | 8 | import "../src/UniswapV3VolatilityOracle.sol"; 9 | import "../src/interfaces/IKeep3rV2Job.sol"; 10 | import "../src/interfaces/IVolatilityOracle.sol"; 11 | 12 | /// for writeBalance 13 | interface IERC20 { 14 | function balanceOf(address) external view returns (uint256); 15 | 16 | function transfer(address to, uint256 amount) external returns (bool); 17 | } 18 | 19 | contract UniswapV3VolatilityOracleTest is Test, IUniswapV3SwapCallback { 20 | using stdStorage for StdStorage; 21 | 22 | event LogString(string topic); 23 | event LogAddress(string topic, address info); 24 | event LogUint(string topic, uint256 info); 25 | event LogInt(string topic, int256 info); 26 | event LogPoolObs(string msg, address pool, uint16 obsInd, uint16 obsCard); 27 | 28 | // IUniswapV3VolatilityOracle events 29 | event VolatilityOracleSet(address indexed oracle); 30 | event VolatilityOracleCacheUpdated(uint256 timestamp); 31 | event TokenVolatilityUpdated( 32 | address indexed tokenA, address indexed tokenB, uint24 feeTier, uint256 volatility, uint256 timestamp 33 | ); 34 | event AdminSet(address indexed admin); 35 | event TokenRefreshListSet(); 36 | 37 | address private constant KEEP3R_ADDRESS = 0xeb02addCfD8B773A5FFA6B9d1FE99c566f8c44CC; 38 | 39 | address private constant DAI_ADDRESS = 0x6B175474E89094C44Da98b954EedeAC495271d0F; 40 | address private constant USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; 41 | address private constant FUN_ADDRESS = 0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b; 42 | address private constant LUSD_ADDRESS = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; 43 | address private constant LINK_ADDRESS = 0x514910771AF9Ca656af840dff83E8264EcF986CA; 44 | address private constant WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 45 | address private constant SNX_ADDRESS = 0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F; 46 | 47 | uint24 private constant POINT_ZERO_ONE_PCT_FEE = 1 * 100; 48 | uint24 private constant POINT_ZERO_FIVE_PCT_FEE = 5 * 100; 49 | uint24 private constant POINT_THREE_PCT_FEE = 3 * 100 * 10; 50 | 51 | /// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) 52 | uint160 internal constant MIN_SQRT_RATIO = 4295128739; 53 | /// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) 54 | uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; 55 | 56 | UniswapV3VolatilityOracle public oracle; 57 | 58 | IUniswapV3VolatilityOracle.UniswapV3PoolInfo[] private defaultTokenRefreshList; 59 | 60 | function setUp() public { 61 | oracle = new UniswapV3VolatilityOracle(KEEP3R_ADDRESS); 62 | 63 | vm.makePersistent(address(oracle)); 64 | 65 | delete defaultTokenRefreshList; 66 | defaultTokenRefreshList.push( 67 | IUniswapV3VolatilityOracle.UniswapV3PoolInfo( 68 | USDC_ADDRESS, DAI_ADDRESS, IUniswapV3VolatilityOracle.UniswapV3FeeTier.PCT_POINT_01 69 | ) 70 | ); 71 | defaultTokenRefreshList.push( 72 | IUniswapV3VolatilityOracle.UniswapV3PoolInfo( 73 | FUN_ADDRESS, DAI_ADDRESS, IUniswapV3VolatilityOracle.UniswapV3FeeTier.PCT_POINT_01 74 | ) 75 | ); 76 | defaultTokenRefreshList.push( 77 | IUniswapV3VolatilityOracle.UniswapV3PoolInfo( 78 | WETH_ADDRESS, DAI_ADDRESS, IUniswapV3VolatilityOracle.UniswapV3FeeTier.PCT_POINT_3 79 | ) 80 | ); 81 | } 82 | 83 | function testAdmin() public { 84 | // some random addr 85 | vm.prank(address(1)); 86 | vm.expectRevert(bytes("!ADMIN")); 87 | oracle.setAdmin(address(this)); 88 | 89 | vm.expectEmit(true, false, false, false); 90 | emit AdminSet(address(1)); 91 | oracle.setAdmin(address(1)); 92 | 93 | vm.expectRevert(bytes("!ADMIN")); 94 | oracle.setAdmin(address(this)); 95 | 96 | vm.expectRevert(bytes("!ADMIN")); 97 | oracle.refreshVolatilityCacheAndMetadataForPool(defaultTokenRefreshList[0]); 98 | 99 | vm.expectRevert(bytes("!ADMIN")); 100 | oracle.setTokenFeeTierRefreshList(defaultTokenRefreshList); 101 | 102 | vm.expectRevert(bytes("!ADMIN")); 103 | oracle.setDefaultFeeTierForTokenPair( 104 | USDC_ADDRESS, DAI_ADDRESS, IUniswapV3VolatilityOracle.UniswapV3FeeTier.PCT_POINT_3 105 | ); 106 | 107 | // USDC-USDT 108 | IUniswapV3Pool pool = IUniswapV3Pool(0x3416cF6C708Da44DB2624D63ea0AAef7113527C6); 109 | vm.expectRevert(bytes("!ADMIN")); 110 | oracle.cacheMetadataFor(pool); 111 | } 112 | 113 | function testGetUniswapV3Pool() public { 114 | IUniswapV3Pool pool = oracle.getV3PoolForTokensAndFee(USDC_ADDRESS, DAI_ADDRESS, POINT_ZERO_ONE_PCT_FEE); 115 | // USDC / DAI @ .01 pct 116 | assertEq(address(pool), 0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168); 117 | 118 | pool = oracle.getV3PoolForTokensAndFee(USDC_ADDRESS, DAI_ADDRESS, POINT_ZERO_FIVE_PCT_FEE); 119 | // USDC / DAI @ .05 pct 120 | assertEq(address(pool), 0x6c6Bc977E13Df9b0de53b251522280BB72383700); 121 | } 122 | 123 | function testSetRefreshTokenList() public { 124 | vm.expectEmit(false, false, false, false); 125 | emit TokenRefreshListSet(); 126 | oracle.setTokenFeeTierRefreshList(defaultTokenRefreshList); 127 | 128 | IUniswapV3VolatilityOracle.UniswapV3PoolInfo[] memory returnedRefreshList = oracle.getTokenFeeTierRefreshList(); 129 | assertEq(defaultTokenRefreshList, returnedRefreshList); 130 | } 131 | 132 | function testTokenVolatilityRefresh() public { 133 | // move forward 1 hour to allow for aloe data requirement 134 | vm.warp(block.timestamp + 1 hours + 1); 135 | oracle.setTokenFeeTierRefreshList(defaultTokenRefreshList); 136 | 137 | for (uint256 i = 0; i < defaultTokenRefreshList.length; i++) { 138 | address tokenA = defaultTokenRefreshList[i].tokenA; 139 | address tokenB = defaultTokenRefreshList[i].tokenB; 140 | IUniswapV3VolatilityOracle.UniswapV3FeeTier feeTier = defaultTokenRefreshList[i].feeTier; 141 | uint24 fee = oracle.getUniswapV3FeeInHundredthsOfBip(feeTier); 142 | vm.expectEmit(true, true, false, false); 143 | emit TokenVolatilityUpdated(tokenA, tokenB, fee, 0, 0); 144 | } 145 | vm.expectEmit(false, false, false, true); 146 | emit VolatilityOracleCacheUpdated(block.timestamp); 147 | uint256 ts = oracle.refreshVolatilityCache(); 148 | assertEq(ts, block.timestamp); 149 | } 150 | 151 | function testGetImpliedVolatility() public { 152 | // move forward 1 hour to allow for aloe data requirement 153 | vm.warp(block.timestamp + 1 hours + 1); 154 | oracle.setTokenFeeTierRefreshList(defaultTokenRefreshList); 155 | _cache1d(); 156 | emit LogString("cached one day"); 157 | for (uint256 i = 0; i < defaultTokenRefreshList.length; i++) { 158 | IUniswapV3VolatilityOracle.UniswapV3PoolInfo storage poolInfo = defaultTokenRefreshList[i]; 159 | _validateCachedVolatilityForPool(poolInfo); 160 | } 161 | } 162 | 163 | function testKeep3r() public { 164 | vm.expectRevert(IKeep3rV2Job.InvalidKeeper.selector); 165 | oracle.work(); 166 | 167 | vm.warp(block.timestamp + 1 hours + 1); 168 | oracle.setTokenFeeTierRefreshList(defaultTokenRefreshList); 169 | vm.mockCall(KEEP3R_ADDRESS, abi.encodeWithSelector(IKeep3rJobWorkable.isKeeper.selector), abi.encode(true)); 170 | vm.mockCall(KEEP3R_ADDRESS, abi.encodeWithSelector(IKeep3rJobWorkable.worked.selector), abi.encode("")); 171 | oracle.work(); 172 | } 173 | 174 | function testDefaultFeeTierManagement() public { 175 | // default fee tier should be uninitialized 176 | IUniswapV3VolatilityOracle.UniswapV3FeeTier tier = 177 | oracle.getDefaultFeeTierForTokenPair(USDC_ADDRESS, DAI_ADDRESS); 178 | assertEq(uint256(tier), uint256(IUniswapV3VolatilityOracle.UniswapV3FeeTier.RESERVED)); 179 | 180 | // calling getIV without specifing a default fee tier for a token pair should revert 181 | vm.expectRevert(IUniswapV3VolatilityOracle.NoFeeTierSpecifiedForTokenPair.selector); 182 | oracle.getImpliedVolatility(USDC_ADDRESS, DAI_ADDRESS); 183 | 184 | // assert the token pair is stored 185 | oracle.setDefaultFeeTierForTokenPair( 186 | USDC_ADDRESS, DAI_ADDRESS, IUniswapV3VolatilityOracle.UniswapV3FeeTier.PCT_POINT_01 187 | ); 188 | tier = oracle.getDefaultFeeTierForTokenPair(USDC_ADDRESS, DAI_ADDRESS); 189 | assertEq(uint256(tier), uint256(IUniswapV3VolatilityOracle.UniswapV3FeeTier.PCT_POINT_01)); 190 | 191 | oracle.setDefaultFeeTierForTokenPair( 192 | USDC_ADDRESS, DAI_ADDRESS, IUniswapV3VolatilityOracle.UniswapV3FeeTier.PCT_POINT_05 193 | ); 194 | tier = oracle.getDefaultFeeTierForTokenPair(USDC_ADDRESS, DAI_ADDRESS); 195 | assertEq(uint256(tier), uint256(IUniswapV3VolatilityOracle.UniswapV3FeeTier.PCT_POINT_05)); 196 | 197 | // assert that the value is used for get IV 198 | vm.expectRevert(bytes("IV Oracle: need more data")); 199 | oracle.getImpliedVolatility(USDC_ADDRESS, DAI_ADDRESS); 200 | } 201 | 202 | /** 203 | * /////////// IUniswapV3SwapCallback ///////////// 204 | */ 205 | function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) public { 206 | emit LogInt("uniswap swap callback, amount0", amount0Delta); 207 | emit LogInt("uniswap swap callback, amount1", amount1Delta); 208 | // only ever transferring DAI to the pool, extend this via data 209 | int256 amountToTransfer = amount0Delta > 0 ? amount0Delta : amount1Delta; 210 | emit LogUint("uniswap swap callback, amountToTransfer", uint256(amountToTransfer)); 211 | address poolAddr = _bytesToAddress(data); 212 | IUniswapV3Pool pool = IUniswapV3Pool(poolAddr); 213 | address erc20 = amount0Delta > 0 ? pool.token0() : pool.token1(); 214 | IERC20(erc20).transfer(poolAddr, uint256(amountToTransfer)); 215 | } 216 | 217 | /** 218 | * ///////// HELPERS ////////// 219 | */ 220 | function assertEq( 221 | IUniswapV3VolatilityOracle.UniswapV3PoolInfo[] memory a, 222 | IUniswapV3VolatilityOracle.UniswapV3PoolInfo[] memory b 223 | ) internal { 224 | // from forg-std/src/Test.sol 225 | if (keccak256(abi.encode(a)) != keccak256(abi.encode(b))) { 226 | emit log("Error: a == b not satisfied [UniswapV3PoolInfo[]]"); 227 | fail(); 228 | } 229 | } 230 | 231 | function _validateCachedVolatilityForPool(IUniswapV3VolatilityOracle.UniswapV3PoolInfo storage poolInfo) internal { 232 | address tokenA = poolInfo.tokenA; 233 | address tokenB = poolInfo.tokenB; 234 | IUniswapV3VolatilityOracle.UniswapV3FeeTier feeTier = poolInfo.feeTier; 235 | uint256 iv = oracle.getImpliedVolatility(tokenA, tokenB, feeTier); 236 | assertFalse(iv == 0, "Volatility is expected to have been refreshed"); 237 | } 238 | 239 | function _cache1d() internal { 240 | // get 24 hours 241 | oracle.setTokenFeeTierRefreshList(defaultTokenRefreshList); 242 | oracle.refreshVolatilityCache(); 243 | for (uint256 i = 0; i < 24; i++) { 244 | emit LogUint("cached hour", i); 245 | // fuzz trades 246 | _simulateUniswapMovements(); 247 | oracle.refreshVolatilityCache(); 248 | // refresh the pool metadata 249 | vm.warp(block.timestamp + 1 hours + 1); 250 | } 251 | 252 | vm.warp(block.timestamp + 3 hours); 253 | oracle.setTokenFeeTierRefreshList(defaultTokenRefreshList); 254 | } 255 | 256 | function _simulateUniswapMovements() internal { 257 | // add tokens to this contract 258 | _writeTokenBalance(address(this), address(DAI_ADDRESS), 1_000_000_000 ether); 259 | 260 | // iterate pools 261 | for (uint256 i = 0; i < defaultTokenRefreshList.length; i++) { 262 | IUniswapV3VolatilityOracle.UniswapV3PoolInfo memory poolInfo = defaultTokenRefreshList[i]; 263 | uint24 fee = oracle.getUniswapV3FeeInHundredthsOfBip(poolInfo.feeTier); 264 | IUniswapV3Pool pool = oracle.getV3PoolForTokensAndFee(poolInfo.tokenA, poolInfo.tokenB, fee); 265 | bool zeroForOne = pool.token0() == DAI_ADDRESS; 266 | // swap tokens on each pool 267 | (int256 amount0, int256 amount1) = pool.swap( 268 | address(this), 269 | zeroForOne, 270 | 1_000_000 ether, 271 | zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, 272 | abi.encodePacked(address(pool)) 273 | ); 274 | 275 | vm.warp(block.timestamp + 1 hours + 1); 276 | 277 | // swap it back 278 | pool.swap( 279 | address(this), 280 | !zeroForOne, 281 | zeroForOne ? -amount1 : -amount0, 282 | !zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, 283 | abi.encodePacked(address(pool)) 284 | ); 285 | 286 | // go back in time 287 | vm.warp(block.timestamp - 1 hours); 288 | (,, uint16 obsInd, uint16 obsCard,,,) = pool.slot0(); 289 | emit LogPoolObs("number of observations in pool", address(pool), obsInd, obsCard); 290 | } 291 | } 292 | 293 | function _writeTokenBalance(address who, address token, uint256 amt) internal { 294 | stdstore.target(token).sig(IERC20(token).balanceOf.selector).with_key(who).checked_write(amt); 295 | } 296 | 297 | function _bytesToAddress(bytes memory bys) internal pure returns (address addr) { 298 | assembly { 299 | addr := mload(add(bys, 0x14)) 300 | } 301 | } 302 | // TODO: Keep3r tests 303 | } 304 | -------------------------------------------------------------------------------- /test/VolatilityOracle.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "../src/UniswapV3VolatilityOracle.sol"; 7 | 8 | contract VolatilityOracleTest is Test { 9 | UniswapV3VolatilityOracle public volatilityOracle; 10 | IUniswapV3Pool public pool; 11 | 12 | function setUp() public { 13 | volatilityOracle = new UniswapV3VolatilityOracle(address(0)); 14 | // USDC-USDT 15 | pool = IUniswapV3Pool(0x3416cF6C708Da44DB2624D63ea0AAef7113527C6); 16 | } 17 | 18 | function testCacheMetadataFor() public { 19 | volatilityOracle.cacheMetadataFor(pool); 20 | 21 | (uint32 maxSecondsAgo, uint24 gamma0, uint24 gamma1, int24 tickSpacing) = 22 | volatilityOracle.cachedPoolMetadata(pool); 23 | 24 | assertEq(maxSecondsAgo, 472649); 25 | assertEq(gamma0, 100); 26 | assertEq(gamma1, 100); 27 | assertEq(tickSpacing, 1); 28 | } 29 | 30 | function testEstimate24H1() public { 31 | volatilityOracle.cacheMetadataFor(pool); 32 | uint256 iv = volatilityOracle.estimate24H(pool); 33 | assertEq(iv, 376869329814192); 34 | 35 | (uint256 feeGrowthGlobal0, uint256 feeGrowthGlobal1, uint256 timestamp) = 36 | volatilityOracle.feeGrowthGlobals(pool, 1); 37 | assertEq(feeGrowthGlobal0, 11372636660945467326552285182743240); 38 | assertEq(feeGrowthGlobal1, 95319452585414684384167792149219305); 39 | assertEq(timestamp, 1661876584); 40 | } 41 | 42 | function testEstimate24H2() public { 43 | volatilityOracle.cacheMetadataFor(pool); 44 | uint256 iv1 = volatilityOracle.estimate24H(pool); 45 | assertEq(iv1, 376869329814192); 46 | 47 | vm.warp(block.timestamp + 30 minutes); 48 | 49 | uint256 iv2 = volatilityOracle.estimate24H(pool); 50 | assertEq(iv2, 0); 51 | 52 | (,, uint256 timestamp) = volatilityOracle.feeGrowthGlobals(pool, 1); 53 | assertEq(timestamp, 1661876584); 54 | (,, timestamp) = volatilityOracle.feeGrowthGlobals(pool, 2); 55 | assertEq(timestamp, 0); 56 | 57 | vm.warp(block.timestamp + 31 minutes); 58 | assertEq(block.timestamp, 1661880244); 59 | 60 | volatilityOracle.estimate24H(pool); 61 | 62 | (,, timestamp) = volatilityOracle.feeGrowthGlobals(pool, 1); 63 | assertEq(timestamp, 1661876584); 64 | (,, timestamp) = volatilityOracle.feeGrowthGlobals(pool, 2); 65 | assertEq(timestamp, 1661880244); 66 | } 67 | 68 | function testEstimate24H3() public { 69 | volatilityOracle.cacheMetadataFor(pool); 70 | 71 | uint256 timestamp; 72 | uint8 readIndex; 73 | uint8 writeIndex; 74 | 75 | for (uint8 i; i < 28; i++) { 76 | volatilityOracle.estimate24H(pool); 77 | (readIndex, writeIndex) = volatilityOracle.feeGrowthGlobalsIndices(pool); 78 | 79 | if (i == 0) { 80 | assertEq(readIndex, 0); 81 | } else if (i < 25) { 82 | assertEq(readIndex, 1); 83 | } else { 84 | assertEq(readIndex, (i + 2) % 25); 85 | } 86 | assertEq(writeIndex, (i + 1) % 25); 87 | 88 | (,, timestamp) = volatilityOracle.feeGrowthGlobals(pool, writeIndex); 89 | assertEq(timestamp, block.timestamp); 90 | 91 | if (i >= 24) { 92 | (,, timestamp) = volatilityOracle.feeGrowthGlobals(pool, readIndex); 93 | assertEq(block.timestamp - timestamp, 24 hours + 24 minutes); 94 | } 95 | 96 | vm.warp(block.timestamp + 61 minutes); 97 | } 98 | } 99 | 100 | function testEstimate24H4() public { 101 | volatilityOracle.cacheMetadataFor(pool); 102 | 103 | volatilityOracle.estimate24H(pool); 104 | vm.warp(block.timestamp + 61 minutes); 105 | volatilityOracle.estimate24H(pool); 106 | vm.warp(block.timestamp + 61 minutes); 107 | volatilityOracle.estimate24H(pool); 108 | vm.warp(block.timestamp + 24 hours); 109 | volatilityOracle.estimate24H(pool); 110 | 111 | (uint8 readIndex,) = volatilityOracle.feeGrowthGlobalsIndices(pool); 112 | assertEq(readIndex, 3); 113 | } 114 | } 115 | --------------------------------------------------------------------------------