From bf352a3f678b42e263e3852858604aec9a39a0f8 Mon Sep 17 00:00:00 2001 From: AlienTornadosaurusHex <> Date: Tue, 6 Jun 2023 21:13:54 +0000 Subject: [PATCH] Finalize untested CurveFeeOracle Signed-off-by: AlienTornadosaurusHex <> --- src/v2/CurveFeeOracle.sol | 298 ++++++++++++++++++++++++++++++++++ src/v2/FeeOracleManager.sol | 4 +- src/v2/InstanceRegistry.sol | 14 +- src/v2/TornadoRouter.sol | 7 + src/v2/UniswapV3FeeOracle.sol | 178 +++++++++++--------- 5 files changed, 414 insertions(+), 87 deletions(-) create mode 100644 src/v2/CurveFeeOracle.sol diff --git a/src/v2/CurveFeeOracle.sol b/src/v2/CurveFeeOracle.sol new file mode 100644 index 0000000..db04b39 --- /dev/null +++ b/src/v2/CurveFeeOracle.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +// OZ imports + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Tornado imports + +import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ITornadoInstance.sol"; + +// Local imports + +import { IFeeOracle } from "./interfaces/IFeeOracle.sol"; + +import { UniswapV3OracleHelper } from "./libraries/UniswapV3OracleHelper.sol"; + +import { UniswapV3FeeOracle } from "./UniswapV3FeeOracle.sol"; + +import { InstanceData } from "./InstanceRegistry.sol"; + +/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CURVE FINANCE INTERFACE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +interface ICurvePriceOracle { + /// @dev For Plain2 and CryptoSwap2 + function price_oracle() external view returns (uint256); + /// @dev For Tricrypto pools + function price_oracle(uint256 k) external view returns (uint256); +} + +/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ CURVE FEE ORACLE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +/** + * @title CurveChainedOracles + * @author AlienTornadosaurusHex + * @notice This library is designed to help us simplify the workflow and introduce at least some packing to + * not waste large amounts of gas due to simplifying. Obviously, the best solution would be to just deploy a + * per-coin oracle, but I've wanted to have a general implementation because it makes the Governance process + * easier. + */ +library CurveChainedOracles { + bytes4 public constant PRICE_ORACLE_SELECTOR = bytes4(keccak256(("price_oracle()"))); + bytes4 public constant PRICE_ORACLE_UINT256_SELECTOR = bytes4(keccak256(("price_oracle(uint256)"))); + + /** + * @notice This function prepares data for chain calling Curve oracles. + * @dev The returns is called oracle because it contains sufficient data to execute the entire process, + * meaning it represents an oracle in a sense. + * @param _oracles Curve contracts compatible with either `price_oracle()` or `price_oracle(uint256)`. + * @param _selectors Selectors for contract, each is only either `keccak256()` or + * `keccak256('price_oracle(uint256)')` + * @param _coins Curve pool coin ids which are used for `price_oracle(uint256)` + * @param _invertPrice For each oracle capable pool, whether the token we are searching the price for is + * token0 or not, or in general whether we want to take the inverse of the price. + * @return oracle Packed bytes data which represents full data for a chained call to Curve oracles. + */ + function createChainedOracle( + ICurvePriceOracle[] memory _oracles, + bytes4[] memory _selectors, + uint8[] memory _coins, + bool[] memory _invertPrice + ) internal view returns (bytes memory oracle) { + uint256 numOracles = _oracles.length; + + for (uint256 o = 0; o < numOracles; o++) { + // The oracle which will be providing the price + ICurvePriceOracle _oracle = _oracles[o]; + + // The selector which determines the call + bytes4 _selector = _selectors[o]; + + // Index of the coin in the curve pool for second UINT256 selector + uint8 _coin = _coins[0]; + + // Check whether the config actually works + if (_selector == PRICE_ORACLE_SELECTOR) { + try _oracle.price_oracle() returns (uint256) { /* Works, do nothing */ } + catch { + require(false, "CurveChainedOracles: test call price_oracle() failed"); + } + } else if (_selector == PRICE_ORACLE_UINT256_SELECTOR) { + try _oracle.price_oracle(_coin) returns (uint256) { /* Works, do nothing */ } + catch { + require(false, "CurveChainedOracles: test call price_oracle(uint256) failed"); + } + } else { + require(false, "CurveChainedOracles: none or invalid selector"); + } + + // We prepend the former iteration, to have it in proper left to right order + oracle = abi.encodePacked(oracle, _oracle, _selector, _coin, _invertPrice[o]); + } + } + + /** + * @notice Function which unpacks `_oracle` data and chains the price calls to return one final price. + * @dev The name `price_oracle` was chosen because it reflects the Curve signature of the same name. + * @param _oracle The packed bytes data. + * @return Price of some token in some other token E18. + */ + function price_oracle(bytes memory _oracle) internal view returns (uint256) { + uint256 price = 1e18; + uint256 priceDivisor = 1e18; + uint256 inverseNumerator = 1e36; + + uint256 numOracles = _oracle.length; + + for (uint256 o = 0; o < numOracles; o++) { + bytes32 chunk; + + // Packed bytes can't be decoded, have to do it ourselves + // mload always reads bytes32, always offset by 26 bytes because data is always stored fully + // packed from left to right + assembly { + chunk := mload(add(_oracle, mul(26, add(o, 1)))) + } + + // Properly read in all of the packed data + + // Always packed from left to right, first 20 bytes is address + ICurvePriceOracle oracle = ICurvePriceOracle(address(bytes20(chunk))); + + // Then from 20 to 24 is selector, shift by 20x8=160 + bytes4 selector = bytes4(chunk << 160); + + // From 24 to 25 is coin index, shift by 160+4x8=192 + uint8 coin = uint8(bytes1(chunk << 192)); + + // From 25 to 26 is whether to invert price, shift by 192+1x8=200 + bool invertPrice = bytes1(chunk << 200) == 0x01; + + uint256 priceFromOracle; + + // Execute according to selector + if (selector == PRICE_ORACLE_SELECTOR) { + priceFromOracle = ICurvePriceOracle(oracle).price_oracle(); + } else if (selector == PRICE_ORACLE_UINT256_SELECTOR) { + priceFromOracle = ICurvePriceOracle(oracle).price_oracle(coin); + } + + // Check whether we need to invert price + if (invertPrice) { + priceFromOracle = inverseNumerator / priceFromOracle; + } + + // Calc price + price = (price * priceFromOracle) / priceDivisor; + } + + return price; + } +} + +/** + * @title CurveFeeOracle + * @author AlienTornadosaurusHex + * @notice Many Curve contracts now have an EMA/TWAP Oracle which can be used instead of having to rely on + * Chainlink (Offchain) Pricefeeds or Uniswap V3 Twap Oracles. We do not use Metapools because they do not + * have a safe enough TWAP oracle, instead we use the `price_oracle` capable contracts, which use a safer + * analytical solution. + */ +contract CurveFeeOracle is IFeeOracle { + using SafeMath for uint256; + using CurveChainedOracles for *; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + /* @dev When dividing by `d*1e18` */ + uint256 public constant INVERSE_NUMERATOR = 1e36; + + /* @dev The Governance Proxy address */ + address public immutable governanceProxyAddress; + + /* @dev For each instance, a set of data which translates to a set of chained price oracle calls, we call + this data "chainedPriceOracles" because it encodes all data necessary to execute the chain and calc the + price */ + mapping(ITornadoInstance => bytes) public chainedPriceOracles; + + /* @dev When setting, store the names as a historical record, key is keccak256(bytes) */ + mapping(bytes32 => string) public chainedPriceOracleNames; + + /* @dev Our Uniswap V3 Fee Oracle, we will need it some time for TORN/ETH */ + UniswapV3FeeOracle public v3FeeOracle; + + /* @dev We will not need the Uniswap V3 Oracle forever though, TORN/ETH pools are possible on Curve too */ + bool public tornOracleIsUniswapV3; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + event TornOracleIsCurve(); + event TornOracleIsUniswapV3(); + event UniswapV3FeeOracleUpdated(address _newUniswapV3FeeOracleAddress); + event ChainedOracleUpdated(address indexed instance, bytes32 newOracleHash, string newName); + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + constructor(address _governanceProxyAddress) public { + governanceProxyAddress = _governanceProxyAddress; + tornOracleIsUniswapV3 = true; + } + + modifier onlyGovernance() { + require(msg.sender == governanceProxyAddress, "UniswapV3FeeOracle: onlyGovernance"); + _; + } + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + function getFee( + IERC20 _torn, + ITornadoInstance _instance, + InstanceData memory, + uint64 _feePercent, + uint64 _feePercentDivisor + ) public view virtual override returns (uint160) { + // If fee is 0 return + if (_feePercent == 0) return 0; + + // Either TOKEN price in ETH or TOKEN price in TORN + uint256 price = chainedPriceOracles[_instance].price_oracle(); + + // If the below is true, then this will finalize the price and give the TOKEN price in TORN + // Above MUST be TOKEN price in ETH, then (ETH/TOKEN)/(ETH/TORN)=TORN/TOKEN, meaning the TOKEN price + // in TORN + if (tornOracleIsUniswapV3) { + price = price * UniswapV3OracleHelper.RATIO_DIVIDER + / v3FeeOracle.getPriceRatioOfTokens(_torn, IERC20(UniswapV3OracleHelper.WETH)); + } + + // The price is now either prepared by the chained oracles or by Uniswap V3, in any case we do the + // legacy calculation and return the fee value + return uint160( + _instance.denomination().mul(price).div(UniswapV3OracleHelper.RATIO_DIVIDER).mul( + uint256(_feePercent) + ).div(uint256(_feePercentDivisor)) + ); + } + + function getChainedOracleNameForInstance(ITornadoInstance _instance) + public + view + virtual + returns (string memory) + { + return chainedPriceOracleNames[getChainedOracleHashForInstance(_instance)]; + } + + function getChainedOracleNameForOracleHash(bytes32 _oracleHash) + public + view + virtual + returns (string memory) + { + return chainedPriceOracleNames[_oracleHash]; + } + + function getChainedOracleHashForInstance(ITornadoInstance _instance) + public + view + virtual + returns (bytes32) + { + return keccak256(chainedPriceOracles[_instance]); + } + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + function setChainedOracleForInstance( + ITornadoInstance _instance, + ICurvePriceOracle[] memory _oracles, + bytes4[] memory _selectors, + uint8[] memory _coins, + bool[] memory _invertPrice, + string memory _name + ) public virtual onlyGovernance { + bytes memory oracle = _oracles.createChainedOracle(_selectors, _coins, _invertPrice); + bytes32 oracleHash = keccak256(oracle); + + chainedPriceOracles[_instance] = oracle; + chainedPriceOracleNames[oracleHash] = _name; + + emit ChainedOracleUpdated(address(_instance), oracleHash, _name); + } + + function setUniswapV3FeeOracle(UniswapV3FeeOracle _uniswapV3FeeOracle) public virtual onlyGovernance { + v3FeeOracle = _uniswapV3FeeOracle; + emit UniswapV3FeeOracleUpdated(address(_uniswapV3FeeOracle)); + } + + function setTornOracleIsUniswapV3(bool _is) public virtual onlyGovernance { + tornOracleIsUniswapV3 = _is; + if (_is) emit TornOracleIsUniswapV3(); + else emit TornOracleIsCurve(); + } +} diff --git a/src/v2/FeeOracleManager.sol b/src/v2/FeeOracleManager.sol index f49375d..7c115fd 100644 --- a/src/v2/FeeOracleManager.sol +++ b/src/v2/FeeOracleManager.sol @@ -40,10 +40,10 @@ contract FeeManagerLegacyStorage { /* @dev From first contract, the only value we keep alive */ uint24 public feeUpdateInterval; - /* @dev From first contract, only used for initialization to preserve old values in the moment */ + /* @dev From first contract, only used for initialization to preserve old values */ mapping(ITornadoInstance => uint160) internal _oldFeesForInstance; - /* @dev From first contract, only used for initialization to preserve old values in the moment */ + /* @dev From first contract, only used for initialization to preserve old values */ mapping(ITornadoInstance => uint256) internal _oldFeesForInstanceUpdateTime; } diff --git a/src/v2/InstanceRegistry.sol b/src/v2/InstanceRegistry.sol index 6d65031..6e55ab9 100644 --- a/src/v2/InstanceRegistry.sol +++ b/src/v2/InstanceRegistry.sol @@ -111,17 +111,17 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali _; } - function initialize(address[] memory _instanceAddresses, address _router) + function initialize(ITornadoInstance[] memory _instances, address _router) external onlyGovernance initializer { - uint256 numInstances = _instanceAddresses.length; + uint256 numInstances = _instances.length; router = TornadoRouter(_router); for (uint256 i = 0; i < numInstances; i++) { - addInstance(ITornadoInstance(_instanceAddresses[i])); + addInstance(_instances[i]); } } @@ -187,6 +187,10 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali ITornadoInstance instance = ITornadoInstance(_instanceAddress); InstanceData memory data = instanceData[instance]; + // Checks whether already removed, so allowance can't be killed + + require(data.isEnabled, "InstanceRegistry: already removed"); + // Kill the allowance of the router first (arbitrary order) if (data.isERC20) { @@ -203,7 +207,7 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali ITornadoInstance lastInstance = instances[lastInstanceIndex]; - // Remove & swap + // Swap position of last instance with old instances[data.index] = lastInstance; @@ -211,7 +215,7 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali instances.pop(); - // Delete for removed instance data + // Delete old instance data delete instanceData[instance]; diff --git a/src/v2/TornadoRouter.sol b/src/v2/TornadoRouter.sol index 3a4d8c0..10e215d 100644 --- a/src/v2/TornadoRouter.sol +++ b/src/v2/TornadoRouter.sol @@ -42,6 +42,10 @@ contract TornadoRouter is Initializable { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ event EncryptedNote(address indexed sender, bytes encryptedNote); + event TokenApproved(address indexed spender, uint256 amount); + + event InstanceRegistryUpdated(address newInstanceRegistryProxyAddress); + event RelayerRegistryUpdated(address newRelayerRegistryProxyAddress); /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -141,14 +145,17 @@ contract TornadoRouter is Initializable { onlyInstanceRegistry { _token.safeApprove(_spender, _amount); + emit TokenApproved(_spender, _amount); } function setInstanceRegistry(address _newInstanceRegistryProxyAddress) external onlyGovernance { instanceRegistry = InstanceRegistry(_newInstanceRegistryProxyAddress); + emit InstanceRegistryUpdated(_newInstanceRegistryProxyAddress); } function setRelayerRegistry(address _newRelayerRegistryProxyAddress) external onlyGovernance { relayerRegistry = RelayerRegistry(_newRelayerRegistryProxyAddress); + emit RelayerRegistryUpdated(_newRelayerRegistryProxyAddress); } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ diff --git a/src/v2/UniswapV3FeeOracle.sol b/src/v2/UniswapV3FeeOracle.sol index 0580436..2401a16 100644 --- a/src/v2/UniswapV3FeeOracle.sol +++ b/src/v2/UniswapV3FeeOracle.sol @@ -25,10 +25,15 @@ import { UniswapV3OracleHelper } from "./libraries/UniswapV3OracleHelper.sol"; import { InstanceData } from "./InstanceRegistry.sol"; -struct UniswapV3FeeData { +/** + * @dev The global configuration for the `UniswapV3FeeOracle` contract. TORN is used in all `getFee()` calls, + * the interval seconds for the TWAP oracle are global, and the minimum observation cardinality assures the + * former interval can be requested. + */ +struct GlobalOracleConfig { uint24 tornPoolFee; - uint24 tokenPoolFee; - uint32 timePeriod; + uint32 twapIntervalSeconds; + uint16 minObservationCardinality; } /** @@ -44,33 +49,23 @@ contract UniswapV3FeeOracle is IFeeOracle { /* @dev The Governance Proxy address */ address public immutable governanceProxyAddress; - /* @dev The Uniswap V3 Factory */ - IUniswapV3Factory public immutable v3Factory; + /* @dev Global configuration valid across all `getFee()` (and other) calls */ + GlobalOracleConfig public globals; - /* @dev The global pool fee value which will always be used for TORN */ - uint24 public globalTornUniswapPoolFee; - - /* @dev The global time period value which will be used for all calculations */ - uint32 public globalUniswapTimePeriod; - - /* @dev The minimum observation cardinality required for a token */ - uint32 public globalMinObservationCardinality; - - /* @dev The feeData in which the above + token specific will be stored */ - mapping(IERC20 => UniswapV3FeeData) public uniswapFeeData; + /* @dev Uniswap pool fees for each token registered */ + mapping(IERC20 => uint24) public poolFeesByToken; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - event UniswapPoolFeeUpdated(IERC20 token, uint24 newUniswapPoolFee); - event GlobalTornUniswapPoolFeeUpdated(uint24 newUniswapPoolFee); - event GlobalUniswapTimePeriodUpdated(uint32 newUniswapTimePeriod); + event PoolFeeUpdated(IERC20 token, uint24 newPoolFee); + event GlobalTornPoolFeeUpdated(uint24 newPoolFee); + event GlobalTwapIntervalSecondsUpdated(uint32 newUniswapTimePeriod); event GlobalMinObservationCardinalityUpdated(uint16 newMinObservationCardinality); /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - constructor(address _governanceProxyAddress, address _uniswapV3FactoryAddress) public { + constructor(address _governanceProxyAddress) public { governanceProxyAddress = _governanceProxyAddress; - v3Factory = IUniswapV3Factory(_uniswapV3FactoryAddress); } modifier onlyGovernance() { @@ -78,56 +73,7 @@ contract UniswapV3FeeOracle is IFeeOracle { _; } - function setGlobalTornUniswapPoolFee(uint24 _newGlobalTornUniswapPoolFee) public virtual onlyGovernance { - globalTornUniswapPoolFee = _newGlobalTornUniswapPoolFee; - emit GlobalTornUniswapPoolFeeUpdated(_newGlobalTornUniswapPoolFee); - } - - function setGlobalUniswapTimePeriod(uint32 _newGlobalUniswapTimePeriod) public virtual onlyGovernance { - globalUniswapTimePeriod = _newGlobalUniswapTimePeriod; - emit GlobalUniswapTimePeriodUpdated(_newGlobalUniswapTimePeriod); - } - - function setGlobalMinObservationCardinality(uint16 _newGlobalMinObservationCardinality) - public - virtual - onlyGovernance - { - globalMinObservationCardinality = _newGlobalMinObservationCardinality; - emit GlobalMinObservationCardinalityUpdated(_newGlobalMinObservationCardinality); - } - - function setUniswapPoolFeeForToken(IERC20 _token, uint24 _tokenUniswapPoolFee) - public - virtual - onlyGovernance - { - uint24 _globalTornUniswapPoolFee = globalTornUniswapPoolFee; - uint32 _globalUniswapTimePeriod = globalUniswapTimePeriod; - - require(_globalTornUniswapPoolFee != 0, "UniswapV3FeeOracle: torn pool fee not initialized"); - require(_globalUniswapTimePeriod != 0, "UniswapV3FeeOracle: time period not initialized"); - - address poolAddress = - v3Factory.getPool(address(_token), UniswapV3OracleHelper.WETH, _tokenUniswapPoolFee); - - require(poolAddress != address(0), "UniswapV3FeeOracle: pool for token and fee does not exist"); - - (,,,, uint16 observationCardinalityNext,,) = IUniswapV3PoolState(poolAddress).slot0(); - - require( - globalMinObservationCardinality <= observationCardinalityNext, - "UniswapV3FeeOracle: pool observation cardinality low" - ); - - uniswapFeeData[_token] = UniswapV3FeeData({ - tornPoolFee: _globalTornUniswapPoolFee, - tokenPoolFee: _tokenUniswapPoolFee, - timePeriod: _globalUniswapTimePeriod - }); - - emit UniswapPoolFeeUpdated(_token, _tokenUniswapPoolFee); - } + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ function getFee( IERC20 _torn, @@ -136,30 +82,102 @@ contract UniswapV3FeeOracle is IFeeOracle { uint64 _feePercent, uint64 _feePercentDivisor ) public view virtual override returns (uint160) { - // For safety, we won't allow calculating fees for disabled instances - require(_data.isEnabled, "UniswapV3FeeOracle: instance not enabled"); - // If fee is 0 return if (_feePercent == 0) return 0; // If it's not an ERC20 it has to be ETH, use the WETH token _data.token = _data.isERC20 ? _data.token : IERC20(UniswapV3OracleHelper.WETH); - // After we have decided our token get fee data for it - UniswapV3FeeData memory uniFeeData = uniswapFeeData[_data.token]; + // Get global config + GlobalOracleConfig memory global = globals; - // Calc price ratio + // Get pool fee for the token and calc the price ratio uint256 tokenPriceRatio = UniswapV3OracleHelper.getPriceRatioOfTokens( [address(_torn), address(_data.token)], - [uniFeeData.tornPoolFee, uniFeeData.tokenPoolFee], - uniFeeData.timePeriod + [global.tornPoolFee, poolFeesByToken[_data.token]], + global.twapIntervalSeconds ); // And now all according to legacy calculation return uint160( _instance.denomination().mul(UniswapV3OracleHelper.RATIO_DIVIDER).div(tokenPriceRatio).mul( uint256(_feePercent) - ).div(_feePercentDivisor) + ).div(uint256(_feePercentDivisor)) ); } + + /** + * @notice This function allows an external contract to rely on the selection mechanism of Uniswap pools + * based on this contracts minimum observation cardinality requirement. + */ + function getPriceRatioOfTokens(IERC20 target, IERC20 quote) public view virtual returns (uint256) { + return UniswapV3OracleHelper.getPriceRatioOfTokens( + [address(target), address(quote)], + [poolFeesByToken[target], poolFeesByToken[quote]], + globals.twapIntervalSeconds + ); + } + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + function setPoolFeeForToken(IERC20 _token, uint24 _tokenPoolFee) public virtual onlyGovernance { + // Get global config + + GlobalOracleConfig memory global = globals; + + // Check whether all globals needed are initialized + + require(global.tornPoolFee != 0, "UniswapV3FeeOracle: torn pool fee not initialized"); + require(global.twapIntervalSeconds != 0, "UniswapV3FeeOracle: time period not initialized"); + + // Check whether a pool exists for the token + fee combination + + address poolAddress = UniswapV3OracleHelper.UniswapV3Factory.getPool( + address(_token), UniswapV3OracleHelper.WETH, _tokenPoolFee + ); + + require(poolAddress != address(0), "UniswapV3FeeOracle: pool for token and fee does not exist"); + + // Check whether the pool has a large enough observation cardinality + + (,,,, uint16 observationCardinalityNext,,) = IUniswapV3PoolState(poolAddress).slot0(); + + require( + global.minObservationCardinality <= observationCardinalityNext, + "UniswapV3FeeOracle: pool observation cardinality low" + ); + + // Store & log + + poolFeesByToken[_token] = _tokenPoolFee; + + emit PoolFeeUpdated(_token, _tokenPoolFee); + } + + function setGlobalTornPoolFee(uint24 _newGlobalTornPoolFee) public virtual onlyGovernance { + globals.tornPoolFee = _newGlobalTornPoolFee; + + // For `getPriceRatioOfTokens` + poolFeesByToken[IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C)] = _newGlobalTornPoolFee; + + emit GlobalTornPoolFeeUpdated(_newGlobalTornPoolFee); + } + + function setGlobalTwapIntervalSeconds(uint32 _newGlobalTwapIntervalSeconds) + public + virtual + onlyGovernance + { + globals.twapIntervalSeconds = _newGlobalTwapIntervalSeconds; + emit GlobalTwapIntervalSecondsUpdated(_newGlobalTwapIntervalSeconds); + } + + function setGlobalMinObservationCardinality(uint16 _newGlobalMinObservationCardinality) + public + virtual + onlyGovernance + { + globals.minObservationCardinality = _newGlobalMinObservationCardinality; + emit GlobalMinObservationCardinalityUpdated(_newGlobalMinObservationCardinality); + } }