From 59a7a1bfbe6a16779697d1772be52428113686b6 Mon Sep 17 00:00:00 2001 From: kyriediculous Date: Tue, 26 Sep 2023 17:14:46 +0700 Subject: [PATCH] Livepeer: Fix Uniswap interaction, use TWAP oracle --- .gitmodules | 3 ++ lib/uniswap-v3-core | 1 + remappings.txt | 1 + src/adapters/LivepeerAdapter.sol | 55 +++++++++++++++----------------- src/utils/TWAP.sol | 37 +++++++++++++++++++++ 5 files changed, 68 insertions(+), 29 deletions(-) create mode 160000 lib/uniswap-v3-core create mode 100644 src/utils/TWAP.sol diff --git a/.gitmodules b/.gitmodules index 5ed1b89..6e308a5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -23,3 +23,6 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/uniswap-v3-core"] + path = lib/uniswap-v3-core + url = https://github.com/tenderize/uniswap-v3-core diff --git a/lib/uniswap-v3-core b/lib/uniswap-v3-core new file mode 160000 index 0000000..f55f3a9 --- /dev/null +++ b/lib/uniswap-v3-core @@ -0,0 +1 @@ +Subproject commit f55f3a97391fa715e6b5ab94a4e9587938dd5612 diff --git a/remappings.txt b/remappings.txt index 69c4f37..838b824 100644 --- a/remappings.txt +++ b/remappings.txt @@ -6,4 +6,5 @@ solmate/=lib/solmate/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/ openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ clones/=lib/clones-with-immutable-args/src/ +@uniswap/v3-core=lib/uniswap-v3-core/contracts/ test/=test/ diff --git a/src/adapters/LivepeerAdapter.sol b/src/adapters/LivepeerAdapter.sol index b3f7fad..5565c76 100644 --- a/src/adapters/LivepeerAdapter.sol +++ b/src/adapters/LivepeerAdapter.sol @@ -19,6 +19,7 @@ import { ILivepeerBondingManager, ILivepeerRoundsManager } from "core/adapters/i import { ISwapRouter } from "core/adapters/interfaces/ISwapRouter.sol"; import { IWETH9 } from "core/adapters/interfaces/IWETH9.sol"; import { IERC165 } from "core/interfaces/IERC165.sol"; +import { TWAP } from "core/utils/TWAP.sol"; contract LivepeerAdapter is Adapter { using SafeTransferLib for ERC20; @@ -125,34 +126,30 @@ contract LivepeerAdapter is Adapter { function _livepeerClaimFees() internal { // get pending fees uint256 pendingFees; - if ((pendingFees = LIVEPEER.pendingFees(address(this), 0)) >= ETH_THRESHOLD) { - // withdraw fees - LIVEPEER.withdrawFees(payable(address(this)), pendingFees); - // convert fees to WETH - WETH.deposit{ value: address(this).balance }(); - ERC20(address(WETH)).safeApprove(address(UNISWAP_ROUTER), address(this).balance); - // Create initial params for swap - ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ - tokenIn: address(WETH), - tokenOut: address(address(LPT)), - fee: UNISWAP_POOL_FEE, - recipient: address(this), - deadline: block.timestamp, - amountIn: address(this).balance, - amountOutMinimum: 0, - sqrtPriceLimitX96: 0 - }); - - (bool success, bytes memory returnData) = - address(UNISWAP_ROUTER).staticcall(abi.encodeCall(UNISWAP_ROUTER.exactInputSingle, (params))); - - if (!success) return; - - // set return value of staticcall to minimum LPT value to receive from swap - params.amountOutMinimum = abi.decode(returnData, (uint256)); - - // execute swap - UNISWAP_ROUTER.exactInputSingle(params); - } + if ((pendingFees = LIVEPEER.pendingFees(address(this), 0)) < ETH_THRESHOLD) return; + + // withdraw fees + LIVEPEER.withdrawFees(payable(address(this)), pendingFees); + // get ETH balance + uint256 ethBalance = address(this).balance; + // convert fees to WETH + WETH.deposit{ value: ethBalance }(); + ERC20(address(WETH)).safeApprove(address(UNISWAP_ROUTER), address(this).balance); + // Calculate Slippage Threshold + uint256 twapPrice = TWAP.getPriceX96FromSqrtPriceX96(TWAP.getSqrtTwapX96(UNI_POOL, TWAP_INTERVAL)); + // Create initial params for swap + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: address(WETH), + tokenOut: address(address(LPT)), + fee: UNISWAP_POOL_FEE, + recipient: address(this), + deadline: block.timestamp, + amountIn: ethBalance, + amountOutMinimum: ethBalance * twapPrice * 90 / 100, // 10% slippage threshold + sqrtPriceLimitX96: 0 + }); + + // execute swap + UNISWAP_ROUTER.exactInputSingle(params); } } diff --git a/src/utils/TWAP.sol b/src/utils/TWAP.sol new file mode 100644 index 0000000..d14e25b --- /dev/null +++ b/src/utils/TWAP.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +// +// _____ _ _ +// |_ _| | | (_) +// | | ___ _ __ __| | ___ _ __ _ _______ +// | |/ _ \ '_ \ / _` |/ _ \ '__| |_ / _ \ +// | | __/ | | | (_| | __/ | | |/ / __/ +// \_/\___|_| |_|\__,_|\___|_| |_/___\___| +// +// Copyright (c) Tenderize Labs Ltd + +import "@uniswap/v3-core/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/libraries/TickMath.sol"; +import "@uniswap/v3-core/libraries/FixedPoint96.sol"; +import "@uniswap/v3-core/libraries/FullMath.sol"; + +library TWAP { + function getSqrtTwapX96(address uniswapV3Pool, uint32 twapInterval) internal view returns (uint160 sqrtPriceX96) { + if (twapInterval == 0) { + // return the current price if twapInterval == 0 + (sqrtPriceX96,,,,,,) = IUniswapV3Pool(uniswapV3Pool).slot0(); + } else { + uint32[] memory secondsAgos = new uint32[](2); + secondsAgos[0] = twapInterval; // from (before) + secondsAgos[1] = 0; // to (now) + + (int56[] memory tickCumulatives,) = IUniswapV3Pool(uniswapV3Pool).observe(secondsAgos); + + // tick(imprecise as it's an integer) to price + sqrtPriceX96 = TickMath.getSqrtRatioAtTick(int24((tickCumulatives[1] - tickCumulatives[0]) / int32(twapInterval))); + } + } + + function getPriceX96FromSqrtPriceX96(uint160 sqrtPriceX96) internal pure returns (uint256 priceX96) { + return FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, FixedPoint96.Q96); + } +}