From 1266541487630f80469fe8ec02227a2fba9473b2 Mon Sep 17 00:00:00 2001 From: pradyuman-verma Date: Sun, 16 Jan 2022 15:14:14 +0530 Subject: [PATCH] added uniswap v3 staker --- .../connectors/uniswap/v3_staker/events.sol | 30 ++ .../connectors/uniswap/v3_staker/helpers.sol | 88 ++++++ .../uniswap/v3_staker/interface.sol | 231 ++++++++++++++++ .../connectors/uniswap/v3_staker/main.sol | 256 ++++++++++++++++++ 4 files changed, 605 insertions(+) create mode 100644 contracts/optimism/connectors/uniswap/v3_staker/events.sol create mode 100644 contracts/optimism/connectors/uniswap/v3_staker/helpers.sol create mode 100644 contracts/optimism/connectors/uniswap/v3_staker/interface.sol create mode 100644 contracts/optimism/connectors/uniswap/v3_staker/main.sol diff --git a/contracts/optimism/connectors/uniswap/v3_staker/events.sol b/contracts/optimism/connectors/uniswap/v3_staker/events.sol new file mode 100644 index 00000000..d670cfaa --- /dev/null +++ b/contracts/optimism/connectors/uniswap/v3_staker/events.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.7.0; + +contract Events { + event LogDeposit(uint256 tokenId); + + event LogDepositAndStake(uint256 tokenId, bytes32 incentiveId); + + event LogWithdraw(uint256 indexed tokenId); + + event LogDepositTransfer(uint256 indexed tokenId, address to); + + event LogStake(uint256 indexed tokenId, bytes32 incentiveId); + + event LogUnstake(uint256 indexed tokenId, bytes32 incentiveId); + + event LogRewardClaimed( + address indexed rewardToken, + uint256 amount + ); + + event LogIncentiveCreated( + bytes32 incentiveId, + address poolAddr, + address refundee, + uint256 startTime, + uint256 endTime, + uint256 reward + ); +} diff --git a/contracts/optimism/connectors/uniswap/v3_staker/helpers.sol b/contracts/optimism/connectors/uniswap/v3_staker/helpers.sol new file mode 100644 index 00000000..07841dc1 --- /dev/null +++ b/contracts/optimism/connectors/uniswap/v3_staker/helpers.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +import {TokenInterface} from "../../../common/interfaces.sol"; +import {DSMath} from "../../../common/math.sol"; +import {Basic} from "../../../common/basic.sol"; +import "./interface.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; + +abstract contract Helpers is DSMath, Basic { + /** + * @dev uniswap v3 NFT Position Manager & Swap Router + */ + INonfungiblePositionManager constant nftManager = + INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); + IUniswapV3Staker constant staker = + IUniswapV3Staker(0x1f98407aaB862CdDeF78Ed252D6f557aA5b0f00d); + + /** + * @dev Get Last NFT Index + * @param user: User address + */ + function _getLastNftId(address user) + internal + view + returns (uint256 tokenId) + { + uint256 len = nftManager.balanceOf(user); + tokenId = nftManager.tokenOfOwnerByIndex(user, len - 1); + } + + function getPoolAddress(uint256 _tokenId) + internal + view + returns (address pool) + { + (bool success, bytes memory data) = address(nftManager).staticcall( + abi.encodeWithSelector(nftManager.positions.selector, _tokenId) + ); + require(success, "fetching positions failed"); + { + (, , address token0, address token1, uint24 fee, , , ) = abi.decode( + data, + ( + uint96, + address, + address, + address, + uint24, + int24, + int24, + uint128 + ) + ); + + pool = PoolAddress.computeAddress( + nftManager.factory(), + PoolAddress.PoolKey({token0: token0, token1: token1, fee: fee}) + ); + } + } + + function _stake( + uint256 _tokenId, + IUniswapV3Staker.IncentiveKey memory _incentiveId + ) internal { + staker.stakeToken(_incentiveId, _tokenId); + } + + function _unstake( + IUniswapV3Staker.IncentiveKey memory _key, + uint256 _tokenId + ) internal { + staker.unstakeToken(_key, _tokenId); + } + + function _claimRewards( + IERC20Minimal _rewardToken, + address _to, + uint256 _amountRequested + ) internal returns (uint256 rewards) { + rewards = staker.claimReward(_rewardToken, _to, _amountRequested); + } +} diff --git a/contracts/optimism/connectors/uniswap/v3_staker/interface.sol b/contracts/optimism/connectors/uniswap/v3_staker/interface.sol new file mode 100644 index 00000000..11a9926b --- /dev/null +++ b/contracts/optimism/connectors/uniswap/v3_staker/interface.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity =0.7.6; +pragma abicoder v2; + +import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol'; + +import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol'; +import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; +import '@uniswap/v3-core/contracts/interfaces/IERC20Minimal.sol'; + +import '@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol'; +import '@uniswap/v3-periphery/contracts/interfaces/IMulticall.sol'; + +/** + * @title Uniswap V3 Staker Interface + * @notice Allows staking nonfungible liquidity tokens in exchange for reward tokens + */ +interface IUniswapV3Staker is IERC721Receiver, IMulticall { + /** + * @param rewardToken The token being distributed as a reward + * @param pool The Uniswap V3 pool + * @param startTime The time when the incentive program begins + * @param endTime The time when rewards stop accruing + * @param refundee The address which receives any remaining reward tokens when the incentive is ended + */ + struct IncentiveKey { + IERC20Minimal rewardToken; + IUniswapV3Pool pool; + uint256 startTime; + uint256 endTime; + address refundee; + } + + /** + * @notice The Uniswap V3 Factory + */ + function factory() external view returns (IUniswapV3Factory); + + /** + * @notice The nonfungible position manager with which this staking contract is compatible + */ + function nonfungiblePositionManager() external view returns (INonfungiblePositionManager); + + /** + * @notice The max duration of an incentive in seconds + */ + function maxIncentiveDuration() external view returns (uint256); + + /** + * @notice The max amount of seconds into the future the incentive startTime can be set + */ + function maxIncentiveStartLeadTime() external view returns (uint256); + + /** + * @notice Represents a staking incentive + * @param incentiveId The ID of the incentive computed from its parameters + * @return totalRewardUnclaimed The amount of reward token not yet claimed by users + * @return totalSecondsClaimedX128 Total liquidity-seconds claimed, represented as a UQ32.128 + * @return numberOfStakes The count of deposits that are currently staked for the incentive + */ + function incentives(bytes32 incentiveId) + external + view + returns ( + uint256 totalRewardUnclaimed, + uint160 totalSecondsClaimedX128, + uint96 numberOfStakes + ); + + /** + * @notice Returns information about a deposited NFT + * @return owner The owner of the deposited NFT + * @return numberOfStakes Counter of how many incentives for which the liquidity is staked + * @return tickLower The lower tick of the range + * @return tickUpper The upper tick of the range + */ + function deposits(uint256 tokenId) + external + view + returns ( + address owner, + uint48 numberOfStakes, + int24 tickLower, + int24 tickUpper + ); + + /** + * @notice Returns information about a staked liquidity NFT + * @param tokenId The ID of the staked token + * @param incentiveId The ID of the incentive for which the token is staked + * @return secondsPerLiquidityInsideInitialX128 secondsPerLiquidity represented as a UQ32.128 + * @return liquidity The amount of liquidity in the NFT as of the last time the rewards were computed + */ + function stakes(uint256 tokenId, bytes32 incentiveId) + external + view + returns (uint160 secondsPerLiquidityInsideInitialX128, uint128 liquidity); + + /** + * @notice Returns amounts of reward tokens owed to a given address according to the last time all stakes were updated + * @param rewardToken The token for which to check rewards + * @param owner The owner for which the rewards owed are checked + * @return rewardsOwed The amount of the reward token claimable by the owner + */ + function rewards(IERC20Minimal rewardToken, address owner) external view returns (uint256 rewardsOwed); + + /** + * @notice Creates a new liquidity mining incentive program + * @param key Details of the incentive to create + * @param reward The amount of reward tokens to be distributed + */ + function createIncentive(IncentiveKey memory key, uint256 reward) external; + + /** + * @notice Ends an incentive after the incentive end time has passed and all stakes have been withdrawn + * @param key Details of the incentive to end + * @return refund The remaining reward tokens when the incentive is ended + */ + function endIncentive(IncentiveKey memory key) external returns (uint256 refund); + + /** + * @notice Transfers ownership of a deposit from the sender to the given recipient + * @param tokenId The ID of the token (and the deposit) to transfer + * @param to The new owner of the deposit + */ + function transferDeposit(uint256 tokenId, address to) external; + + /** + * @notice Withdraws a Uniswap V3 LP token `tokenId` from this contract to the recipient `to` + * @param tokenId The unique identifier of an Uniswap V3 LP token + * @param to The address where the LP token will be sent + * @param data An optional data array that will be passed along to the `to` address via the NFT safeTransferFrom + */ + function withdrawToken( + uint256 tokenId, + address to, + bytes memory data + ) external; + + /** + * @notice Stakes a Uniswap V3 LP token + * @param key The key of the incentive for which to stake the NFT + * @param tokenId The ID of the token to stake + */ + function stakeToken(IncentiveKey memory key, uint256 tokenId) external; + + /** + * @notice Unstakes a Uniswap V3 LP token + * @param key The key of the incentive for which to unstake the NFT + * @param tokenId The ID of the token to unstake + */ + function unstakeToken(IncentiveKey memory key, uint256 tokenId) external; + + /** + * @notice Transfers `amountRequested` of accrued `rewardToken` rewards from the contract to the recipient `to` + * @param rewardToken The token being distributed as a reward + * @param to The address where claimed rewards will be sent to + * @param amountRequested The amount of reward tokens to claim. Claims entire reward amount if set to 0. + * @return reward The amount of reward tokens claimed + */ + function claimReward( + IERC20Minimal rewardToken, + address to, + uint256 amountRequested + ) external returns (uint256 reward); + + /** + * @notice Calculates the reward amount that will be received for the given stake + * @param key The key of the incentive + * @param tokenId The ID of the token + * @return reward The reward accrued to the NFT for the given incentive thus far + */ + function getRewardInfo(IncentiveKey memory key, uint256 tokenId) + external + returns (uint256 reward, uint160 secondsInsideX128); + + /** + * @notice Event emitted when a liquidity mining incentive has been created + * @param rewardToken The token being distributed as a reward + * @param pool The Uniswap V3 pool + * @param startTime The time when the incentive program begins + * @param endTime The time when rewards stop accruing + * @param refundee The address which receives any remaining reward tokens after the end time + * @param reward The amount of reward tokens to be distributed + */ + event IncentiveCreated( + IERC20Minimal indexed rewardToken, + IUniswapV3Pool indexed pool, + uint256 startTime, + uint256 endTime, + address refundee, + uint256 reward + ); + + /** + * @notice Event that can be emitted when a liquidity mining incentive has ended + * @param incentiveId The incentive which is ending + * @param refund The amount of reward tokens refunded + */ + event IncentiveEnded(bytes32 indexed incentiveId, uint256 refund); + + /** + * @notice Emitted when ownership of a deposit changes + * @param tokenId The ID of the deposit (and token) that is being transferred + * @param oldOwner The owner before the deposit was transferred + * @param newOwner The owner after the deposit was transferred + */ + event DepositTransferred(uint256 indexed tokenId, address indexed oldOwner, address indexed newOwner); + + /** + * @notice Event emitted when a Uniswap V3 LP token has been staked + * @param tokenId The unique identifier of an Uniswap V3 LP token + * @param liquidity The amount of liquidity staked + * @param incentiveId The incentive in which the token is staking + */ + event TokenStaked(uint256 indexed tokenId, bytes32 indexed incentiveId, uint128 liquidity); + + /** + * @notice Event emitted when a Uniswap V3 LP token has been unstaked + * @param tokenId The unique identifier of an Uniswap V3 LP token + * @param incentiveId The incentive in which the token is staking + */ + event TokenUnstaked(uint256 indexed tokenId, bytes32 indexed incentiveId); + + /** + * @notice Event emitted when a reward token has been claimed + * @param to The address where claimed rewards were sent to + * @param reward The amount of reward tokens claimed + */ + event RewardClaimed(address indexed to, uint256 reward); +} diff --git a/contracts/optimism/connectors/uniswap/v3_staker/main.sol b/contracts/optimism/connectors/uniswap/v3_staker/main.sol new file mode 100644 index 00000000..016bd3c0 --- /dev/null +++ b/contracts/optimism/connectors/uniswap/v3_staker/main.sol @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.7.6; +pragma abicoder v2; + +/** + * @title Uniswap v3. + * @dev Decentralized Exchange. + */ + +import {TokenInterface} from "../../../common/interfaces.sol"; +import "./interface.sol"; +import {Helpers} from "./helpers.sol"; +import {Events} from "./events.sol"; + +abstract contract UniswapResolver is Helpers, Events { + /** + * @dev Deposit NFT token + * @notice Transfer deposited NFT token + * @param _tokenId NFT LP Token ID + */ + function deposit(uint256 _tokenId) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + if (_tokenId == 0) _tokenId = _getLastNftId(address(this)); + nftManager.safeTransferFrom( + address(this), + address(staker), + _tokenId, + "" + ); + + _eventName = "LogDeposit(uint256)"; + _eventParam = abi.encode(_tokenId); + } + + /** + * @dev Deposit and Stake NFT token + * @notice To Deposit and Stake NFT for Staking + * @param _rewardToken _rewardToken address + * @param _startTime stake start time + * @param _endTime stake end time + * @param _refundee refundee address + * @param _tokenId NFT LP token id + */ + function depositAndStake ( + address _rewardToken, + uint256 _startTime, + uint256 _endTime, + address _refundee, + uint256 _tokenId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + if (_tokenId == 0) _tokenId = _getLastNftId(address(this)); + nftManager.safeTransferFrom( + address(this), + address(staker), + _tokenId, + "" + ); + + address poolAddr = getPoolAddress(_tokenId); + + IUniswapV3Pool pool = IUniswapV3Pool(poolAddr); + IUniswapV3Staker.IncentiveKey memory _key = IUniswapV3Staker + .IncentiveKey( + IERC20Minimal(_rewardToken), + pool, + _startTime, + _endTime, + _refundee + ); + _stake(_tokenId, _key); + + _eventName = "LogDepositAndStake(uint256,bytes32)"; + _eventParam = abi.encode(_tokenId, keccak256(abi.encode(_key))); + } + + /** + * @dev Deposit Transfer + * @notice Transfer deposited NFT token + * @param _tokenId NFT LP Token ID + * @param _to address to transfer + */ + function transferDeposit(uint256 _tokenId, address _to) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + staker.transferDeposit(_tokenId, _to); + + _eventName = "LogDepositTransfer(uint256,address)"; + _eventParam = abi.encode(_tokenId, _to); + } + + /** + * @dev Withdraw NFT LP token + * @notice Withdraw NFT LP token from staking pool + * @param _tokenId NFT LP Token ID + */ + function withdraw(uint256 _tokenId) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + staker.withdrawToken(_tokenId, address(this), ""); + + _eventName = "LogWithdraw(uint256)"; + _eventParam = abi.encode(_tokenId); + } + + /** + * @dev Stake NFT LP token + * @notice Stake NFT LP Position + * @param _rewardToken _rewardToken address + * @param _startTime stake start time + * @param _endTime stake end time + * @param _refundee refundee address + * @param _tokenId NFT LP token id + */ + function stake ( + address _rewardToken, + uint256 _startTime, + uint256 _endTime, + address _refundee, + uint256 _tokenId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + address poolAddr = getPoolAddress(_tokenId); + + IUniswapV3Pool pool = IUniswapV3Pool(poolAddr); + IUniswapV3Staker.IncentiveKey memory _key = IUniswapV3Staker + .IncentiveKey( + IERC20Minimal(_rewardToken), + pool, + _startTime, + _endTime, + _refundee + ); + _stake(_tokenId, _key); + + _eventName = "LogStake(uint256,bytes32)"; + _eventParam = abi.encode(_tokenId, keccak256(abi.encode(_key))); + } + + /** + * @dev Unstake NFT LP token + * @notice Unstake NFT LP Position + * @param _rewardToken _rewardToken address + * @param _startTime stake start time + * @param _endTime stake end time + * @param _refundee refundee address + * @param _tokenId NFT LP token id + */ + function unstake( + address _rewardToken, + uint256 _startTime, + uint256 _endTime, + address _refundee, + uint256 _tokenId + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + address poolAddr = getPoolAddress(_tokenId); + + IUniswapV3Pool pool = IUniswapV3Pool(poolAddr); + IUniswapV3Staker.IncentiveKey memory _key = IUniswapV3Staker + .IncentiveKey( + IERC20Minimal(_rewardToken), + pool, + _startTime, + _endTime, + _refundee + ); + _unstake(_key, _tokenId); + _eventName = "LogUnstake(uint256,bytes32)"; + _eventParam = abi.encode(_tokenId, keccak256(abi.encode(_key))); + } + + /** + * @dev Claim rewards + * @notice Claim rewards + * @param _rewardToken _rewardToken address + * @param _amount requested amount + */ + function claimRewards( + address _rewardToken, + uint256 _amount + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + uint256 rewards = _claimRewards( + IERC20Minimal(_rewardToken), + address(this), + _amount + ); + + _eventName = "LogRewardClaimed(address,uint256)"; + _eventParam = abi.encode(_rewardToken, rewards); + } + + /** + * @dev Create incentive + * @notice Create incentive + * @param _rewardToken _rewardToken address + * @param _length incentive length + * @param _refundee refundee address + * @param _poolAddr Uniswap V3 Pool address + * @param _reward reward amount + */ + function createIncentive( + address _rewardToken, + uint256 _length, + address _refundee, + address _poolAddr, + uint256 _reward + ) + external + payable + returns (string memory _eventName, bytes memory _eventParam) + { + IUniswapV3Pool pool = IUniswapV3Pool(_poolAddr); + uint256 _startTime = block.timestamp; + uint256 _endTime = _startTime + _length; + IUniswapV3Staker.IncentiveKey memory _key = IUniswapV3Staker + .IncentiveKey( + IERC20Minimal(_rewardToken), + pool, + _startTime, + _endTime, + _refundee + ); + if (_rewardToken != ethAddr) { + IERC20Minimal(_rewardToken).approve(address(staker), _reward); + } + staker.createIncentive(_key, _reward); + + _eventName = "LogIncentiveCreated(bytes32,address,address,uint256,uint256,uint256)"; + _eventParam = abi.encode(keccak256(abi.encode(_key)), _poolAddr, _refundee, _startTime, _endTime, _reward); + } +} + +contract ConnectV2UniswapV3Staker is UniswapResolver { + string public constant name = "Uniswap-V3-Staker-v1.1"; +}