diff --git a/.env.example b/.env.example index 11c505b..d72baec 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,7 @@ -MAINNET_RPC_URL= \ No newline at end of file +# Network & other +MAINNET_RPC_URL= +ETHERSCAN_KEY= +PRIVATE_KEY= + +# The one deployed from this repo +TORNADO_ROUTER_ADDRESS= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0aacacd..56d0450 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ node_modules/ yarn-error.log yarn.lock cache_hardhat -artifacts \ No newline at end of file +artifacts +broadcast +.yarn +yarn.lock \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 7580a3f..a0ab329 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,7 @@ # General src = 'src' out = 'out' -libs = ["node_modules", "lib"] +libs = [ "lib"] # Compiler evm_version = 'shanghai' @@ -18,21 +18,8 @@ rpc_endpoints = { mainnet = "${MAINNET_RPC_URL}" } # Tests verbosity = 2 -# Remappings -remappings = [ - 'src/=src/', - 'base/=src/base/', - 'common/=src/common/', - 'ds-test/=lib/ds-test/src/', - 'solmate/=lib/solmate/src/', - 'forge-std/=lib/forge-std/src/', - 'torn-token/=lib/torn-token/', - 'tornado-governance/=lib/tornado-governance/', - 'tornado-anonymity-mining/=lib/tornado-anonymity-mining/', - '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', - '@uniswap/v3-periphery/=lib/v3-periphery/', - '@uniswap/v3-core/=lib/v3-core/', -] +[etherscan] +mainnet = { key = "${ETHERSCAN_KEY}" } [fmt] line_length = 110 diff --git a/gas/all.md b/gas/all.md new file mode 100644 index 0000000..f063cc4 --- /dev/null +++ b/gas/all.md @@ -0,0 +1,67 @@ +| src/proposals/CRVUSDInstancesProposal.sol:CRVUSDInstancesProposal contract | | | | | | +| -------------------------------------------------------------------------- | --------------- | ------- | ------- | ------- | ------- | +| Deployment Cost | Deployment Size | | | | | +| 743500 | 4033 | | | | | +| Function Name | min | avg | median | max | # calls | +| executeProposal | 1217647 | 1217647 | 1217647 | 1217647 | 1 | + + +| src/proposals/InfrastructureUpgradeProposal.sol:InfrastructureUpgradeProposal contract | | | | | | +| -------------------------------------------------------------------------------------- | --------------- | ------- | ------- | ------- | ------- | +| Deployment Cost | Deployment Size | | | | | +| 1198424 | 6427 | | | | | +| Function Name | min | avg | median | max | # calls | +| executeProposal | 2630600 | 2630600 | 2630600 | 2630600 | 1 | + + +| src/v2/CurveFeeOracle.sol:CurveFeeOracle contract | | | | | | +| ------------------------------------------------- | --------------- | ----- | ------ | ------ | ------- | +| Deployment Cost | Deployment Size | | | | | +| 1168992 | 5938 | | | | | +| Function Name | min | avg | median | max | # calls | +| getFee | 1463 | 1463 | 1463 | 1463 | 5 | +| modifyChainedOracleForInstance | 83128 | 93498 | 83128 | 132879 | 5 | +| setUniswapV3FeeOracle | 1713 | 2713 | 2713 | 3713 | 2 | + + +| src/v2/FeeOracleManager.sol:FeeOracleManager contract | | | | | | +| ----------------------------------------------------- | --------------- | ------ | ------ | ------ | ------- | +| Deployment Cost | Deployment Size | | | | | +| 1420779 | 7415 | | | | | +| Function Name | min | avg | median | max | # calls | +| initialize | 847003 | 847003 | 847003 | 847003 | 1 | +| setFeeOracle | 54524 | 54524 | 54524 | 54524 | 5 | +| setFeePercentForInstance | 2466 | 2466 | 2466 | 2466 | 5 | + + +| src/v2/InstanceRegistry.sol:InstanceRegistry contract | | | | | | +| ----------------------------------------------------- | --------------- | ------- | ------- | ------- | ------- | +| Deployment Cost | Deployment Size | | | | | +| 1340669 | 6890 | | | | | +| Function Name | min | avg | median | max | # calls | +| addInstance | 84801 | 85801 | 84801 | 89801 | 5 | +| getInstanceData | 1469 | 1469 | 1469 | 1469 | 5 | +| initialize | 1419165 | 1419165 | 1419165 | 1419165 | 1 | + + +| src/v2/TornadoRouter.sol:TornadoRouter contract | | | | | | +| ----------------------------------------------- | --------------- | ----- | ------ | ----- | ------- | +| Deployment Cost | Deployment Size | | | | | +| 1079192 | 5584 | | | | | +| Function Name | min | avg | median | max | # calls | +| approveTokenForInstance | 27373 | 29130 | 27762 | 33647 | 18 | +| initialize | 45887 | 45887 | 45887 | 45887 | 1 | + + +| src/v2/UniswapV3FeeOracle.sol:UniswapV3FeeOracle contract | | | | | | +| --------------------------------------------------------- | --------------- | ----- | ------ | ----- | ------- | +| Deployment Cost | Deployment Size | | | | | +| 1357082 | 6972 | | | | | +| Function Name | min | avg | median | max | # calls | +| setGlobalMinObservationCardinality | 1773 | 1773 | 1773 | 1773 | 2 | +| setGlobalTornPoolFee | 45872 | 45872 | 45872 | 45872 | 1 | +| setGlobalTwapIntervalSeconds | 1666 | 1666 | 1666 | 1666 | 1 | +| setPoolFeeForToken | 2688 | 12087 | 5895 | 36795 | 17 | + + + diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..ab4e4b8 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,14 @@ +src/=src/ +base/=src/base/ +common/=src/common/ +ds-test/=lib/ds-test/src/ +solmate/=lib/solmate/src/ +forge-std/=lib/forge-std/src/ +torn-token/=lib/torn-token/ +tornado-governance/=lib/tornado-governance/ +tornado-anonymity-mining/=lib/tornado-anonymity-mining/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@uniswap/v2-core/=node_modules/@uniswap/v2-core/contracts/ +@uniswap/v2-periphery/=node_modules/@uniswap/v2-periphery/contracts/ +@uniswap/v3-periphery/=lib/v3-periphery/ +@uniswap/v3-core/=lib/v3-core/ \ No newline at end of file diff --git a/script/Swap.sol.x b/script/Swap.sol.x new file mode 100755 index 0000000..59ffbf2 --- /dev/null +++ b/script/Swap.sol.x @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +// STD imports + +import { Script } from "forge-std/Script.sol"; + +import { console2 } from "forge-std/console2.sol"; + +// Local imports + +import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ITornadoInstance.sol"; + +import { FeeManager } from "../src/v1/tornado-proxy/FeeManager.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// SPD + +/// @title Callback for IUniswapV3PoolActions#swap +/// @notice Any contract that calls IUniswapV3PoolActions#swap must implement this interface +interface IUniswapV3SwapCallback { + /// @notice Called to `msg.sender` after executing a swap via IUniswapV3Pool#swap. + /// @dev In the implementation you must pay the pool tokens owed for the swap. + /// The caller of this method must be checked to be a UniswapV3Pool deployed by the canonical + /// UniswapV3Factory. + /// amount0Delta and amount1Delta can both be 0 if no tokens were swapped. + /// @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by + /// the pool by + /// the end of the swap. If positive, the callback must send that amount of token0 to the pool. + /// @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by + /// the pool by + /// the end of the swap. If positive, the callback must send that amount of token1 to the pool. + /// @param data Any data passed through by the caller via the IUniswapV3PoolActions#swap call + function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external; +} + +/// @title Router token swapping functionality +/// @notice Functions for swapping tokens via Uniswap V3 +interface ISwapRouter is IUniswapV3SwapCallback { + struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another token + /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata + /// @return amountOut The amount of the received token + function exactInputSingle(ExactInputSingleParams calldata params) + external + payable + returns (uint256 amountOut); + + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in + /// calldata + /// @return amountOut The amount of the received token + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); + + struct ExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + uint160 sqrtPriceLimitX96; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another token + /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata + /// @return amountIn The amount of the input token + function exactOutputSingle(ExactOutputSingleParams calldata params) + external + payable + returns (uint256 amountIn); + + struct ExactOutputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountOut; + uint256 amountInMaximum; + } + + /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path + /// (reversed) + /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in + /// calldata + /// @return amountIn The amount of the input token + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); +} + +contract DeployScript is Script { + IERC20 public constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + IERC20 public constant TORN = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); + + function run() external { + uint256 key = vm.envUint("PRIVATE_KEY"); + + ISwapRouter router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + + uint256 bal = TORN.balanceOf(0x895CB4C75e9be8ABA117bF4E044416C855018ea0); + + vm.startBroadcast(key); + + TORN.approve(address(router), bal); + + ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ + path: abi.encodePacked(TORN, uint24(10_000), WETH), + recipient: 0x895CB4C75e9be8ABA117bF4E044416C855018ea0, + deadline: block.timestamp + 6 minutes, + amountIn: bal, + amountOutMinimum: 0 + }); + + router.exactInput(params); + + vm.stopBroadcast(); + } +} diff --git a/script/TornadoRouter.s.sol.x b/script/TornadoRouter.s.sol.x new file mode 100755 index 0000000..c2677d6 --- /dev/null +++ b/script/TornadoRouter.s.sol.x @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +// STD imports + +import { Script } from "forge-std/Script.sol"; + +import { console2 } from "forge-std/console2.sol"; + +// Local imports + +import { TornadoAddresses } from "../src/common/TornadoAddresses.sol"; + +import { TornadoRouter } from "../src/v2/TornadoRouter.sol"; + +contract DeployScript is TornadoAddresses, Script { + function run() external { + uint256 key = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(key); + + TornadoRouter router = new TornadoRouter(getGovernanceProxyAddress()); + + vm.stopBroadcast(); + } +} diff --git a/script/Update.sol.x b/script/Update.sol.x new file mode 100755 index 0000000..e771ca6 --- /dev/null +++ b/script/Update.sol.x @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.12; +pragma experimental ABIEncoderV2; + +// STD imports + +import { Script } from "forge-std/Script.sol"; + +import { console2 } from "forge-std/console2.sol"; + +// Local imports + +import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ITornadoInstance.sol"; + +import { FeeManager } from "../src/v1/tornado-proxy/FeeManager.sol"; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract DeployScript is Script { + IERC20 public constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + IERC20 public constant TORN = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); + + function run() external { + uint256 key = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(key); + + FeeManager fees = FeeManager(0x5f6c97C6AD7bdd0AE7E0Dd4ca33A4ED3fDabD4D7); + + fees.updateFee(ITornadoInstance(0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF)); + fees.updateFee(ITornadoInstance(0xA160cdAB225685dA1d56aa342Ad8841c3b53f291)); + fees.updateFee(ITornadoInstance(0x07687e702b410Fa43f4cB4Af7FA097918ffD2730)); + fees.updateFee(ITornadoInstance(0x23773E65ed146A459791799d01336DB287f25334)); + + vm.stopBroadcast(); + } +} diff --git a/src/proposals/CRVUSDInstancesProposal.sol b/src/proposals/CRVUSDInstancesProposal.sol index e7255bd..883140f 100644 --- a/src/proposals/CRVUSDInstancesProposal.sol +++ b/src/proposals/CRVUSDInstancesProposal.sol @@ -19,7 +19,7 @@ import { InstanceRegistry } from "../v2/InstanceRegistry.sol"; import { CurveFeeOracle, ICurvePriceOracle, CurveChainedOracles } from "../v2/CurveFeeOracle.sol"; -import { UniswapV3FeeOracle } from "../v2/UniswapV3FeeOracle.sol"; +import { UniswapFeeOracle } from "../v2/UniswapFeeOracle.sol"; import { TornadoRouter } from "../v2/TornadoRouter.sol"; @@ -47,7 +47,7 @@ contract CRVUSDInstancesProposal { /* @dev This is the Uniswap V3 Oracle which we will use for all of our traditional instances, but it will also help the Curve instances, the former must have been deployed with the address of this */ - UniswapV3FeeOracle public immutable v3FeeOracle; + UniswapFeeOracle public immutable uniswapFeeOracle; /* @dev This is the CurveFeeOracle contract which will be deployed beforehand and which will be able to use multiple Curve pools as oracles, at once. */ @@ -55,9 +55,9 @@ contract CRVUSDInstancesProposal { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - constructor(address _deployedCurveFeeOracleAddress, address _deployedUniswapV3FeeOracleAddress) public { + constructor(address _deployedCurveFeeOracleAddress, address _deployedUniswapFeeOracleAddress) public { curveFeeOracle = CurveFeeOracle(_deployedCurveFeeOracleAddress); - v3FeeOracle = UniswapV3FeeOracle(_deployedUniswapV3FeeOracleAddress); + uniswapFeeOracle = UniswapFeeOracle(_deployedUniswapFeeOracleAddress); } /** @@ -74,7 +74,7 @@ contract CRVUSDInstancesProposal { // Ok, first add the Uniswap V3 Oracle to the contract - curveFeeOracle.setUniswapV3FeeOracle(v3FeeOracle); + curveFeeOracle.setUniswapFeeOracle(uniswapFeeOracle); // Then, add necessary oracles for the CRVUSD price, to the CurveFeeOracle diff --git a/src/proposals/InfrastructureUpgradeProposal.sol b/src/proposals/InfrastructureUpgradeProposal.sol index b6c2e95..4e825a6 100644 --- a/src/proposals/InfrastructureUpgradeProposal.sol +++ b/src/proposals/InfrastructureUpgradeProposal.sol @@ -18,7 +18,7 @@ import { FeeOracleManager } from "../v2/FeeOracleManager.sol"; import { InstanceRegistry } from "../v2/InstanceRegistry.sol"; -import { UniswapV3FeeOracle, UniswapV3OracleHelper } from "../v2/UniswapV3FeeOracle.sol"; +import { UniswapFeeOracle, UniswapV3OracleHelper } from "../v2/UniswapFeeOracle.sol"; import { TornadoRouter } from "../v2/TornadoRouter.sol"; @@ -42,7 +42,7 @@ contract InfrastructureUpgradeProposal { /* @dev This is the Uniswap V3 Oracle which we will use for all of our traditional instances, but it will also help the CurveFeeOracle, the former must have been deployed witht the address of this */ - address public immutable deployedUniswapV3FeeOracleAddress; + address public immutable deployedUniswapFeeOracleAddress; /* @dev The implementation address of the FeeManager upgrade contract */ address public immutable deployedFeeOracleManagerImplementationAddress; @@ -56,12 +56,12 @@ contract InfrastructureUpgradeProposal { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ constructor( - address _deployedUniswapV3FeeOracleAddress, + address _deployedUniswapFeeOracleAddress, address _deployedFeeOracleManagerImplementationAddress, address _deployedInstanceRegistryImplementationAddress, address _deployedTornadoRouterAddress ) public { - deployedUniswapV3FeeOracleAddress = _deployedUniswapV3FeeOracleAddress; + deployedUniswapFeeOracleAddress = _deployedUniswapFeeOracleAddress; deployedFeeOracleManagerImplementationAddress = _deployedFeeOracleManagerImplementationAddress; deployedInstanceRegistryImplementationAddress = _deployedInstanceRegistryImplementationAddress; deployedTornadoRouterAddress = _deployedTornadoRouterAddress; @@ -91,7 +91,7 @@ contract InfrastructureUpgradeProposal { // out from the on-chain instance registry. FeeOracleManager(feeManagerProxyAddress).initialize( - deployedUniswapV3FeeOracleAddress, + deployedUniswapFeeOracleAddress, instanceRegistryProxyAddress, _getAllInstances(), _getAllInstanceFeePercents() @@ -117,13 +117,11 @@ contract InfrastructureUpgradeProposal { // legacy cDAI pools which are in a kind of stupid liquidity condition, in any case there is a small // amount of funds in there so let's add them too - UniswapV3FeeOracle v3FeeOracle = UniswapV3FeeOracle(deployedUniswapV3FeeOracleAddress); + UniswapFeeOracle uniswapFeeOracle = UniswapFeeOracle(deployedUniswapFeeOracleAddress); - v3FeeOracle.setGlobalTornPoolFee(10_000, true); // Legacy value, still makes sense + uniswapFeeOracle.setTwapIntervalSeconds(5400); // Legacy value, still makes sense - v3FeeOracle.setGlobalTwapIntervalSeconds(5400); // Legacy value, still makes sense - - v3FeeOracle.setGlobalMinObservationCardinality(1); // Set it to minimum so cDAI passes + uniswapFeeOracle.setMinObservationCardinality(1); // Set it to minimum so cDAI passes // Each of the instances are going to require a Uniswap Pool Fee to be set such that we actually know // what pools to lookup when querying for Oracle data. This data has also been read out from the @@ -131,12 +129,12 @@ contract InfrastructureUpgradeProposal { // respectively "default" pool fees. This is not to be confused with pool protocol fees, these have // been recorded in the (first) legacy initialization above - _setAllInstancePoolFeesInOracle(v3FeeOracle); + _setAllInstancePoolFeesInOracle(uniswapFeeOracle); - v3FeeOracle.setGlobalMinObservationCardinality(10); // Now set the cardinality to a reasonable value + uniswapFeeOracle.setMinObservationCardinality(10); // Now set the cardinality to a reasonable value } - function _setAllInstancePoolFeesInOracle(UniswapV3FeeOracle _v3FeeOracle) internal { + function _setAllInstancePoolFeesInOracle(UniswapFeeOracle _uniswapFeeOracle) internal { ITornadoInstance[] memory instances = _getAllInstances(); IERC20 weth = IERC20(UniswapV3OracleHelper.WETH); @@ -146,31 +144,31 @@ contract InfrastructureUpgradeProposal { IERC20 wbtc = IERC20(instances[14].token()); /* ETH instances */ - _v3FeeOracle.setPoolFeeForToken(weth, uint24(0x000)); - _v3FeeOracle.setPoolFeeForToken(weth, uint24(0x000)); - _v3FeeOracle.setPoolFeeForToken(weth, uint24(0x000)); - _v3FeeOracle.setPoolFeeForToken(weth, uint24(0x000)); + _uniswapFeeOracle.setPoolFeeForToken(weth, uint24(0x000)); + _uniswapFeeOracle.setPoolFeeForToken(weth, uint24(0x000)); + _uniswapFeeOracle.setPoolFeeForToken(weth, uint24(0x000)); + _uniswapFeeOracle.setPoolFeeForToken(weth, uint24(0x000)); /* DAI instances */ - _v3FeeOracle.setPoolFeeForToken(dai, uint24(0xbb8)); - _v3FeeOracle.setPoolFeeForToken(dai, uint24(0xbb8)); - _v3FeeOracle.setPoolFeeForToken(dai, uint24(0xbb8)); - _v3FeeOracle.setPoolFeeForToken(dai, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(dai, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(dai, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(dai, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(dai, uint24(0xbb8)); /* cDAI instances */ - _v3FeeOracle.setPoolFeeForToken(cdai, uint24(0xbb8)); - _v3FeeOracle.setPoolFeeForToken(cdai, uint24(0xbb8)); - _v3FeeOracle.setPoolFeeForToken(cdai, uint24(0xbb8)); - _v3FeeOracle.setPoolFeeForToken(cdai, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(cdai, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(cdai, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(cdai, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(cdai, uint24(0xbb8)); /* USDT instances */ - _v3FeeOracle.setPoolFeeForToken(usdt, uint24(0x1f4)); - _v3FeeOracle.setPoolFeeForToken(usdt, uint24(0x1f4)); + _uniswapFeeOracle.setPoolFeeForToken(usdt, uint24(0x1f4)); + _uniswapFeeOracle.setPoolFeeForToken(usdt, uint24(0x1f4)); /* WBTC instances */ - _v3FeeOracle.setPoolFeeForToken(wbtc, uint24(0xbb8)); - _v3FeeOracle.setPoolFeeForToken(wbtc, uint24(0xbb8)); - _v3FeeOracle.setPoolFeeForToken(wbtc, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(wbtc, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(wbtc, uint24(0xbb8)); + _uniswapFeeOracle.setPoolFeeForToken(wbtc, uint24(0xbb8)); } function _getAllInstances() internal pure returns (ITornadoInstance[] memory _addresses) { diff --git a/src/v2/CurveFeeOracle.sol b/src/v2/CurveFeeOracle.sol index 9eff5d2..de89494 100644 --- a/src/v2/CurveFeeOracle.sol +++ b/src/v2/CurveFeeOracle.sol @@ -18,7 +18,7 @@ import { IFeeOracle } from "./interfaces/IFeeOracle.sol"; import { UniswapV3OracleHelper } from "./libraries/UniswapV3OracleHelper.sol"; -import { UniswapV3FeeOracle } from "./UniswapV3FeeOracle.sol"; +import { UniswapFeeOracle } from "./UniswapFeeOracle.sol"; import { InstanceData } from "./InstanceRegistry.sol"; @@ -170,28 +170,38 @@ contract CurveFeeOracle is IFeeOracle { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - /* @dev The Governance Proxy address */ + /** + * @notice 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 */ + /** + * @notice 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) */ + /** + * @notice 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; + /** + * @notice Our Uniswap V3 Fee Oracle, we will need it some time for TORN/ETH + */ + UniswapFeeOracle public uniswapFeeOracle; - /* @dev We will not need the Uniswap V3 Oracle forever though, TORN/ETH pools are possible on Curve too */ + /** + * @notice 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 UniswapFeeOracleUpdated(address _newUniswapFeeOracleAddress); event ChainedOracleUpdated(address indexed instance, bytes32 newOracleHash, string newName); /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -202,18 +212,20 @@ contract CurveFeeOracle is IFeeOracle { } modifier onlyGovernance() { - require(msg.sender == governanceProxyAddress, "UniswapV3FeeOracle: onlyGovernance"); + require(msg.sender == governanceProxyAddress, "UniswapFeeOracle: onlyGovernance"); _; } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + function update() public virtual override { } + function getFee( - IERC20 _torn, + IERC20, ITornadoInstance _instance, InstanceData memory, - uint64 _feePercent, - uint64 _feePercentDivisor + uint32 _feePercent, + uint32 _feePercentDivisor ) public view virtual override returns (uint160) { // If fee is 0 return if (_feePercent == 0) return 0; @@ -226,7 +238,7 @@ contract CurveFeeOracle is IFeeOracle { // in TORN if (tornOracleIsUniswapV3) { price = price * UniswapV3OracleHelper.RATIO_DIVIDER - / v3FeeOracle.getPriceRatioOfTokens(_torn, IERC20(UniswapV3OracleHelper.WETH)); + / uniswapFeeOracle.getTokenPerTORN(IERC20(UniswapV3OracleHelper.WETH)); } // The price is now either prepared by the chained oracles or by Uniswap V3, in any case we do the @@ -284,9 +296,9 @@ contract CurveFeeOracle is IFeeOracle { emit ChainedOracleUpdated(address(_instance), oracleHash, _name); } - function setUniswapV3FeeOracle(UniswapV3FeeOracle _uniswapV3FeeOracle) public virtual onlyGovernance { - v3FeeOracle = _uniswapV3FeeOracle; - emit UniswapV3FeeOracleUpdated(address(_uniswapV3FeeOracle)); + function setUniswapFeeOracle(UniswapFeeOracle _uniswapFeeOracle) public virtual onlyGovernance { + uniswapFeeOracle = _uniswapFeeOracle; + emit UniswapFeeOracleUpdated(address(_uniswapFeeOracle)); } function setTornOracleIsUniswapV3(bool _is) public virtual onlyGovernance { diff --git a/src/v2/FeeOracleManager.sol b/src/v2/FeeOracleManager.sol index 37a218b..fb440c4 100644 --- a/src/v2/FeeOracleManager.sol +++ b/src/v2/FeeOracleManager.sol @@ -25,19 +25,29 @@ import { IFeeOracle } from "./interfaces/IFeeOracle.sol"; * FeeOracleManager (formerly FeeManager). */ contract FeeManagerLegacyStorage { - /* @dev From first contract */ + /** + * @dev From first contract + */ uint24 private _deprecatedUniswapTornPoolSwappingFee; - /* @dev From first contract */ + /** + * @dev From first contract + */ uint32 private _deprecatedUniswapTimePeriod; - /* @dev From first contract, the only value we keep alive */ - uint24 public feeUpdateInterval; + /** + * @dev From first contract, the only value we keep alive + */ + uint24 internal _oldFeeUpdateInterval; - /* @dev From first contract, only used for initialization to preserve old values */ + /** + * @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 */ + /** + * @dev From first contract, only used for initialization to preserve old values + */ mapping(ITornadoInstance => uint256) internal _oldFeesForInstanceUpdateTime; } @@ -45,9 +55,10 @@ contract FeeManagerLegacyStorage { * @notice Fee data which is valid and also influences fee updating across all oracles. */ struct FeeData { - uint160 feeAmount; - uint64 feePercent; - uint32 lastUpdated; + uint160 amount; + uint32 percent; + uint32 updateInterval; + uint32 lastUpdateTime; } /** @@ -59,30 +70,42 @@ struct FeeData { contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - /* @dev Divide protocol fee by this to get the percent value */ - uint64 public constant FEE_PERCENT_DIVISOR = 1 ether; + /** + * @notice Divide protocol fee by this to get the percent value + */ + uint32 public constant FEE_PERCENT_DIVISOR = 10_000; - /* @dev The Governance Proxy address */ + /** + * @notice The Governance Proxy address + */ address public immutable governanceProxyAddress; - /* @dev The TORN token */ + /** + * @notice The TORN token + */ IERC20 public immutable torn; - /* @dev The InstanceRegistry contract */ + /** + * @notice The InstanceRegistry contract + */ InstanceRegistry public instanceRegistry; - /* @dev Each instance has a dedicated fee oracle, these only compute the values */ + /** + * @notice Each instance has a dedicated fee oracle, these only compute the values + */ mapping(ITornadoInstance => IFeeOracle) public instanceFeeOracles; - /* @dev The data for each instance will be stored in this contract */ - mapping(ITornadoInstance => FeeData) public feeDataForInstance; + /** + * @notice The data for each instance will be stored in this contract + */ + mapping(ITornadoInstance => FeeData) public feesByInstance; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ event FeeUpdateIntervalUpdated(uint24 newLimit); event FeeUpdated(address indexed instance, uint256 newFee); event OracleUpdated(address indexed instance, address oracle); - event InstanceFeePercentUpdated(address indexed instance, uint64 newFeePercent); + event InstanceFeePercentUpdated(address indexed instance, uint32 newFeePercent); event InstanceRegistryUpdated(address newAddress); /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -102,10 +125,10 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { * making sure to not reference old data anywhere. */ function initialize( - address _uniswapV3FeeOracle, + address _uniswapFeeOracle, address _instanceRegistryAddress, ITornadoInstance[] memory _instances, - uint256[] memory _feePercents + uint256[] memory _percents ) external onlyGovernance initializer { // Get num of existing instances uint256 numInstances = _instances.length; @@ -115,14 +138,15 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { ITornadoInstance instance = _instances[i]; // Store it's old data and the percent fees which Governance will command - feeDataForInstance[instance] = FeeData({ - feeAmount: _oldFeesForInstance[instance], - feePercent: uint64(_feePercents[i]), - lastUpdated: uint32(_oldFeesForInstanceUpdateTime[instance]) + feesByInstance[instance] = FeeData({ + amount: _oldFeesForInstance[instance], + percent: uint32(_percents[i]), + updateInterval: _oldFeeUpdateInterval, + lastUpdateTime: uint32(_oldFeesForInstanceUpdateTime[instance]) }); // All old pools use the uniswap fee oracle - instanceFeeOracles[instance] = IFeeOracle(_uniswapV3FeeOracle); + instanceFeeOracles[instance] = IFeeOracle(_uniswapFeeOracle); } // Finally also store the instance registry @@ -160,42 +184,45 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { virtual returns (uint160) { - // First get fee data - FeeData memory feeData = feeDataForInstance[_instance]; + // Get fee data & oracle + FeeData memory fee = feesByInstance[_instance]; + IFeeOracle oracle = instanceFeeOracles[_instance]; + + // Check whether the instance is registered + require(address(oracle) != address(0), "FeeOracleManager: instance has no oracle"); // Now update if we do not respect the interval or we respect it and are in the interval - if (!_respectFeeUpdateInterval || (feeUpdateInterval < -feeData.lastUpdated + now)) { - IFeeOracle oracle = instanceFeeOracles[_instance]; - - // Check whether the instance is registered - require(address(oracle) != address(0), "FeeOracleManager: instance has no oracle"); + if (!_respectFeeUpdateInterval || (fee.updateInterval < now - fee.lastUpdateTime)) { + // Allow oracle to gate for fee manager and implement own logic + oracle.update(); // There must a be a fee set otherwise it's just 0 - if (feeData.feePercent != 0) { - feeData.feeAmount = oracle.getFee( + if (fee.percent != 0) { + fee.amount = oracle.getFee( torn, _instance, instanceRegistry.getInstanceData(_instance), - feeData.feePercent, + fee.percent, FEE_PERCENT_DIVISOR ); } else { - feeData.feeAmount = 0; + fee.amount = 0; } // Store - feeDataForInstance[_instance] = FeeData({ - feeAmount: feeData.feeAmount, - feePercent: feeData.feePercent, - lastUpdated: uint32(now) + feesByInstance[_instance] = FeeData({ + amount: fee.amount, + percent: fee.percent, + updateInterval: fee.updateInterval, + lastUpdateTime: uint32(now) }); // Log - emit FeeUpdated(address(_instance), feeData.feeAmount); + emit FeeUpdated(address(_instance), fee.amount); } // In any case return a value - return feeData.feeAmount; + return fee.amount; } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -212,45 +239,50 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { // Nominally fee percent should be set first for an instance, but we cannot be sure // whether fee percent 0 is intentional, so we don't check - FeeData memory feeData = feeDataForInstance[instance]; - - // Fee which may be recalculated - uint160 fee; + FeeData memory fee = feesByInstance[instance]; // An address(0) oracle means we're removing an oracle for an instance if (_oracleAddress != address(0)) { - // Reverts if oracle does not conform to interface - fee = oracle.getFee( - torn, - instance, - instanceRegistry.getInstanceData(instance), - feeData.feePercent, - FEE_PERCENT_DIVISOR + // Reverts if oracle doesn't implement + oracle.update(); + + // Reverts if oracle doesn't implement + fee.amount = oracle.getFee( + torn, instance, instanceRegistry.getInstanceData(instance), fee.percent, FEE_PERCENT_DIVISOR ); // Note down updated fee - feeDataForInstance[instance] = - FeeData({ feeAmount: fee, feePercent: feeData.feePercent, lastUpdated: uint32(now) }); + feesByInstance[instance] = FeeData({ + amount: fee.amount, + percent: fee.percent, + updateInterval: fee.updateInterval, + lastUpdateTime: uint32(now) + }); + + // Log fee update + emit FeeUpdated(_instanceAddress, fee.amount); } // Ok, set the oracle instanceFeeOracles[instance] = oracle; - // Logs + // Log oracle update emit OracleUpdated(_instanceAddress, _oracleAddress); - emit FeeUpdated(_instanceAddress, fee); } - function setFeePercentForInstance(ITornadoInstance _instance, uint64 _newFeePercent) + function setFeePercentForInstance(ITornadoInstance _instance, uint32 _newFeePercent) external onlyGovernance { - feeDataForInstance[_instance].feePercent = _newFeePercent; + feesByInstance[_instance].percent = _newFeePercent; emit InstanceFeePercentUpdated(address(_instance), _newFeePercent); } - function setFeeUpdateInterval(uint24 newLimit) external onlyGovernance { - feeUpdateInterval = newLimit; + function setFeeUpdateIntervalForInstance(ITornadoInstance _instance, uint24 newLimit) + external + onlyGovernance + { + feesByInstance[_instance].updateInterval = newLimit; emit FeeUpdateIntervalUpdated(newLimit); } @@ -265,21 +297,30 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { torn, instance, instanceRegistry.getInstanceData(instance), - feeDataForInstance[instance].feePercent, + feesByInstance[instance].percent, FEE_PERCENT_DIVISOR ); } function getLastFeeForInstance(ITornadoInstance instance) public view virtual returns (uint160) { - return feeDataForInstance[instance].feeAmount; + return feesByInstance[instance].amount; } function getLastUpdatedTimeForInstance(ITornadoInstance instance) public view virtual returns (uint32) { - return feeDataForInstance[instance].lastUpdated; + return feesByInstance[instance].lastUpdateTime; } - function getFeePercentForInstance(ITornadoInstance instance) public view virtual returns (uint64) { - return feeDataForInstance[instance].feePercent; + function getFeePercentForInstance(ITornadoInstance instance) public view virtual returns (uint32) { + return feesByInstance[instance].percent; + } + + function getFeeUpdateIntervalForInstance(ITornadoInstance instance) + public + view + virtual + returns (uint32) + { + return feesByInstance[instance].updateInterval; } function getAllFeeDeviations() public view virtual returns (int256[] memory) { @@ -299,23 +340,15 @@ contract FeeOracleManager is FeeManagerLegacyStorage, Initializable { for (uint256 i = 0; i < numInstances; i++) { ITornadoInstance instance = _instances[i]; - FeeData memory feeData = feeDataForInstance[instance]; + FeeData memory fee = feesByInstance[instance]; uint256 marketFee = instanceFeeOracles[instance].getFee( - torn, - instance, - instanceRegistry.getInstanceData(instance), - feeData.feePercent, - FEE_PERCENT_DIVISOR + torn, instance, instanceRegistry.getInstanceData(instance), fee.percent, FEE_PERCENT_DIVISOR ); - int256 deviation; - if (marketFee != 0) { - deviation = int256((feeData.feeAmount * 1000) / marketFee) - 1000; + deviations[i] = int256((fee.amount * 1000) / marketFee) - 1000; } - - deviations[i] = deviation; } } } diff --git a/src/v2/InstanceRegistry.sol b/src/v2/InstanceRegistry.sol index 6c8641e..a81a59d 100644 --- a/src/v2/InstanceRegistry.sol +++ b/src/v2/InstanceRegistry.sol @@ -44,19 +44,29 @@ contract InstanceRegistryLegacyStorage { uint32 deprecatedProtocolFeePercentage; } - /* @dev From Initializable.sol of first contract */ + /** + * @dev From Initializable.sol of first contract + */ bool private _deprecatedInitialized; - /* @dev From Initializable.sol of first contract */ + /** + * @dev From Initializable.sol of first contract + */ bool private _deprecatedInitializing; - /* @dev From first contract */ + /** + * @dev From first contract + */ address private _deprecatedRouterAddress; - /* @dev From first contract */ + /** + * @dev From first contract + */ mapping(address => LegacyInstanceStructPlaceholder) private _deprecatedInstances; - /* @dev From first contract */ + /** + * @dev From first contract + */ ITornadoInstance[] private _deprecatedInstanceIds; } @@ -82,16 +92,24 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - /* @dev The address of the Governance proxy */ + /** + * @notice The address of the Governance proxy + */ address public immutable governanceProxyAddress; - /* @dev Essential data regarding instances, see struct above */ + /** + * @notice Essential data regarding instances, see struct above + */ mapping(ITornadoInstance => InstanceData) public instanceData; - /* @dev All instances enumerable */ + /** + * @notice All instances enumerable + */ ITornadoInstance[] public instances; - /* @dev The router which processes txs which we must command to approve tokens */ + /** + * @notice The router which processes txs which we must command to approve tokens + */ TornadoRouter public router; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -255,10 +273,10 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali virtual returns (ITornadoInstance[] memory allInstances) { - allInstances = new ITornadoInstance[](-_inclusiveStartIndex + 1 + _inclusiveEndIndex); + allInstances = new ITornadoInstance[](1 + _inclusiveEndIndex - _inclusiveStartIndex); for (uint256 i = _inclusiveStartIndex; i < _inclusiveEndIndex + 1; i++) { - allInstances[i] = instances[i]; + allInstances[i - _inclusiveStartIndex] = instances[i]; } } @@ -272,10 +290,10 @@ contract InstanceRegistry is InstanceRegistryLegacyStorage, EnsResolve, Initiali virtual returns (InstanceData[] memory data) { - data = new InstanceData[](-_inclusiveStartIndex + 1 + _inclusiveEndIndex); + data = new InstanceData[](1 + _inclusiveEndIndex - _inclusiveStartIndex); for (uint256 i = _inclusiveStartIndex; i < _inclusiveEndIndex + 1; i++) { - data[i] = instanceData[instances[i]]; + data[i - _inclusiveStartIndex] = instanceData[instances[i]]; } } diff --git a/src/v2/TornadoRouter.sol b/src/v2/TornadoRouter.sol index 29a02be..b8e6db2 100644 --- a/src/v2/TornadoRouter.sol +++ b/src/v2/TornadoRouter.sol @@ -30,13 +30,19 @@ contract TornadoRouter is Initializable { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - /* @dev The address of the Governance proxy */ + /** + * @notice The address of the Governance proxy + */ address public immutable governanceProxyAddress; - /* @dev The instance registry */ + /** + * @notice The instance registry + */ InstanceRegistry public instanceRegistry; - /* @dev The relayer registry */ + /** + * @notice The relayer registry + */ RelayerRegistry public relayerRegistry; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -72,6 +78,12 @@ contract TornadoRouter is Initializable { relayerRegistry = RelayerRegistry(_relayerRegistryAddress); } + /** + * @notice Function to deposit into a Tornado instance. + * @param _tornado The instance to deposit into (address). + * @param _commitment The commitment which will be added to the Merkle Tree. + * @param _encryptedNote An encrypted note tied to the commitment which may be logged. + */ function deposit(ITornadoInstance _tornado, bytes32 _commitment, bytes calldata _encryptedNote) public payable @@ -79,6 +91,7 @@ contract TornadoRouter is Initializable { { (IERC20 token,, bool isERC20, bool isEnabled) = instanceRegistry.instanceData(_tornado); + // Better than having it revert at safeTransferFrom require(isEnabled, "TornadoRouter: instance not enabled"); if (isERC20) { @@ -90,6 +103,28 @@ contract TornadoRouter is Initializable { emit EncryptedNote(msg.sender, _encryptedNote); } + /** + * @notice Withdraw from a Tornado Instance. NOTE: Until infrastructure, which will include a + * smart-contract relayer wallet, isn't deployed, this function will be the only gateway for withdrawing + * from the _new_ instances. But fear not! Each instance contains a permissionless function by the name of + * `checkInfrastructureIsDead()`, which can be used in the case that the infra is deactivated or + * malfunctions (this contract can't malfunction to block withdrawals). It makes the instance work just + * like a regular one again! The intention is to call the formerly mentioned function when the new + * infrastructure is deployed, and until then, use the simple fix in the new instances to route + * withdrawals + * through the registry. ALSO NOTE, that manual depositors and withdrawers will still be able to + * permissionlessly use the instances, they anyways do not rely on the `relayer` address field, and as + * such they will be able to claim their deposits no matter whether the check is effective or not! + * @param _tornado The Tornado instance to withdraw from. + * @param _proof Bytes proof data. + * @param _root A current or historical bytes32 root of the Merkle Tree within the proofs context. + * @param _nullifierHash The bytes32 nullifierHash for the deposit. + * @param _recipient The address of recipient for withdrawn funds. + * @param _relayer The address of the relayer which will be making the withdrawal. + * @param _fee The fee in bips to pay the relayer. + * @param _refund If swapping into ETH on the other side, use this to specify how much should be paid for + * it. + */ function withdraw( ITornadoInstance _tornado, bytes calldata _proof, @@ -100,11 +135,19 @@ contract TornadoRouter is Initializable { uint256 _fee, uint256 _refund ) public payable virtual { + // If an instance is turned off, we still want withdrawals to pass + // so instead of requiring `isEnabled`, we will only use it to + // decide whether relayers should burn or not. This is beneficial + // for every party involved! (,,, bool isEnabled) = instanceRegistry.instanceData(_tornado); - require(isEnabled, "TornadoRouter: instance not enabled"); - - relayerRegistry.burn(msg.sender, _relayer, _tornado); + // Remove the require and instead just skip if there is no + // relayer being used or the instance is not enabled. Note that, + // even if the registry gets hijacked, you can just do + // manual withdrawals (using the SDK, for example). + if (_relayer != address(0) && isEnabled) { + relayerRegistry.burn(msg.sender, _relayer, _tornado); + } _tornado.withdraw{ value: msg.value }( _proof, _root, _nullifierHash, _recipient, _relayer, _fee, _refund diff --git a/src/v2/UniswapFeeOracle.sol b/src/v2/UniswapFeeOracle.sol new file mode 100644 index 0000000..ce7fc63 --- /dev/null +++ b/src/v2/UniswapFeeOracle.sol @@ -0,0 +1,394 @@ +// 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"; + +// 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 { IFeeOracle } from "./interfaces/IFeeOracle.sol"; + +import { UniswapV3OracleHelper } from "./libraries/UniswapV3OracleHelper.sol"; + +import { InstanceData } from "./InstanceRegistry.sol"; + +/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UNISWAP INLINED ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +interface IUniswapV2Pair { + event Approval(address indexed owner, address indexed spender, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); + + function name() external pure returns (string memory); + function symbol() external pure returns (string memory); + function decimals() external pure returns (uint8); + function totalSupply() external view returns (uint256); + function balanceOf(address owner) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); + + function approve(address spender, uint256 value) external returns (bool); + function transfer(address to, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); + + function DOMAIN_SEPARATOR() external view returns (bytes32); + function PERMIT_TYPEHASH() external pure returns (bytes32); + function nonces(address owner) external view returns (uint256); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + event Mint(address indexed sender, uint256 amount0, uint256 amount1); + event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to); + event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + address indexed to + ); + event Sync(uint112 reserve0, uint112 reserve1); + + function MINIMUM_LIQUIDITY() external pure returns (uint256); + function factory() external view returns (address); + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() + external + view + returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function price0CumulativeLast() external view returns (uint256); + function price1CumulativeLast() external view returns (uint256); + function kLast() external view returns (uint256); + + function mint(address to) external returns (uint256 liquidity); + function burn(address to) external returns (uint256 amount0, uint256 amount1); + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; + function skim(address to) external; + function sync() external; + + function initialize(address, address) external; +} + +library FixedPoint { + // range: [0, 2**112 - 1] + // resolution: 1 / 2**112 + struct uq112x112 { + uint224 _x; + } + + // range: [0, 2**144 - 1] + // resolution: 1 / 2**112 + struct uq144x112 { + uint256 _x; + } + + uint8 private constant RESOLUTION = 112; + + // encode a uint112 as a UQ112x112 + function encode(uint112 x) internal pure returns (uq112x112 memory) { + return uq112x112(uint224(x) << RESOLUTION); + } + + // encodes a uint144 as a UQ144x112 + function encode144(uint144 x) internal pure returns (uq144x112 memory) { + return uq144x112(uint256(x) << RESOLUTION); + } + + // divide a UQ112x112 by a uint112, returning a UQ112x112 + function div(uq112x112 memory self, uint112 x) internal pure returns (uq112x112 memory) { + require(x != 0, "FixedPoint: DIV_BY_ZERO"); + return uq112x112(self._x / uint224(x)); + } + + // multiply a UQ112x112 by a uint, returning a UQ144x112 + // reverts on overflow + function mul(uq112x112 memory self, uint256 y) internal pure returns (uq144x112 memory) { + uint256 z; + require( + y == 0 || (z = uint256(self._x) * y) / y == uint256(self._x), + "FixedPoint: MULTIPLICATION_OVERFLOW" + ); + return uq144x112(z); + } + + // returns a UQ112x112 which represents the ratio of the numerator to the denominator + // equivalent to encode(numerator).div(denominator) + function fraction(uint112 numerator, uint112 denominator) internal pure returns (uq112x112 memory) { + require(denominator > 0, "FixedPoint: DIV_BY_ZERO"); + return uq112x112((uint224(numerator) << RESOLUTION) / denominator); + } + + // decode a UQ112x112 into a uint112 by truncating after the radix point + function decode(uq112x112 memory self) internal pure returns (uint112) { + return uint112(self._x >> RESOLUTION); + } + + // decode a UQ144x112 into a uint144 by truncating after the radix point + function decode144(uq144x112 memory self) internal pure returns (uint144) { + return uint144(self._x >> RESOLUTION); + } +} + +library UniswapV2OracleLibrary { + using FixedPoint for *; + + // helper function that returns the current block timestamp within the range of uint32, i.e. [0, 2**32 - + // 1] + function currentBlockTimestamp() internal view returns (uint32) { + return uint32(block.timestamp % 2 ** 32); + } + + // produces the cumulative price using counterfactuals to save gas and avoid a call to sync. + function currentCumulativePrices(address pair) + internal + view + returns (uint256 price0Cumulative, uint256 price1Cumulative, uint32 blockTimestamp) + { + blockTimestamp = currentBlockTimestamp(); + price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); + price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); + + // if time has elapsed since the last update on the pair, mock the accumulated price values + (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves(); + if (blockTimestampLast != blockTimestamp) { + // subtraction overflow is desired + uint32 timeElapsed = blockTimestamp - blockTimestampLast; + // addition overflow is desired + // counterfactual + price0Cumulative += uint256(FixedPoint.fraction(reserve1, reserve0)._x) * timeElapsed; + // counterfactual + price1Cumulative += uint256(FixedPoint.fraction(reserve0, reserve1)._x) * timeElapsed; + } + } +} + +/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OUR CONTRACT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + +struct TWAPData { + FixedPoint.uq112x112 averagePrice; + uint32 updatedTimestamp; +} + +contract UniswapFeeOracle is IFeeOracle { + using SafeMath for uint256; + using FixedPoint for *; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + /** + * @notice Address of the Uniswap V2 TORN pool + */ + address public constant v2TORNPoolAddress = 0x0C722a487876989Af8a05FFfB6e32e45cc23FB3A; + + /** + * @notice The Governance Proxy address + */ + address public immutable governanceProxyAddress; + + /** + * @notice The Fee Oracle Manager address + */ + address public feeOracleManagerAddress; + + /** + * @notice The TWAP interval in seconds (this is common) + */ + uint32 public twapIntervalSeconds; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ V3 ORACLE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + /** + * @notice Uniswap V3 pool fees for each token registered + */ + mapping(IERC20 => uint24) public poolFeesByToken; + + /** + * @notice The minimum observation cardinality for other pools + */ + uint16 public minObservationCardinality; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ V2 TWAP ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + /** + * @notice Uniswap V2 torn pool cumulative price last + */ + uint256 public lastCumulativeTORNPriceInETH; + + /** + * @notice Last TWAP data + */ + TWAPData public last; + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + event PoolFeeUpdated(IERC20 token, uint24 newPoolFee); + event GlobalTORNPoolFeeUpdated(uint24 newPoolFee); + event GlobalTwapIntervalSecondsUpdated(uint32 newUniswapTimePeriod); + event GlobalMinObservationCardinalityUpdated(uint16 newMinObservationCardinality); + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LOGIC ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + constructor(address _governanceProxyAddress, address _feeOracleManagerAddress) public { + governanceProxyAddress = _governanceProxyAddress; + feeOracleManagerAddress = _feeOracleManagerAddress; + + (, uint256 _price1CumulativeLast, uint32 _timestamp) = + UniswapV2OracleLibrary.currentCumulativePrices(v2TORNPoolAddress); + + lastCumulativeTORNPriceInETH = _price1CumulativeLast; + last.updatedTimestamp = _timestamp; + } + + modifier onlyGovernance() { + require(msg.sender == governanceProxyAddress, "UniswapV3FeeOracle: onlyGovernance"); + _; + } + + modifier onlyFeeOracleManager() { + require(msg.sender == feeOracleManagerAddress, "UniswapV3FeeOracle: onlyGovernance"); + _; + } + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + function getFee( + IERC20, + ITornadoInstance _instance, + InstanceData memory _data, + uint32 _feePercent, + uint32 _feePercentDivisor + ) public view virtual override returns (uint160) { + // If 0%, 0 + 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); + + // Now get the price ratio + uint256 priceRatio = getTokenPerTORN(_data.token); + + // Denomination (tokens) times torn per token, through e18 because both are in e18, + // times fee percent and then through for proper value + return uint160( + _instance.denomination().mul(1e18).div(priceRatio).mul(uint256(_feePercent)).div( + uint256(_feePercentDivisor) + ) + ); + } + + function getTokenPerTORN(IERC20 _token) public view virtual returns (uint256) { + // Get the average price of ETH in the TOKEN + uint256 ethPerTokenAverage = UniswapV3OracleHelper.getPriceOfTokenInWETH( + address(_token), poolFeesByToken[_token], twapIntervalSeconds + ); + + // Now get average price of TORN per TOKEN + return ethPerTokenAverage * last.averagePrice.decode() / 1e18; + } + + function update() public virtual override onlyFeeOracleManager { + // Get the timestamp of the last update + uint32 timestampLastUpdate = last.updatedTimestamp; + + // Get the current one by Uniswaps logic, has inbuilt overflow + uint32 currentTimestamp = UniswapV2OracleLibrary.currentBlockTimestamp(); + + // Calculate elapsed time, no matter whether overflow (uniswap logic) + uint32 elapsed = currentTimestamp - timestampLastUpdate; + + // Get last token0/token1 meaning TORN per ETH + (, uint256 _price1CumulativeLast,) = UniswapV2OracleLibrary.currentCumulativePrices(v2TORNPoolAddress); + + // Save TWAP data, meaning the average price (TORN per ETH) + last = TWAPData({ + averagePrice: FixedPoint.uq112x112( + uint224((_price1CumulativeLast - lastCumulativeTORNPriceInETH) / elapsed) + ), + updatedTimestamp: currentTimestamp + }); + + // Update the cumulative value + lastCumulativeTORNPriceInETH = _price1CumulativeLast; + } + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + function setPoolFeeForToken(IERC20 _token, uint24 _tokenPoolFee) public virtual onlyGovernance { + // Get global config + + // Check whether globals are initialized + + require(twapIntervalSeconds != 0, "UniswapV3FeeOracle: time period not initialized"); + require(minObservationCardinality != 0, "UniswapV3FeeOracle: cardinality not initialized"); + + // Only do this if not zeroing out + if (_tokenPoolFee != 0) { + // 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( + minObservationCardinality <= observationCardinalityNext, + "UniswapV3FeeOracle: pool observation cardinality low" + ); + } + + // Store & log + + poolFeesByToken[_token] = _tokenPoolFee; + + emit PoolFeeUpdated(_token, _tokenPoolFee); + } + + function setTwapIntervalSeconds(uint32 _newGlobalTwapIntervalSeconds) public virtual onlyGovernance { + twapIntervalSeconds = _newGlobalTwapIntervalSeconds; + emit GlobalTwapIntervalSecondsUpdated(_newGlobalTwapIntervalSeconds); + } + + function setMinObservationCardinality(uint16 _newGlobalMinObservationCardinality) + public + virtual + onlyGovernance + { + minObservationCardinality = _newGlobalMinObservationCardinality; + emit GlobalMinObservationCardinalityUpdated(_newGlobalMinObservationCardinality); + } + + function setFeeOracleManagerAddress(address _newFeeOracleManagerAddress) public virtual onlyGovernance { + feeOracleManagerAddress = _newFeeOracleManagerAddress; + } + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + function getAverageTORNPerETH() public view virtual returns (uint256) { + return last.averagePrice.decode(); + } +} diff --git a/src/v2/UniswapV3FeeOracle.sol b/src/v2/UniswapV3FeeOracle.sol index 4423c99..70d26fc 100644 --- a/src/v2/UniswapV3FeeOracle.sol +++ b/src/v2/UniswapV3FeeOracle.sol @@ -46,13 +46,19 @@ contract UniswapV3FeeOracle is IFeeOracle { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - /* @dev The Governance Proxy address */ + /** + * @notice The Governance Proxy address + */ address public immutable governanceProxyAddress; - /* @dev Global configuration valid across all `getFee()` (and other) calls */ + /** + * @notice Global configuration valid across all `getFee()` (and other) calls + */ GlobalOracleConfig public globals; - /* @dev Uniswap pool fees for each token registered */ + /** + * @notice Uniswap pool fees for each token registered + */ mapping(IERC20 => uint24) public poolFeesByToken; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ EVENTS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -75,12 +81,14 @@ contract UniswapV3FeeOracle is IFeeOracle { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GETTERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + function update() public virtual override { } + function getFee( IERC20 _torn, ITornadoInstance _instance, InstanceData memory _data, - uint64 _feePercent, - uint64 _feePercentDivisor + uint32 _feePercent, + uint32 _feePercentDivisor ) public view virtual override returns (uint160) { // If fee is 0 return if (_feePercent == 0) return 0; diff --git a/src/v2/interfaces/IFeeOracle.sol b/src/v2/interfaces/IFeeOracle.sol index 353758b..74e6268 100644 --- a/src/v2/interfaces/IFeeOracle.sol +++ b/src/v2/interfaces/IFeeOracle.sol @@ -16,11 +16,13 @@ import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ import { InstanceData } from "../InstanceRegistry.sol"; interface IFeeOracle { + function update() external; + function getFee( IERC20 _torn, ITornadoInstance _instance, InstanceData memory _data, - uint64 _feePercent, - uint64 _feePercentDivisor + uint32 _feePercent, + uint32 _feePercentDivisor ) external view returns (uint160); } diff --git a/test/OracleTests.sol b/test/OracleTests.sol index 4359c8d..a31a359 100644 --- a/test/OracleTests.sol +++ b/test/OracleTests.sol @@ -19,18 +19,23 @@ import { console2 } from "forge-std/console2.sol"; // Local imports +import { FeeOracleManager } from "src/v2/FeeOracleManager.sol"; + import { IGovernance, Proposal } from "common/interfaces/IGovernance.sol"; import { TornadoAddresses } from "common/TornadoAddresses.sol"; -import { UniswapV3FeeOracle } from "src/v2/UniswapV3FeeOracle.sol"; +import { UniswapFeeOracle } from "src/v2/UniswapFeeOracle.sol"; import { CurveFeeOracle, ICurvePriceOracle, CurveChainedOracles } from "src/v2/CurveFeeOracle.sol"; import { InstanceData } from "src/v2/InstanceRegistry.sol"; contract OracleTests is Test { - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ + + FeeOracleManager public constant feeOracleManager = + FeeOracleManager(0x5f6c97C6AD7bdd0AE7E0Dd4ca33A4ED3fDabD4D7); address public constant crvUSDUSDCStableswap2Pool = 0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E; @@ -40,23 +45,22 @@ contract OracleTests is Test { IERC20 public constant TORN = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); - UniswapV3FeeOracle v3FeeOracle; + UniswapFeeOracle uniswapFeeOracle; CurveFeeOracle feeOracle; - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ TESTING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ TESTING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ function setUp() public { vm.createSelectFork(vm.envString("MAINNET_RPC_URL")); - v3FeeOracle = new UniswapV3FeeOracle(address(this)); - v3FeeOracle.setGlobalTornPoolFee(10_000, true); - v3FeeOracle.setGlobalTwapIntervalSeconds(5400); - v3FeeOracle.setGlobalMinObservationCardinality(10); + uniswapFeeOracle = new UniswapFeeOracle(address(this), address(feeOracleManager)); + uniswapFeeOracle.setTwapIntervalSeconds(5400); + uniswapFeeOracle.setMinObservationCardinality(10); feeOracle = new CurveFeeOracle(address(this)); feeOracle.setTornOracleIsUniswapV3(false); - feeOracle.setUniswapV3FeeOracle(v3FeeOracle); + feeOracle.setUniswapFeeOracle(uniswapFeeOracle); } function test_curveFeeSingleTricrypto() public { @@ -89,7 +93,7 @@ contract OracleTests is Test { ); } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HELPERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HELPERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ function _setCurveFeeSimpleTricryptoOracleForInstance( CurveFeeOracle _feeOracle, diff --git a/test/ProposalTests.sol b/test/ProposalTests.sol index 847726a..acedea0 100644 --- a/test/ProposalTests.sol +++ b/test/ProposalTests.sol @@ -11,13 +11,17 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { console2 } from "forge-std/console2.sol"; +// Tornado imports + +import { ITornadoInstance } from "tornado-anonymity-mining/contracts/interfaces/ITornadoInstance.sol"; + // Local imports -import { UniswapV3FeeOracle } from "src/v2/UniswapV3FeeOracle.sol"; +import { UniswapFeeOracle } from "src/v2/UniswapFeeOracle.sol"; import { CurveFeeOracle, ICurvePriceOracle, CurveChainedOracles } from "src/v2/CurveFeeOracle.sol"; -import { InstanceRegistry } from "src/v2/InstanceRegistry.sol"; +import { InstanceRegistry, InstanceData } from "src/v2/InstanceRegistry.sol"; import { FeeOracleManager } from "src/v2/FeeOracleManager.sol"; @@ -31,49 +35,486 @@ import { CRVUSDInstancesProposal } from "src/proposals/CRVUSDInstancesProposal.s import { TornadoProposalTest, ProposalState } from "./TornadoProposalTest.sol"; -contract ProposalTests is TornadoProposalTest { +contract Instances { + /* ETH instances */ + ITornadoInstance public constant eth01 = ITornadoInstance(0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc); + ITornadoInstance public constant eth1 = ITornadoInstance(0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936); + ITornadoInstance public constant eth10 = ITornadoInstance(0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF); + ITornadoInstance public constant eth100 = ITornadoInstance(0xA160cdAB225685dA1d56aa342Ad8841c3b53f291); + + /* DAI instances */ + ITornadoInstance public constant dai100 = ITornadoInstance(0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3); + ITornadoInstance public constant dai1000 = ITornadoInstance(0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144); + ITornadoInstance public constant dai10000 = ITornadoInstance(0x07687e702b410Fa43f4cB4Af7FA097918ffD2730); + ITornadoInstance public constant dai100000 = ITornadoInstance(0x23773E65ed146A459791799d01336DB287f25334); + + /* cDAI instances */ + ITornadoInstance public constant cdai100 = ITornadoInstance(0x22aaA7720ddd5388A3c0A3333430953C68f1849b); + ITornadoInstance public constant cdai1000 = ITornadoInstance(0x03893a7c7463AE47D46bc7f091665f1893656003); + ITornadoInstance public constant cdai10000 = ITornadoInstance(0x2717c5e28cf931547B621a5dddb772Ab6A35B701); + ITornadoInstance public constant cdai100000 = ITornadoInstance(0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af); + + /* USDT instances */ + ITornadoInstance public constant usdt100 = ITornadoInstance(0x169AD27A470D064DEDE56a2D3ff727986b15D52B); + ITornadoInstance public constant usdt1000 = ITornadoInstance(0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f); + + /* WBTC instances */ + ITornadoInstance public constant wbtc01 = ITornadoInstance(0x178169B423a011fff22B9e3F3abeA13414dDD0F1); + ITornadoInstance public constant wbtc1 = ITornadoInstance(0x610B717796ad172B316836AC95a2ffad065CeaB4); + ITornadoInstance public constant wbtc10 = ITornadoInstance(0xbB93e510BbCD0B7beb5A853875f9eC60275CF498); + + /* CRVUSD instances */ + ITornadoInstance public constant cu100 = ITornadoInstance(0x913a73486Dc4AA3832A56d461542836C1eeB93be); + ITornadoInstance public constant cu1_000 = ITornadoInstance(0x5A6b3C829dB3e938C885000c6E93CF35E74876a4); + ITornadoInstance public constant cu10_000 = ITornadoInstance(0x49f173CDAB99a2C3800F1255393DF9B7a17B82Bb); + ITornadoInstance public constant cu100_000 = ITornadoInstance(0x4640Dffc9fD0B113B983e3A350b070a119CA143C); + ITornadoInstance public constant cu1_000_000 = + ITornadoInstance(0xc4eA8Bd3Fd76f3c255395793B47F7c55aD59d991); +} + +contract ProposalTests is Instances, TornadoProposalTest { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ IERC20 public constant TORN = IERC20(0x77777FeDdddFfC19Ff86DB637967013e6C6A116C); + IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); - CurveFeeOracle curveFeeOracle; + FeeOracleManager public constant feeOracleManager = + FeeOracleManager(0x5f6c97C6AD7bdd0AE7E0Dd4ca33A4ED3fDabD4D7); - UniswapV3FeeOracle v3FeeOracle; + InstanceRegistry public constant instanceRegistry = + InstanceRegistry(0xB20c66C4DE72433F3cE747b58B86830c459CA911); + + // Implementations InstanceRegistry implInstanceRegisty; - FeeOracleManager feeOracleManager; + FeeOracleManager implFeeOracleManager; + + // Actual contracts + + CurveFeeOracle curveFeeOracle; + + UniswapFeeOracle uniswapFeeOracle; TornadoRouter router; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ TESTING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ function setUp() public override { - super.setUp(); + TornadoProposalTest.setUp(); - vm.rollFork(17_438_199); + vm.rollFork(17_486_841); curveFeeOracle = new CurveFeeOracle(address(governance)); - v3FeeOracle = new UniswapV3FeeOracle(address(governance)); + uniswapFeeOracle = new UniswapFeeOracle(address(governance), address(feeOracleManager)); vm.prank(address(governance)); - curveFeeOracle.setUniswapV3FeeOracle(v3FeeOracle); + curveFeeOracle.setUniswapFeeOracle(uniswapFeeOracle); implInstanceRegisty = new InstanceRegistry(address(governance)); - feeOracleManager = new FeeOracleManager(address(TORN), address(governance)); + implFeeOracleManager = new FeeOracleManager(address(TORN), address(governance)); - router = new TornadoRouter(address(governance)); + router = TornadoRouter(vm.envAddress("TORNADO_ROUTER_ADDRESS")); + } + + function test_crvusdInstancesBasic() public { + // Do the proposal first + test_crvusdInstancesProposalBasic(); + + // Try to update crvusd 10000 + feeOracleManager.updateFee(cu10_000, false); + + // Try to update all + ITornadoInstance[] memory _toUpdate = new ITornadoInstance[](5); + + _toUpdate[0] = cu100; + _toUpdate[1] = cu1_000; + _toUpdate[2] = cu10_000; + _toUpdate[3] = cu100_000; + _toUpdate[4] = cu1_000_000; + + feeOracleManager.updateFees(_toUpdate, false); + + // Print fees to console. + + console2.log("\n~~~~~~~~~~~~~~~~~~ LAST FEES ~~~~~~~~~~~~~~~~~~\n"); + console2.log("cu100: ", uint256(feeOracleManager.getLastFeeForInstance(cu100))); + console2.log("cu1_000: ", uint256(feeOracleManager.getLastFeeForInstance(cu1_000))); + console2.log("cu10_000: ", uint256(feeOracleManager.getLastFeeForInstance(cu10_000))); + console2.log("cu100_000: ", uint256(feeOracleManager.getLastFeeForInstance(cu100_000))); + console2.log("cu1_000_000: ", uint256(feeOracleManager.getLastFeeForInstance(cu1_000_000))); + console2.log("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"); + + delimit(); + + console2.log("\n~~~~~~~~~~~~~~~~~~ UPDATED FEES ~~~~~~~~~~~~~~~~~~\n"); + console2.log("cu100: ", uint256(feeOracleManager.instanceFeeWithUpdate(cu100))); + console2.log("cu1_000: ", uint256(feeOracleManager.instanceFeeWithUpdate(cu1_000))); + console2.log("cu10_000: ", uint256(feeOracleManager.instanceFeeWithUpdate(cu10_000))); + console2.log("cu100_000: ", uint256(feeOracleManager.instanceFeeWithUpdate(cu100_000))); + console2.log("cu1_000_000: ", uint256(feeOracleManager.instanceFeeWithUpdate(cu1_000_000))); + console2.log("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"); + + delimit(); + + console2.log("\n~~~~~~~~~~~~~~~~~~ DATA ~~~~~~~~~~~~~~~~~~\n"); + InstanceData memory data = instanceRegistry.getInstanceData(cu100); + delimit(); + console2.log("cu100:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + delimit(); + data = instanceRegistry.getInstanceData(cu1_000); + console2.log("cu1_000:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + delimit(); + data = instanceRegistry.getInstanceData(cu10_000); + console2.log("cu10_000:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + delimit(); + data = instanceRegistry.getInstanceData(cu100_000); + console2.log("cu100_000:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + delimit(); + data = instanceRegistry.getInstanceData(cu1_000_000); + console2.log("cu1_000_000:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + delimit(); + console2.log("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"); + + // Some assertions + + require( + strcomp(curveFeeOracle.getChainedOracleNameForInstance(cu100), "ETH/CRVUSD"), + "oracle name doesnt match" + ); + require( + strcomp( + curveFeeOracle.getChainedOracleNameForOracleHash( + curveFeeOracle.getChainedOracleHashForInstance(cu1_000) + ), + "ETH/CRVUSD" + ), + "oracle name doesnt match" + ); + + // Should be able to update all fees + feeOracleManager.updateAllFees(true); + feeOracleManager.updateAllFees(false); + + // Just for some gas values for this call + instanceRegistry.instanceData(cu100); + instanceRegistry.instanceData(cu1_000); + } + + function test_infrastructureBasic() public { + // Do the proposal first + test_infrastructureUpgradeProposalBasic(); + + // Try to update eth10 + feeOracleManager.updateFee(eth10, false); + + // Try to update multiple + ITornadoInstance[] memory _toUpdate = new ITornadoInstance[](3); + + _toUpdate[0] = eth100; + _toUpdate[1] = dai10000; + _toUpdate[2] = usdt100; + + feeOracleManager.updateFees(_toUpdate, false); + + require(feeOracleManager.getLastUpdatedTimeForInstance(eth100) == now, "timeup"); + + // Must not be able to re-initialize the InstanceRegistry + + vm.expectRevert(); + vm.prank(address(governance)); + instanceRegistry.initialize(_toUpdate, router); + + vm.expectRevert(); + instanceRegistry.initialize(_toUpdate, router); + + // Must not be able to re-initialize the FeeOracleManager + + vm.expectRevert(); + vm.prank(address(governance)); + feeOracleManager.initialize( + address(uniswapFeeOracle), address(instanceRegistry), _toUpdate, feeArrayForTesting_1() + ); + + vm.expectRevert(); + feeOracleManager.initialize( + address(uniswapFeeOracle), address(instanceRegistry), _toUpdate, feeArrayForTesting_1() + ); + + // Must not be able to re-initialize the router... the contract addresses are unimportant + + vm.expectRevert(); + vm.prank(address(governance)); + router.initialize(address(instanceRegistry), address(instanceRegistry)); + + vm.expectRevert(); + router.initialize(address(instanceRegistry), address(instanceRegistry)); + + // Check fee logic and print to console. + + console2.log("\n~~~~~~~~~~~~~~~~~~ LAST FEES ~~~~~~~~~~~~~~~~~~\n"); + console2.log("eth10: ", uint256(feeOracleManager.getLastFeeForInstance(eth10))); + console2.log("eth100: ", uint256(feeOracleManager.getLastFeeForInstance(eth100))); + console2.log("dai10000: ", uint256(feeOracleManager.getLastFeeForInstance(dai10000))); + console2.log("usdt100: ", uint256(feeOracleManager.getLastFeeForInstance(usdt100))); + console2.log("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"); + + delimit(); + + console2.log("\n~~~~~~~~~~~~~~~~~~ UPDATED FEES ~~~~~~~~~~~~~~~~~~\n"); + console2.log("eth10: ", uint256(feeOracleManager.instanceFeeWithUpdate(eth10))); + console2.log("eth100: ", uint256(feeOracleManager.instanceFeeWithUpdate(eth100))); + console2.log("dai10000: ", uint256(feeOracleManager.instanceFeeWithUpdate(dai10000))); + console2.log("usdt100: ", uint256(feeOracleManager.instanceFeeWithUpdate(usdt100))); + console2.log("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"); + + delimit(); + + console2.log("\n~~~~~~~~~~~~~~~~~~ DATA ~~~~~~~~~~~~~~~~~~\n"); + InstanceData memory data = instanceRegistry.getInstanceData(eth10); + delimit(); + console2.log("eth10:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + delimit(); + data = instanceRegistry.getInstanceData(eth100); + console2.log("eth100:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + delimit(); + data = instanceRegistry.getInstanceData(dai10000); + console2.log("dai10000:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + delimit(); + data = instanceRegistry.getInstanceData(usdt100); + console2.log("usdt100:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + delimit(); + console2.log("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"); + + // Now should be able to rmemove an instance + vm.prank(address(governance)); + instanceRegistry.removeInstanceByAddress(address(dai10000)); // DAI10k + + vm.prank(address(governance)); + instanceRegistry.removeInstanceByIndex(2); // ETH10 + + // Now log again + console2.log("\n~~~~~~~~~~~~~~~~~~ SHOULD HAVE CHANGED ~~~~~~~~~~~~~~~~~~\n"); + delimit(); + data = instanceRegistry.getInstanceData(2); + console2.log("NOT eth10:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + delimit(); + data = instanceRegistry.getInstanceData(6); + console2.log("NOT dai10000:"); + console2.log("token: ", address(data.token)); + console2.log("index: ", uint256(data.index)); + console2.log("iserc20: ", data.isERC20); + console2.log("isenabled: ", data.isEnabled); + console2.log("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"); + + // Now add them back and only do assertions + vm.prank(address(governance)); + instanceRegistry.addInstance(dai10000); + + vm.prank(address(governance)); + instanceRegistry.addInstance(eth10); + + vm.prank(address(governance)); + data = instanceRegistry.getInstanceData(dai10000); + require(data.token == DAI, "not dai"); + require(data.index == 15, "not 15"); + require(data.isERC20, "not token"); + require(data.isEnabled, "not enabled"); + + vm.prank(address(governance)); + data = instanceRegistry.getInstanceData(eth10); + require(data.token == IERC20(0), "not eth"); + require(data.index == 16, "not last"); + require(!data.isERC20, "not not token"); + require(data.isEnabled, "not enabled"); + + // Some assertions to test getters + require(instanceRegistry.getInstanceIndex(eth100) == 3, "Wrong index"); + require(instanceRegistry.getInstanceToken(eth100) == IERC20(address(0)), "Wrong token"); + require(instanceRegistry.getInstanceToken(dai10000) == DAI, "Wrong token"); + require(instanceRegistry.isEnabledInstance(dai10000) == true, "Wrong token"); + + // Just to get some gas values for this fn + instanceRegistry.instanceData(eth10); + instanceRegistry.instanceData(eth100); + instanceRegistry.instanceData(dai10000); + + // Expect no update if in inside the time limit interval + + uint256 startTimestamp = block.timestamp; + uint256 eth100Fee = feeOracleManager.getLastFeeForInstance(eth100); + uint256 interval = feeOracleManager.getFeeUpdateIntervalForInstance(eth100); + + vm.prank(address(governance)); + feeOracleManager.setFeePercentForInstance(eth100, 50); + + feeOracleManager.updateFee(eth100, true); + require(eth100Fee == feeOracleManager.getLastFeeForInstance(eth100), "wrong fee 1"); + + vm.warp(startTimestamp + interval + 5 days); + + feeOracleManager.updateFee(eth100, true); + uint256 eth100Fee2 = feeOracleManager.getLastFeeForInstance(eth100); + require(eth100Fee != eth100Fee2, "wrong fee 2"); + + vm.prank(address(governance)); + feeOracleManager.setFeePercentForInstance(eth100, 30); + + vm.warp(startTimestamp + 2 * interval + 10 days); + + feeOracleManager.updateFee(eth100, true); + require(eth100Fee == feeOracleManager.getLastFeeForInstance(eth100), "wrong fee 3"); + + vm.warp(startTimestamp); + + // Should be able to update all fees + feeOracleManager.updateAllFees(true); + feeOracleManager.updateAllFees(false); + + // Deviations don't revert + + feeOracleManager.getAllFeeDeviations(); + + // There is a deviation + + vm.prank(address(governance)); + feeOracleManager.setFeePercentForInstance(eth100, 10); + + _toUpdate = new ITornadoInstance[](1); + + _toUpdate[0] = eth100; + + int256[] memory eth100Deviations = feeOracleManager.getFeeDeviationsForInstances(_toUpdate); + + require(eth100Deviations[0] != 0, "deviations"); + + vm.prank(address(governance)); + feeOracleManager.setFeePercentForInstance(eth100, 30); + + feeOracleManager.updateFee(eth100, false); + + // Should be able to deposit via router + + address depositor = address(bytes20(bytes32(keccak256("depositor")))); + + vm.deal(depositor, 1 ether); // for gas + + vm.prank(address(DAI)); + DAI.transfer(depositor, 100_000e18); + + require(DAI.balanceOf(depositor) == 100_000e18, "not enough depo bal"); + + vm.prank(address(depositor)); + DAI.approve(address(router), 100_000e18); + + bytes32 commitment = 0x00eb84375e4bfd042468fe624e6d322dba4f601b37dd0a44f741d3cb04127df0; + + bytes memory empty; + + vm.prank(address(depositor)); + + router.deposit(dai100000, commitment, empty); + + // Let's toss in some functions for coverage + + vm.prank(address(governance)); + feeOracleManager.setFeeUpdateIntervalForInstance(eth100, 60_000); + + vm.prank(address(governance)); + feeOracleManager.setFeeUpdateIntervalForInstance(eth100, uint24(interval)); + + require(feeOracleManager.getFeePercentForInstance(eth100) == 30, "fee percent eth100"); + require(feeOracleManager.getLastUpdatedTimeForInstance(eth100) == now, "updated time eth100"); + + // Reverts + + vm.expectRevert(); + feeOracleManager.setInstanceRegistry(address(0)); + vm.expectRevert(); + feeOracleManager.setFeeOracle(address(eth100), address(0)); + vm.expectRevert(); + feeOracleManager.setFeePercentForInstance(eth100, 40); + vm.expectRevert(); + feeOracleManager.setFeeUpdateIntervalForInstance(eth100, uint24(interval - 3)); + + // Should be able to delete and set an oracle + + vm.prank(address(governance)); + feeOracleManager.setFeeOracle(address(eth100), address(0)); + + require(address(feeOracleManager.instanceFeeOracles(eth100)) == address(0), "fee oracle deletion"); + + vm.prank(address(governance)); + feeOracleManager.setFeeOracle(address(eth100), address(uniswapFeeOracle)); + } + + function test_crvusdInstancesProposalBasic() public { + // First pass the former proposal + test_infrastructureUpgradeProposalBasic(); + + // Then create the crvUSD proposal + address proposal = + address(new CRVUSDInstancesProposal(address(curveFeeOracle), address(uniswapFeeOracle))); + + // Propose + uint256 id = easyPropose(proposal); + + // Wait + waitUntilExecutable(id); + + // Exec + governance.execute(id); } function test_infrastructureUpgradeProposalBasic() public { // Create proposal address proposal = address( new InfrastructureUpgradeProposal( - address(v3FeeOracle), - address(feeOracleManager), + address(uniswapFeeOracle), + address(implFeeOracleManager), address(implInstanceRegisty), address(router) ) @@ -87,22 +528,30 @@ contract ProposalTests is TornadoProposalTest { // Exec governance.execute(id); + + // Test whethe resolving names works... with the Governance contract + + require( + address(instanceRegistry.getInstanceByENSName("governance.contract.tornadocash.eth")) + == address(governance), + "ENSResolving doesn't work" + ); } - function test_crvusdInstancesProposalBasic() public { - // First pass the former proposal - test_infrastructureUpgradeProposalBasic(); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HELPERS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Then create the crvUSD proposal - address proposal = address(new CRVUSDInstancesProposal(address(curveFeeOracle), address(v3FeeOracle))); + function delimit() internal view { + console2.log(); + } - // Propose - uint256 id = easyPropose(proposal); + function strcomp(string memory left, string memory right) internal pure returns (bool) { + return keccak256(abi.encode(left)) == keccak256(abi.encode(right)); + } - // Wait - waitUntilExecutable(id); - - // Exec - governance.execute(id); + function feeArrayForTesting_1() internal pure returns (uint256[] memory fees) { + fees = new uint256[](3); + fees[0] = 30; + fees[1] = 30; + fees[2] = 30; } }