diff --git a/src/v2/FeeOracleManager.sol b/src/v2/FeeOracleManager.sol index 779eb4b..f49375d 100644 --- a/src/v2/FeeOracleManager.sol +++ b/src/v2/FeeOracleManager.sol @@ -14,7 +14,7 @@ import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ // Local Imports -import { InstanceRegistry } from "../v1/tornado-proxy/InstanceRegistry.sol"; +import { InstanceRegistry } from "./InstanceRegistry.sol"; import { IFeeOracle } from "./interfaces/IFeeOracle.sol"; @@ -31,13 +31,20 @@ struct FeeData { * FeeOracleManager (formerly FeeManager). */ contract FeeManagerLegacyStorage { - uint24 public deprecatedUniswapTornPoolSwappingFee; - uint32 public deprecatedUniswapTimePeriod; + /* @dev From first contract */ + uint24 private _deprecatedUniswapTornPoolSwappingFee; + /* @dev From first contract */ + uint32 private _deprecatedUniswapTimePeriod; + + /* @dev From first contract, the only value we keep alive */ uint24 public feeUpdateInterval; - mapping(ITornadoInstance => uint160) public oldFeesForInstance; - mapping(ITornadoInstance => uint256) public oldFeesForInstanceUpdateTime; + /* @dev From first contract, only used for initialization to preserve old values in the moment */ + mapping(ITornadoInstance => uint160) internal _oldFeesForInstance; + + /* @dev From first contract, only used for initialization to preserve old values in the moment */ + mapping(ITornadoInstance => uint256) internal _oldFeesForInstanceUpdateTime; } /** @@ -50,14 +57,14 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* @dev Divide protocol fee by this to get the percent value */ - uint64 public constant PROTOCOL_FEE_DIVISOR = 1 ether; - - /* @dev The address of the TORN token */ - address public immutable tornTokenAddress; + uint64 public constant FEE_PERCENT_DIVISOR = 1 ether; /* @dev The Governance Proxy address */ address public immutable governanceProxyAddress; + /* @dev The TORN token */ + IERC20 public immutable torn; + /* @dev The InstanceRegistry contract */ InstanceRegistry public instanceRegistry; @@ -69,14 +76,16 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + event FeeUpdateIntervalUpdated(uint24 newLimit); event FeeUpdated(address indexed instance, uint256 newFee); - event NewOracleSet(address indexed instance, address oracle); - event NewFeeUpdateIntervalSet(uint24 limit); + event OracleUpdated(address indexed instance, address oracle); + event InstanceFeePercentUpdated(address indexed instance, uint64 newFeePercent); + event InstanceRegistryUpdated(address newAddress); /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ constructor(address _tornTokenAddress, address _governanceProxyAddress) public { - tornTokenAddress = _tornTokenAddress; + torn = IERC20(_tornTokenAddress); governanceProxyAddress = _governanceProxyAddress; } @@ -90,43 +99,46 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { * making sure to not reference old data anywhere. */ function initialize( - address _uniswapPoolFeeOracle, + address _uniswapV3FeeOracle, address _instanceRegistryAddress, address[] calldata _instanceAddresses, uint256[] calldata _feePercents ) external onlyGovernance initializer { + // Get num of existing instances uint256 numInstances = _instanceAddresses.length; for (uint256 i = 0; i < numInstances; i++) { + // For each instance ITornadoInstance instance = ITornadoInstance(_instanceAddresses[i]); + // Store it's old data and the percent fees which will Governance will command feeDataForInstance[instance] = FeeData({ - feeAmount: oldFeesForInstance[instance], + feeAmount: _oldFeesForInstance[instance], feePercent: uint64(_feePercents[i]), - lastUpdated: uint32(oldFeesForInstanceUpdateTime[instance]) + lastUpdated: uint32(_oldFeesForInstanceUpdateTime[instance]) }); - instanceFeeOracles[instance] = IFeeOracle(_uniswapPoolFeeOracle); + // All old pools use the uniswap fee oracle + instanceFeeOracles[instance] = IFeeOracle(_uniswapV3FeeOracle); } + // Finally also store the instance registry instanceRegistry = InstanceRegistry(_instanceRegistryAddress); } function instanceFeeWithUpdate(ITornadoInstance _instance) public virtual returns (uint160) { - FeeData memory feeData = feeDataForInstance[_instance]; - - if (feeUpdateInterval < now - feeData.lastUpdated) { - feeData.feeAmount = updateFee(_instance); - } - - return feeData.feeAmount; + return updateFee(_instance, true); } - function updateAllFees() public virtual returns (uint160[] memory newFees) { - return updateFees(instanceRegistry.getAllInstanceAddresses()); + function updateAllFees(bool _respectFeeUpdateInterval) + public + virtual + returns (uint160[] memory newFees) + { + return updateFees(instanceRegistry.getAllInstances(), _respectFeeUpdateInterval); } - function updateFees(ITornadoInstance[] memory _instances) + function updateFees(ITornadoInstance[] memory _instances, bool _respectFeeUpdateInterval) public virtual returns (uint160[] memory newFees) @@ -136,49 +148,74 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { newFees = new uint160[](numInstances); for (uint256 i = 0; i < numInstances; i++) { - newFees[i] = updateFee(_instances[i]); + newFees[i] = updateFee(_instances[i], _respectFeeUpdateInterval); } } - function updateFee(ITornadoInstance _instance) public virtual returns (uint160 newFee) { - // This will revert if no contract is set - newFee = instanceFeeOracles[_instance].getFee(); + function updateFee(ITornadoInstance _instance, bool _respectFeeUpdateInterval) + public + virtual + returns (uint160) + { + // First get fee data + FeeData memory feeData = feeDataForInstance[_instance]; - feeDataForInstance[_instance] = FeeData({ - feeAmount: newFee, - feePercent: feeDataForInstance[_instance].feePercent, - lastUpdated: uint32(now) - }); + // Now update if we do not respect the interval or we respect it and are in the interval + if (!_respectFeeUpdateInterval || feeUpdateInterval < -feeData.lastUpdated + now) { + // This will revert if no contract is set + feeData.feeAmount = instanceFeeOracles[_instance].getFee( + torn, + _instance, + instanceRegistry.getInstanceData(_instance), + feeData.feePercent, + FEE_PERCENT_DIVISOR + ); - emit FeeUpdated(address(_instance), newFee); + // Store + feeDataForInstance[_instance] = FeeData({ + feeAmount: feeData.feeAmount, + feePercent: feeData.feePercent, + lastUpdated: uint32(now) + }); - return newFee; + // Log + emit FeeUpdated(address(_instance), feeData.feeAmount); + } + + // In any case return a value + return feeData.feeAmount; } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - function setInstanceRegistry(address _newInstanceRegistryAddress) external onlyGovernance { - instanceRegistry = InstanceRegistry(_newInstanceRegistryAddress); + function setInstanceRegistry(address _newInstanceRegistryProxyAddress) external onlyGovernance { + instanceRegistry = InstanceRegistry(_newInstanceRegistryProxyAddress); + emit InstanceRegistryUpdated(_newInstanceRegistryProxyAddress); } function setFeeOracle(address _instanceAddress, address _oracleAddress) external onlyGovernance { ITornadoInstance instance = ITornadoInstance(_instanceAddress); IFeeOracle oracle = IFeeOracle(_oracleAddress); + FeeData memory feeData = feeDataForInstance[instance]; // Reverts if no oracle or does not conform to interface - uint160 fee = oracle.getFee(); + uint160 fee = oracle.getFee( + torn, + instance, + instanceRegistry.getInstanceData(instance), + feeData.feePercent, + FEE_PERCENT_DIVISOR + ); // Ok, set the oracle instanceFeeOracles[instance] = oracle; // Note down updated fee - feeDataForInstance[instance] = FeeData({ - feeAmount: fee, - feePercent: feeDataForInstance[instance].feePercent, - lastUpdated: uint32(now) - }); + feeDataForInstance[instance] = + FeeData({ feeAmount: fee, feePercent: feeData.feePercent, lastUpdated: uint32(now) }); - emit NewOracleSet(_instanceAddress, _oracleAddress); + // Logs + emit OracleUpdated(_instanceAddress, _oracleAddress); emit FeeUpdated(_instanceAddress, fee); } @@ -187,11 +224,12 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { onlyGovernance { feeDataForInstance[_instance].feePercent = _newFeePercent; + emit InstanceFeePercentUpdated(address(_instance), _newFeePercent); } function setFeeUpdateInterval(uint24 newLimit) external onlyGovernance { feeUpdateInterval = newLimit; - emit NewFeeUpdateIntervalSet(newLimit); + emit FeeUpdateIntervalUpdated(newLimit); } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -201,7 +239,13 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { } function getUpdatedFeeForInstance(ITornadoInstance instance) public view virtual returns (uint160) { - return instanceFeeOracles[instance].getFee(); + return instanceFeeOracles[instance].getFee( + torn, + instance, + instanceRegistry.getInstanceData(instance), + feeDataForInstance[instance].feePercent, + FEE_PERCENT_DIVISOR + ); } function getLastFeeForInstance(ITornadoInstance instance) public view virtual returns (uint160) { @@ -212,17 +256,25 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { return feeDataForInstance[instance].lastUpdated; } - function getFeeDeviations() public view virtual returns (int256[] memory results) { - ITornadoInstance[] memory instances = instanceRegistry.getAllInstanceAddresses(); + function getFeeDeviations() public view virtual returns (int256[] memory deviations) { + ITornadoInstance[] memory instances = instanceRegistry.getAllInstances(); uint256 numInstances = instances.length; - results = new int256[](numInstances); + deviations = new int256[](numInstances); for (uint256 i = 0; i < numInstances; i++) { ITornadoInstance instance = instances[i]; - uint256 marketFee = instanceFeeOracles[instance].getFee(); + FeeData memory feeData = feeDataForInstance[instance]; + + uint256 marketFee = instanceFeeOracles[instance].getFee( + torn, + instance, + instanceRegistry.getInstanceData(instance), + feeData.feePercent, + FEE_PERCENT_DIVISOR + ); int256 deviation; @@ -230,7 +282,7 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { deviation = int256((feeDataForInstance[instance].feeAmount * 1000) / marketFee) - 1000; } - results[i] = deviation; + deviations[i] = deviation; } } } diff --git a/src/v2/InstanceRegistry.sol b/src/v2/InstanceRegistry.sol index 4ff81a3..6d65031 100644 --- a/src/v2/InstanceRegistry.sol +++ b/src/v2/InstanceRegistry.sol @@ -16,6 +16,7 @@ import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ // Local imports +import { NameEncoder } from "./libraries/NameEncoder.sol"; import { TornadoRouter } from "./TornadoRouter.sol"; /** @@ -23,17 +24,18 @@ import { TornadoRouter } from "./TornadoRouter.sol"; * @author AlienTornadosaurusHex * @notice A contract which enumerates Tornado Cash pool instances, and also stores essential data regarding * them. - * @dev This is contract will help us layout storage properly for a proxy upgrade for the impl + * @dev This contract will help us layout storage properly for a proxy upgrade for the impl * InstanceRegistry. */ contract InstanceRegistryLegacyStorage { - /* From first contract */ + /* From first contract, just so right uint type is chosen */ enum LegacyStatePlaceholder { DISABLED, ENABLED } - /* From first contract */ + /* From first contract, just if necessary to be able to properly unpack and determine value storage + addresses and offsets */ struct LegacyInstanceStructPlaceholder { bool deprecatedIsERC20; address deprecatedToken; @@ -42,22 +44,25 @@ contract InstanceRegistryLegacyStorage { uint32 deprecatedProtocolFeePercentage; } - /* From Initializable.sol of first contract */ + /* @dev From Initializable.sol of first contract */ bool private _deprecatedInitialized; - /* From Initializable.sol of first contract */ + /* @dev From Initializable.sol of first contract */ bool private _deprecatedInitializing; - /* From first contract */ + /* @dev From first contract */ address private _deprecatedRouterAddress; - /* From first contract */ + /* @dev From first contract */ mapping(address => LegacyInstanceStructPlaceholder) private _deprecatedInstances; - /* From first contract */ + /* @dev From first contract */ ITornadoInstance[] private _deprecatedInstanceIds; } +/** + * @dev This struct holds barebones information regarding an instance and its data location in storage. + */ struct InstanceData { IERC20 token; uint80 index; @@ -91,7 +96,9 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - event NewRouterRegistered(address newRouterAddress); + event RouterRegistered(address newRouterAddress); + event InstanceAdded(address indexed instance, uint80 dataIndex, bool isERC20); + event InstanceRemoved(address indexed instance); /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -120,9 +127,12 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali function addInstance(ITornadoInstance _instance) public virtual onlyGovernance { bool isEnabled = instanceData[_instance].isEnabled; + + require(!isEnabled, "InstanceRegistry: can't add the same instance."); + bool isERC20 = false; - IERC20 token; + IERC20 token = IERC20(address(0)); // ETH instances do not know of a `token()` call try _instance.token() returns (address _tokenAddress) { @@ -132,8 +142,8 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali /* It's an ETH instance, do nothing */ } - require(!isEnabled, "InstanceRegistry: can't add the same instance."); - + // If it is ERC20 then make the router give an approval for the Tornado instance to allow the token + // amount... if it hasn't already done so if (isERC20) { uint256 routerAllowanceForInstance = token.allowance(address(router), address(_instance)); @@ -142,20 +152,33 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali } } + // Add it to the enumerable instances.push(_instance); - instanceData[_instance] = InstanceData({ - token: token, - index: uint64(instances.length - 1), - isERC20: token != IERC20(address(0)), // TODO: Is this true? - isEnabled: isEnabled - }); + uint64 instanceIndex = uint64(instances.length - 1); + + // Set data + instanceData[_instance] = + InstanceData({ token: token, index: instanceIndex, isERC20: isERC20, isEnabled: isEnabled }); + + // Log + emit InstanceAdded(address(_instance), instanceIndex, isERC20); } + /** + * @notice Remove an instance, only callable by Governance. + * @dev The modifier is in the internal call. + * @param _instanceIndex The index of the instance to remove. + */ function removeInstanceByIndex(uint256 _instanceIndex) public virtual { _removeInstanceByAddress(address(instances[_instanceIndex])); } + /** + * @notice Remove an instance, only callable by Governance. + * @dev The modifier is in the internal call. + * @param _instanceAddress The adress of the instance to remove. + */ function removeInstanceByAddress(address _instanceAddress) public virtual { _removeInstanceByAddress(_instanceAddress); } @@ -164,6 +187,8 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali ITornadoInstance instance = ITornadoInstance(_instanceAddress); InstanceData memory data = instanceData[instance]; + // Kill the allowance of the router first (arbitrary order) + if (data.isERC20) { uint256 routerAllowanceForInstance = data.token.allowance(address(router), _instanceAddress); @@ -172,17 +197,34 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali } } - instances[data.index] = instances[instances.length - 1]; + // We need to revert all changes, meaning modify position of last instance and change index in data + + uint64 lastInstanceIndex = uint64(instances.length - 1); + + ITornadoInstance lastInstance = instances[lastInstanceIndex]; + + // Remove & swap + + instances[data.index] = lastInstance; + + instanceData[lastInstance].index = data.index; + instances.pop(); + // Delete for removed instance data + delete instanceData[instance]; + + // Log + + emit InstanceRemoved(_instanceAddress); } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ function setTornadoRouter(address _newRouterAddress) external onlyGovernance { router = TornadoRouter(_newRouterAddress); - emit NewRouterRegistered(_newRouterAddress); + emit RouterRegistered(_newRouterAddress); } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -211,6 +253,15 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali } } + function getInstanceData(ITornadoInstance _instance) + public + view + virtual + returns (InstanceData memory data) + { + return instanceData[_instance]; + } + function getInstanceToken(ITornadoInstance _instance) public view virtual returns (IERC20) { return instanceData[_instance].token; } @@ -218,4 +269,37 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali function getInstanceIndex(ITornadoInstance _instance) public view virtual returns (uint80) { return instanceData[_instance].index; } + + function isRegisteredInstance(address _instanceAddress) public view virtual returns (bool) { + return isEnabledInstance(_instanceAddress); + } + + function isEnabledInstance(address _instanceAddress) public view virtual returns (bool) { + return instanceData[ITornadoInstance(_instanceAddress)].isEnabled; + } + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ENS GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + function getInstanceByENSName(string memory _instanceENSName) + public + view + virtual + returns (ITornadoInstance) + { + (, bytes32 node) = NameEncoder.dnsEncodeName(_instanceENSName); + return ITornadoInstance(resolve(node)); + } + + function isRegisteredInstanceByENSName(string memory _instanceENSName) + public + view + virtual + returns (bool) + { + return isEnabledInstanceByENSName(_instanceENSName); + } + + function isEnabledInstanceByENSName(string memory _instanceENSName) public view virtual returns (bool) { + return isEnabledInstance(address(getInstanceByENSName(_instanceENSName))); + } } diff --git a/src/v2/TornadoRouter.sol b/src/v2/TornadoRouter.sol index 65b4a91..3a4d8c0 100644 --- a/src/v2/TornadoRouter.sol +++ b/src/v2/TornadoRouter.sol @@ -19,15 +19,24 @@ import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ import { RelayerRegistry } from "../v1/RelayerRegistry.sol"; import { InstanceRegistry, InstanceData } from "./InstanceRegistry.sol"; +/** + * @title TornadoRouter + * @author AlienTornadosaurusHex + * @notice This contracts is a router for all Tornado Cash deposits and withdrawals + * @dev This is an improved version of the TornadoRouter with a modified design from the original contract. + */ contract TornadoRouter is Initializable { using SafeERC20 for IERC20; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + /* @dev The address of the Governance proxy */ address public immutable governanceProxyAddress; + /* @dev The instance registry */ InstanceRegistry public instanceRegistry; + /* @dev The relayer registry */ RelayerRegistry public relayerRegistry; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -50,7 +59,7 @@ contract TornadoRouter is Initializable { _; } - function initalize(address _instanceRegistryAddress, address _relayerRegistryAddress) + function initialize(address _instanceRegistryAddress, address _relayerRegistryAddress) external onlyGovernance initializer @@ -112,12 +121,12 @@ contract TornadoRouter is Initializable { require(_to != address(0), "TORN: can not send to zero address"); if (_token == IERC20(0)) { - // for Ether + // For Ether uint256 totalBalance = address(this).balance; uint256 balance = Math.min(totalBalance, _amount); _to.transfer(balance); } else { - // any other erc20 + // For any other ERC20 uint256 totalBalance = _token.balanceOf(address(this)); uint256 balance = Math.min(totalBalance, _amount); require(balance > 0, "TORN: trying to send 0 balance"); @@ -134,6 +143,14 @@ contract TornadoRouter is Initializable { _token.safeApprove(_spender, _amount); } + function setInstanceRegistry(address _newInstanceRegistryProxyAddress) external onlyGovernance { + instanceRegistry = InstanceRegistry(_newInstanceRegistryProxyAddress); + } + + function setRelayerRegistry(address _newRelayerRegistryProxyAddress) external onlyGovernance { + relayerRegistry = RelayerRegistry(_newRelayerRegistryProxyAddress); + } + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ function version() public pure virtual returns (string memory) { diff --git a/src/v2/UniswapV3FeeOracle.sol b/src/v2/UniswapV3FeeOracle.sol index 82c90c5..0580436 100644 --- a/src/v2/UniswapV3FeeOracle.sol +++ b/src/v2/UniswapV3FeeOracle.sol @@ -6,12 +6,160 @@ pragma experimental ABIEncoderV2; // OZ imports import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Uniswap imports + +import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import { IUniswapV3PoolState } from "@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolState.sol"; + +// Tornado imports + +import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ITornadoInstance.sol"; // Local imports -import { UniswapV3OracleHelper } from "./libraries/UniswapV3OracleHelper.sol"; import { IFeeOracle } from "./interfaces/IFeeOracle.sol"; -contract UniswapV3FeeOracle is IFeeOracle { - function getFee() public view virtual override returns (uint160) { } +import { UniswapV3OracleHelper } from "./libraries/UniswapV3OracleHelper.sol"; + +import { InstanceData } from "./InstanceRegistry.sol"; + +struct UniswapV3FeeData { + uint24 tornPoolFee; + uint24 tokenPoolFee; + uint32 timePeriod; +} + +/** + * @title UniswapV3FeeOracle + * @author AlienTornadosaurusHex + * @notice A TORN fee oracle for any Uniswap V3 token pool. + */ +contract UniswapV3FeeOracle is IFeeOracle { + using SafeMath for uint256; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + /* @dev The Governance Proxy address */ + address public immutable governanceProxyAddress; + + /* @dev The Uniswap V3 Factory */ + IUniswapV3Factory public immutable v3Factory; + + /* @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; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + event UniswapPoolFeeUpdated(IERC20 token, uint24 newUniswapPoolFee); + event GlobalTornUniswapPoolFeeUpdated(uint24 newUniswapPoolFee); + event GlobalUniswapTimePeriodUpdated(uint32 newUniswapTimePeriod); + event GlobalMinObservationCardinalityUpdated(uint16 newMinObservationCardinality); + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + constructor(address _governanceProxyAddress, address _uniswapV3FactoryAddress) public { + governanceProxyAddress = _governanceProxyAddress; + v3Factory = IUniswapV3Factory(_uniswapV3FactoryAddress); + } + + modifier onlyGovernance() { + require(msg.sender == governanceProxyAddress, "UniswapV3FeeOracle: onlyGovernance"); + _; + } + + 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); + } + + function getFee( + IERC20 _torn, + ITornadoInstance _instance, + InstanceData memory _data, + 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]; + + // Calc price ratio + uint256 tokenPriceRatio = UniswapV3OracleHelper.getPriceRatioOfTokens( + [address(_torn), address(_data.token)], + [uniFeeData.tornPoolFee, uniFeeData.tokenPoolFee], + uniFeeData.timePeriod + ); + + // And now all according to legacy calculation + return uint160( + _instance.denomination().mul(UniswapV3OracleHelper.RATIO_DIVIDER).div(tokenPriceRatio).mul( + uint256(_feePercent) + ).div(_feePercentDivisor) + ); + } } diff --git a/src/v2/interfaces/IFeeOracle.sol b/src/v2/interfaces/IFeeOracle.sol index e553f09..353758b 100644 --- a/src/v2/interfaces/IFeeOracle.sol +++ b/src/v2/interfaces/IFeeOracle.sol @@ -3,6 +3,24 @@ pragma solidity ^0.6.12; pragma experimental ABIEncoderV2; +// OZ Imports + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Tornado imports + +import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ITornadoInstance.sol"; + +// Local imports + +import { InstanceData } from "../InstanceRegistry.sol"; + interface IFeeOracle { - function getFee() external view returns (uint160); + function getFee( + IERC20 _torn, + ITornadoInstance _instance, + InstanceData memory _data, + uint64 _feePercent, + uint64 _feePercentDivisor + ) external view returns (uint160); } diff --git a/src/v2/libraries/BytesUtils.sol b/src/v2/libraries/BytesUtils.sol new file mode 100644 index 0000000..311fbd2 --- /dev/null +++ b/src/v2/libraries/BytesUtils.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +library BytesUtils { + /* + * @dev Returns the keccak-256 hash of a byte range. + * @param self The byte string to hash. + * @param offset The position to start hashing at. + * @param len The number of bytes to hash. + * @return The hash of the byte range. + */ + function keccak(bytes memory self, uint256 offset, uint256 len) internal pure returns (bytes32 ret) { + require(offset + len <= self.length); + assembly { + ret := keccak256(add(add(self, 32), offset), len) + } + } + + /** + * @dev Returns the ENS namehash of a DNS-encoded name. + * @param self The DNS-encoded name to hash. + * @param offset The offset at which to start hashing. + * @return The namehash of the name. + */ + function namehash(bytes memory self, uint256 offset) internal pure returns (bytes32) { + (bytes32 labelhash, uint256 newOffset) = readLabel(self, offset); + if (labelhash == bytes32(0)) { + require(offset == self.length - 1, "namehash: Junk at end of name"); + return bytes32(0); + } + return keccak256(abi.encodePacked(namehash(self, newOffset), labelhash)); + } + + /** + * @dev Returns the keccak-256 hash of a DNS-encoded label, and the offset to the start of the next label. + * @param self The byte string to read a label from. + * @param idx The index to read a label at. + * @return labelhash The hash of the label at the specified index, or 0 if it is the last label. + * @return newIdx The index of the start of the next label. + */ + function readLabel(bytes memory self, uint256 idx) + internal + pure + returns (bytes32 labelhash, uint256 newIdx) + { + require(idx < self.length, "readLabel: Index out of bounds"); + uint256 len = uint256(uint8(self[idx])); + if (len > 0) { + labelhash = keccak(self, idx + 1, len); + } else { + labelhash = bytes32(0); + } + newIdx = idx + len + 1; + } +} diff --git a/src/v2/libraries/NameEncoder.sol b/src/v2/libraries/NameEncoder.sol new file mode 100644 index 0000000..4b570ca --- /dev/null +++ b/src/v2/libraries/NameEncoder.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +import { BytesUtils } from "./BytesUtils.sol"; + +library NameEncoder { + using BytesUtils for bytes; + + function dnsEncodeName(string memory name) internal pure returns (bytes memory dnsName, bytes32 node) { + uint8 labelLength = 0; + bytes memory bytesName = bytes(name); + uint256 length = bytesName.length; + dnsName = new bytes(length + 2); + node = 0; + if (length == 0) { + dnsName[0] = 0; + return (dnsName, node); + } + + for (uint256 i = length - 1; i >= 0; i--) { + if (bytesName[i] == ".") { + dnsName[i + 1] = bytes1(labelLength); + node = keccak256(abi.encodePacked(node, bytesName.keccak(i + 1, labelLength))); + labelLength = 0; + } else { + labelLength += 1; + dnsName[i + 1] = bytesName[i]; + } + if (i == 0) { + break; + } + } + + node = keccak256(abi.encodePacked(node, bytesName.keccak(0, labelLength))); + + dnsName[0] = bytes1(labelLength); + return (dnsName, node); + } +}