From f73f5dbdc0153bdbebd93aee06e6a829c1a43816 Mon Sep 17 00:00:00 2001 From: ryanycw Date: Tue, 4 Jul 2023 16:40:57 +0800 Subject: [PATCH] chore: all contracts from previous repo --- src/Counter.sol | 14 - src/UniStatusViewer.sol | 108 +++ src/UniVaultProxy.sol | 28 + src/UniVaultStorageV1.sol | 104 +++ src/UniVaultUpgradeableV1.sol | 422 +++++++++++ src/Zap.sol | 338 +++++++++ src/beacon/UniVaultBeacon.sol | 52 ++ .../UniVaultBeaconProxyImplementation.sol | 51 ++ src/factory/UniV3VaultFactory.sol | 150 ++++ src/inheritance/ControllableInit.sol | 29 + src/inheritance/GovernableInit.sol | 47 ++ src/inheritance/Storage.sol | 33 + src/interface/IController.sol | 6 + src/interface/IFeeRewardForwarderV6.sol | 9 + src/interface/IRewardPool.sol | 7 + src/interface/IUniVaultStorageV1.sol | 49 ++ src/interface/IUniVaultV1.sol | 42 ++ src/interface/IUniVaultV2.sol | 41 ++ src/interface/IUniversalLiquidator.sol | 27 + .../IUniversalLiquidatorRegistry.sol | 7 + src/interface/WETH9.sol | 12 + src/interface/oracle/IDexPriceAggregator.sol | 17 + src/interface/uniswap/IUniswapV3Pool.sol | 5 + src/interface/uniswap/IUniswapV3Staker.sol | 167 +++++ src/interface/uniswapV2/IUniswapV2Factory.sol | 15 + .../uniswapV2/IUniswapV2Migrator.sol | 6 + src/interface/uniswapV2/IUniswapV2Pair.sol | 54 ++ .../uniswapV2/IUniswapV2Router01.sol | 102 +++ .../uniswapV2/IUniswapV2Router02.sol | 146 ++++ src/lib/BytesLib.sol | 490 +++++++++++++ src/mock/BoredSwapper.sol | 59 ++ src/mock/PositionMinter.sol | 82 +++ src/mock/UniVaultSubModuleDepsoitV1Debug.sol | 641 +++++++++++++++++ src/mock/UniVaultUpgradeableV1Debug.sol | 415 +++++++++++ src/mock/ZapDebug.sol | 344 +++++++++ .../UniVaultSubModuleChangeRangeV1.sol | 267 +++++++ .../UniVaultSubModuleChangeRangeV2Managed.sol | 274 ++++++++ src/submodules/UniVaultSubModuleDepositV1.sol | 659 ++++++++++++++++++ .../UniVaultSubModuleDepositV2Managed.sol | 206 ++++++ ...UniVaultSubmoduleRewardV1LiquidateOnly.sol | 231 ++++++ src/submodules/interface/IUniStatusViewer.sol | 6 + .../IUniVaultSubmoduleChangeRangeV1.sol | 7 + ...IUniVaultSubmoduleChangeRangeV2Managed.sol | 8 + .../interface/IUniVaultSubmoduleDepositV1.sol | 60 ++ .../interface/IUniVaultSubmoduleDepositV2.sol | 43 ++ ...UniVaultSubmoduleRewardV1LiquidateOnly.sol | 10 + .../UniVaultUpgradeableV1SoleLPqfCR.sol | 409 +++++++++++ src/utility/UniVaultUpgradeableV1qfCR.sol | 345 +++++++++ 48 files changed, 6630 insertions(+), 14 deletions(-) delete mode 100644 src/Counter.sol create mode 100644 src/UniStatusViewer.sol create mode 100644 src/UniVaultProxy.sol create mode 100644 src/UniVaultStorageV1.sol create mode 100644 src/UniVaultUpgradeableV1.sol create mode 100644 src/Zap.sol create mode 100644 src/beacon/UniVaultBeacon.sol create mode 100644 src/beacon/UniVaultBeaconProxyImplementation.sol create mode 100644 src/factory/UniV3VaultFactory.sol create mode 100644 src/inheritance/ControllableInit.sol create mode 100644 src/inheritance/GovernableInit.sol create mode 100644 src/inheritance/Storage.sol create mode 100644 src/interface/IController.sol create mode 100644 src/interface/IFeeRewardForwarderV6.sol create mode 100644 src/interface/IRewardPool.sol create mode 100644 src/interface/IUniVaultStorageV1.sol create mode 100644 src/interface/IUniVaultV1.sol create mode 100644 src/interface/IUniVaultV2.sol create mode 100644 src/interface/IUniversalLiquidator.sol create mode 100644 src/interface/IUniversalLiquidatorRegistry.sol create mode 100644 src/interface/WETH9.sol create mode 100644 src/interface/oracle/IDexPriceAggregator.sol create mode 100644 src/interface/uniswap/IUniswapV3Pool.sol create mode 100644 src/interface/uniswap/IUniswapV3Staker.sol create mode 100644 src/interface/uniswapV2/IUniswapV2Factory.sol create mode 100644 src/interface/uniswapV2/IUniswapV2Migrator.sol create mode 100644 src/interface/uniswapV2/IUniswapV2Pair.sol create mode 100644 src/interface/uniswapV2/IUniswapV2Router01.sol create mode 100644 src/interface/uniswapV2/IUniswapV2Router02.sol create mode 100644 src/lib/BytesLib.sol create mode 100644 src/mock/BoredSwapper.sol create mode 100644 src/mock/PositionMinter.sol create mode 100644 src/mock/UniVaultSubModuleDepsoitV1Debug.sol create mode 100644 src/mock/UniVaultUpgradeableV1Debug.sol create mode 100644 src/mock/ZapDebug.sol create mode 100644 src/submodules/UniVaultSubModuleChangeRangeV1.sol create mode 100644 src/submodules/UniVaultSubModuleChangeRangeV2Managed.sol create mode 100644 src/submodules/UniVaultSubModuleDepositV1.sol create mode 100644 src/submodules/UniVaultSubModuleDepositV2Managed.sol create mode 100644 src/submodules/UniVaultSubmoduleRewardV1LiquidateOnly.sol create mode 100644 src/submodules/interface/IUniStatusViewer.sol create mode 100644 src/submodules/interface/IUniVaultSubmoduleChangeRangeV1.sol create mode 100644 src/submodules/interface/IUniVaultSubmoduleChangeRangeV2Managed.sol create mode 100644 src/submodules/interface/IUniVaultSubmoduleDepositV1.sol create mode 100644 src/submodules/interface/IUniVaultSubmoduleDepositV2.sol create mode 100644 src/submodules/interface/IUniVaultSubmoduleRewardV1LiquidateOnly.sol create mode 100644 src/utility/UniVaultUpgradeableV1SoleLPqfCR.sol create mode 100644 src/utility/UniVaultUpgradeableV1qfCR.sol diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/UniStatusViewer.sol b/src/UniStatusViewer.sol new file mode 100644 index 0000000..2aa8127 --- /dev/null +++ b/src/UniStatusViewer.sol @@ -0,0 +1,108 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma experimental ABIEncoderV2; + +import "hardhat/console.sol"; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// Harvest +import "./interface/IUniVaultV1.sol"; +import "./interface/IUniVaultStorageV1.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; + +// Uniswap v2 +import "./interface/uniswapV2/IUniswapV2Pair.sol"; +import "./interface/uniswapV2/IUniswapV2Factory.sol"; +import "./interface/uniswapV2/IUniswapV2Router02.sol"; + +contract UniStatusViewer { + using SafeMathUpgradeable for uint256; + + address constant _UNI_POS_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + address constant _V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address constant _V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + + function getSqrtPriceX96(address _token0, address _token1, uint24 _fee) public view returns (uint160) { + address poolAddr = IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(_token0, _token1, _fee); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } + + function getSqrtPriceX96ForPosition(uint256 posId) public view returns (uint160) { + (,, address _token0, address _token1, uint24 _fee,,,,,,,) = INonfungiblePositionManager(_UNI_POS_MANAGER).positions(posId); + return getSqrtPriceX96(_token0, _token1, _fee); + } + + function getAmountsForPosition(uint256 posId) public view returns (uint256 amount0, uint256 amount1) { + (,, address _token0, address _token1, uint24 _fee, int24 _tickLower, int24 _tickUpper, uint128 _liquidity,,,,) = + INonfungiblePositionManager(_UNI_POS_MANAGER).positions(posId); + uint160 sqrtRatioX96 = getSqrtPriceX96(_token0, _token1, _fee); + uint160 sqrtRatioXA96 = TickMath.getSqrtRatioAtTick(_tickLower); + uint160 sqrtRatioXB96 = TickMath.getSqrtRatioAtTick(_tickUpper); + (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtRatioX96, sqrtRatioXA96, sqrtRatioXB96, _liquidity); + } + + function getAmountsForUserShare(address vaultAddr, uint256 userShare) + public + view + returns (uint256 userAmount0, uint256 userAmount1) + { + uint256 posId = IUniVaultStorageV1(IUniVaultV1(vaultAddr).getStorage()).posId(); + (uint256 amount0, uint256 amount1) = getAmountsForPosition(posId); + userAmount0 = amount0.mul(userShare).div(IERC20Upgradeable(vaultAddr).totalSupply()); + userAmount1 = amount1.mul(userShare).div(IERC20Upgradeable(vaultAddr).totalSupply()); + } + + function enumerateNftRelevantTo(uint256 posId, address nftOwner) public view returns (uint256[] memory) { + (,, address _token0, address _token1,,,,,,,,) = INonfungiblePositionManager(_UNI_POS_MANAGER).positions(posId); + uint256 userNftCount = INonfungiblePositionManager(_UNI_POS_MANAGER).balanceOf(nftOwner); + uint256[] memory ids = new uint256[](userNftCount); + + uint256 validCount = 0; + for (uint256 i = 0; i < userNftCount; i++) { + uint256 id = INonfungiblePositionManager(_UNI_POS_MANAGER).tokenOfOwnerByIndex(nftOwner, i); + + (,, address _utoken0, address _utoken1,,,,,,,,) = INonfungiblePositionManager(_UNI_POS_MANAGER).positions(posId); + if (_utoken0 == _token0 && _utoken1 == _token1) { + ids[validCount] = id; + validCount++; + } + } + + uint256[] memory relevantIds = new uint256[](validCount); + for (uint256 i = 0; i < validCount; i++) { + relevantIds[i] = ids[i]; + } + return relevantIds; + } + + /** + * Returns the number of tokens that would be returned now when removing the liquidity + * from the corresponding v2 pair + */ + function quoteV2Migration(address _token0, address _token1, uint256 _lpAmount) public view returns (uint256, uint256) { + address pair = IUniswapV2Factory(_V2_FACTORY).getPair(_token0, _token1); + (uint112 amount0, uint112 amount1,) = IUniswapV2Pair(pair).getReserves(); + uint256 totalSupply = IERC20Upgradeable(pair).totalSupply(); + if (_token0 == IUniswapV2Pair(pair).token0()) { + // order matches v3 + return (_lpAmount.mul(uint256(amount0)).div(totalSupply), _lpAmount.mul(uint256(amount1)).div(totalSupply)); + } else { + // order is reversed + return (_lpAmount.mul(uint256(amount1)).div(totalSupply), _lpAmount.mul(uint256(amount0)).div(totalSupply)); + } + } +} diff --git a/src/UniVaultProxy.sol b/src/UniVaultProxy.sol new file mode 100644 index 0000000..f089901 --- /dev/null +++ b/src/UniVaultProxy.sol @@ -0,0 +1,28 @@ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/proxy/UpgradeableProxy.sol"; +import "./interface/IUniVaultV1.sol"; + +contract UniVaultProxy is UpgradeableProxy { + constructor(address _logic, bytes memory _data) UpgradeableProxy(_logic, _data) {} + + /** + * The main logic. If the timer has elapsed and there is a schedule upgrade, + * the governance can upgrade the vault + */ + function upgrade() external { + (bool should, address newImplementation) = IUniVaultV1(address(this)).shouldUpgrade(); + require(should, "Upgrade not scheduled"); + _upgradeTo(newImplementation); + + // the finalization needs to be executed on itself to update the storage of this proxy + // it also needs to be invoked by the governance, not by address(this), so delegatecall is needed + (bool success, bytes memory result) = address(this).delegatecall(abi.encodeWithSignature("finalizeUpgrade()")); + + require(success, "Issue when finalizing the upgrade"); + } + + function implementation() external view returns (address) { + return _implementation(); + } +} diff --git a/src/UniVaultStorageV1.sol b/src/UniVaultStorageV1.sol new file mode 100644 index 0000000..60fb6bc --- /dev/null +++ b/src/UniVaultStorageV1.sol @@ -0,0 +1,104 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; + +contract UniVaultStorageV1 { + uint256 public posId; + address public token0; + address public token1; + address public zapContract; + address public vault; + address public feeRewardForwarder; + uint256 public profitShareRatio; + address public platformTarget; + uint256 public platformRatio; + + int24 public tickUpper; + int24 public tickLower; + uint24 public fee; + address public nextImplementation; + uint256 public nextImplementationTimestamp; + uint256 public nextImplementationDelay; + + modifier onlyVault() { + require(msg.sender == vault, "Only vault"); + _; + } + + constructor(address _vault) { + vault = _vault; + } + + function initializeByVault( + address _token0, + address _token1, + uint24 _fee, + int24 _tickLower, + int24 _tickUpper, + address _zapContract + ) public onlyVault { + token0 = _token0; + token1 = _token1; + fee = _fee; + tickLower = _tickLower; + tickUpper = _tickUpper; + zapContract = _zapContract; + } + + function setZapContract(address _zap) public onlyVault { + zapContract = _zap; + } + + function setToken0(address _token) public onlyVault { + token0 = _token; + } + + function setToken1(address _token) public onlyVault { + token1 = _token; + } + + function setFee(uint24 _fee) public onlyVault { + fee = _fee; + } + + function setTickUpper(int24 _tick) public onlyVault { + tickUpper = _tick; + } + + function setTickLower(int24 _tick) public onlyVault { + tickLower = _tick; + } + + function setPosId(uint256 _posId) public onlyVault { + posId = _posId; + } + + function setVault(address _vault) public onlyVault { + vault = _vault; + } + + function setFeeRewardForwarder(address _feeRewardForwarder) public onlyVault { + feeRewardForwarder = _feeRewardForwarder; + } + + function configureFees(address _feeRewardForwarder, uint256 _feeRatio, address _platformTarget, uint256 _platformRatio) + public + onlyVault + { + feeRewardForwarder = _feeRewardForwarder; + profitShareRatio = _feeRatio; + platformTarget = _platformTarget; + platformRatio = _platformRatio; + } + + function setNextImplementation(address _impl) public onlyVault { + nextImplementation = _impl; + } + + function setNextImplementationTimestamp(uint256 _timestamp) public onlyVault { + nextImplementationTimestamp = _timestamp; + } + + function setNextImplementationDelay(uint256 _delay) public onlyVault { + nextImplementationDelay = _delay; + } +} diff --git a/src/UniVaultUpgradeableV1.sol b/src/UniVaultUpgradeableV1.sol new file mode 100644 index 0000000..7a09189 --- /dev/null +++ b/src/UniVaultUpgradeableV1.sol @@ -0,0 +1,422 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// ERC721 +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +// UniswapV3 staker +import "./interface/uniswap/IUniswapV3Staker.sol"; + +// reentrancy guard +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; + +// Harvest System +import "./inheritance/ControllableInit.sol"; + +// Storage for this UniVault +import "./interface/IUniVaultStorageV1.sol"; + +// BytesLib +import "./lib/BytesLib.sol"; + +contract UniVaultUpgradeableV1 is ERC20Upgradeable, ERC721HolderUpgradeable, ReentrancyGuardUpgradeable, ControllableInit { + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + using BytesLib for bytes; + + bytes32 internal constant _DATA_CONTRACT_SLOT = 0x4c2252f3318958b38b23790562cc9d391075b8dadbfe0e707aed11afe13228b7; + address internal constant _NFT_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + uint256 internal constant _UNDERLYING_UNIT = 1e18; + address internal constant _V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address internal constant _V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + address internal constant _UNI_STAKER = 0x1f98407aaB862CdDeF78Ed252D6f557aA5b0f00d; + + bytes32 internal constant _VAULT_PAUSE_SLOT = 0xde039a7c768eade9368187c932ab7b9ca8d5872604278b5ebfe45ab5eaf85140; + + event SwapFeeClaimed(uint256 token0Fee, uint256 token1Fee, uint256 timestamp); + event Deposit(address indexed user, uint256 token0Amount, uint256 token1Amount); + event Withdraw(address indexed user, uint256 token0Amount, uint256 token1Amount); + + /** + * @dev Ensures that the price between submitting the transaction and when + * the transaction gets mined did not move (checks slippage) + */ + modifier checkSqrtPriceX96(uint256 offChainSqrtPriceX96, uint256 tolerance) { + uint256 current = uint256(getSqrtPriceX96()); + uint256 step = offChainSqrtPriceX96.mul(tolerance).div(1000); + require(current < offChainSqrtPriceX96.add(step), "Price too high"); + require(current > offChainSqrtPriceX96.sub(step), "Price too low"); + _; + } + + modifier checkVaultNotPaused() { + require(!getBoolean(_VAULT_PAUSE_SLOT), "Vault is paused"); + _; + } + + modifier onlySelf() { + require(msg.sender == address(this), "only for internal"); + _; + } + + bytes32 internal constant _GLOBAL_STORAGE_SLOT = 0xea3b316d3f7b97449bd56fdd0c7f95b3a874cb501e4c5d85e01bf23443a180c8; + + struct GlobalStorage { + // direct mapping from function signature to submodule address + mapping(bytes4 => address) functionToSubmodule; + // Mapping from submodule hash to submodule, this is used for internal delegate calls as in doHardwork + // where we know what submodules we are calling, but those functions are not open to the public + mapping(bytes32 => address) submoduleAddress; + } + + // functions are tied to a submodule + // this allows us to update a submodule without updating all the function signature to submodule + function functionHandler(bytes4 functionSignature) public returns (address submodule) { + GlobalStorage storage globalStorage = getGlobalStorage(); + return globalStorage.functionToSubmodule[functionSignature]; + } + + function getGlobalStorage() internal pure returns (GlobalStorage storage globalStorage) { + assembly { + globalStorage.slot := _GLOBAL_STORAGE_SLOT + } + } + + /** + * @dev Creates the contract and ensures that the slot matches the hash + */ + constructor() { + assert(_DATA_CONTRACT_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.dataContract")) - 1)); + assert(_VAULT_PAUSE_SLOT == bytes32(uint256(keccak256("eip1967.univault.pause")) - 1)); + assert(_GLOBAL_STORAGE_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.globalStorage")) - 1)); + } + + function vaultPaused() public returns (bool) { + return getBoolean(_VAULT_PAUSE_SLOT); + } + + function setVaultPause(bool _flag) public onlyGovernance { + return setBoolean(_VAULT_PAUSE_SLOT, _flag); + } + + function initializeEmpty() public initializer {} + + /** + * @dev Initializes the contract. Used by the proxy pattern. + */ + function initialize(uint256 _tokenId, address _dataContract, address _storage, address _zapContract) public initializer { + // initializing the governance for the contract + ControllableInit.initialize(_storage); + // sets data storage contract to the right slot and initializes it + setAddress(_DATA_CONTRACT_SLOT, _dataContract); + + IERC721Upgradeable(_NFT_POSITION_MANAGER).transferFrom(msg.sender, address(this), _tokenId); + + // fetch info from nft. + (,, address _token0, address _token1, uint24 _fee, int24 _tickLower, int24 _tickUpper, uint256 _initialLiquidity,,,,) = + INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(_tokenId); + + IUniVaultStorageV1(_dataContract).initializeByVault(_token0, _token1, _fee, _tickLower, _tickUpper, _zapContract); + // initializing the vault token. By default, it has 18 decimals. + __ERC20_init_unchained( + string(abi.encodePacked("fUniV3_", ERC20Upgradeable(_token0).symbol(), "_", ERC20Upgradeable(_token1).symbol())), + string(abi.encodePacked("fUniV3_", ERC20Upgradeable(_token0).symbol(), "_", ERC20Upgradeable(_token1).symbol())) + ); + + // set the NFT ID in the data contract + getStorage().setPosId(_tokenId); + // mint the initial shares and send them to the specified recipient, as well as overflow of the tokens + + _mint(msg.sender, uint256(_initialLiquidity)); + } + + function configureVault( + address submoduleDeposit, + address _feeRewardForwarder, + uint256 _feeRatio, + address _platformTarget, + uint256 _platformRatio + ) public onlyGovernance { + getStorage().configureFees(_feeRewardForwarder, _feeRatio, _platformTarget, _platformRatio); + + setSubmodule(keccak256("deposit"), submoduleDeposit); + } + + /** + * Does the hard work. Only whitelisted chads. + */ + function doHardWork() external onlyControllerOrGovernance { + address rewardSubmodule = submoduleAddress(keccak256("reward")); + address depositSubmodule = submoduleAddress(keccak256("deposit")); + bool success; + bytes memory data; + string memory revertMsg; + // Reward: Pre-process & Claiming & Liquidate + if (rewardSubmodule != address(0)) { + (success, data) = (rewardSubmodule).delegatecall(abi.encodeWithSignature("doHardwork_preprocessForReward()")); + revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + + // Deposit submodule collect fees & zap & increase liquidity : compoundRewards() + // Does not emit the APR event + (success, data) = (depositSubmodule).delegatecall(abi.encodeWithSignature("compoundRewards()")); + revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + + // Deposit submodule collect fees & zap & increase liquidity : collectFeesAndCompound() + // Does not emits the APR event + (success, data) = (depositSubmodule).delegatecall(abi.encodeWithSignature("collectFeesAndCompound()")); + revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + + // Reward: Post-process + if (rewardSubmodule != address(0)) { + (success, data) = (rewardSubmodule).delegatecall(abi.encodeWithSignature("doHardwork_postprocessForReward()")); + revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + } + + /** + * @dev Returns the total liquidity stored in the position + * Dev Note: need to turn on the solc optimizer otherwise the compiler + * will throw stack too deep on this function + */ + function underlyingBalanceWithInvestment() public view returns (uint256) { + // note that the liquidity is not a token, so there is no local balance added + (,,,,,,, uint128 liquidity,,,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(getStorage().posId()); + return liquidity; + } + + /** + * @dev A dummy method to get the controller working + */ + function setStrategy(address unused) public onlyControllerOrGovernance { + // intentionally a no-op + } + + /** + * @dev A dummy method to get the controller working + */ + function strategy() public view returns (address) { + return address(this); + } + + /** + * @dev Sets the Zap contract + */ + function setZapContract(address newAddress) public onlyGovernance { + getStorage().setZapContract(newAddress); + } + + /** + * @dev Returns the price per full share, scaled to 1e18 + */ + function getPricePerFullShare() public view returns (uint256) { + return totalSupply() == 0 ? _UNDERLYING_UNIT : _UNDERLYING_UNIT.mul(underlyingBalanceWithInvestment()).div(totalSupply()); + } + + /** + * @dev Convenience getter for the data contract. + */ + function getStorage() public view returns (IUniVaultStorageV1) { + return IUniVaultStorageV1(getAddress(_DATA_CONTRACT_SLOT)); + } + + /** + * @dev Convenience getter for token0. + */ + function token0() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token0()); + } + + /** + * @dev Convenience getter for token1. + */ + function token1() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token1()); + } + + /** + * @dev Convenience getter for the current sqrtPriceX96 of the Uniswap pool. + */ + function getSqrtPriceX96() public view returns (uint160) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } + + /** + * @dev Sets an boolean to a slot. + */ + function setBoolean(bytes32 slot, bool _flag) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _flag) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getBoolean(bytes32 slot) internal view returns (bool str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Sets an address to a slot. + */ + function setAddress(bytes32 slot, address _address) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _address) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getAddress(bytes32 slot) internal view returns (address str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * V1 Data contract & Upgradeability + */ + + // Keeping this for the legacy interface + /** + * @dev Configure fees + * @param _feeRewardForwarder the address of the fee reward forwarder + * @param _feeRatio the profit sharing fee percentage (100% = 10000) + * @param _platformTarget the address for gas fees contributions + * @param _platformRatio the percentage for gas fee contributions (100% = 10000) + */ + function configureFees(address _feeRewardForwarder, uint256 _feeRatio, address _platformTarget, uint256 _platformRatio) + external + onlyGovernance + { + getStorage().configureFees(_feeRewardForwarder, _feeRatio, _platformTarget, _platformRatio); + } + + /** + * @dev Schedules an upgrade to the new implementation for the proxy. + */ + function scheduleUpgrade(address impl) public onlyGovernance { + getStorage().setNextImplementation(impl); + getStorage().setNextImplementationTimestamp(getStorage().nextImplementationDelay().add(block.timestamp)); + } + + /** + * @dev Tells the proxy if an upgrade is scheduled and the timelock elapsed. + */ + function shouldUpgrade() external view returns (bool, address) { + return ( + getStorage().nextImplementationTimestamp() != 0 && block.timestamp > getStorage().nextImplementationTimestamp() + && getStorage().nextImplementation() != address(0), + getStorage().nextImplementation() + ); + } + + /** + * @dev Completes the upgrade and resets the upgrade state to no upgrade. + */ + function finalizeUpgrade() external onlyGovernance { + getStorage().setNextImplementation(address(0)); + getStorage().setNextImplementationTimestamp(0); + + getStorage().setZapContract(0x9B47D39cb1b02Bb77A517fEA39963534a6243Df3); + setSubmodule(keccak256("deposit"), 0x5736fDdd7E421B7608B5418a190A5d938b849CeB); + } + + /** + * @dev Increases the upgrade timelock. Governance only. + */ + function increaseUpgradeDelay(uint256 delay) external onlyGovernance { + getStorage().setNextImplementationDelay(delay.add(getStorage().nextImplementationDelay())); + } + + function submoduleAddress(bytes32 submoduleHash) public view returns (address) { + GlobalStorage storage globalStorage = getGlobalStorage(); + address currentSubmodule = globalStorage.submoduleAddress[submoduleHash]; + return currentSubmodule; + } + + /* + When we set a submodule, the mother vault deregisters the interface + from the previous submodule that is registered under the hash, + + it registers the new submodule under the hash, + then registers the interface for the new submodule + */ + function setSubmodule(bytes32 submoduleHash, address submoduleAddr) public onlyGovernance { + GlobalStorage storage globalStorage = getGlobalStorage(); + address oldSubmodule = globalStorage.submoduleAddress[submoduleHash]; + + // if there's an old submodule, deregisters its interface + if (oldSubmodule != address(0)) { + (bool success, bytes memory data) = + (oldSubmodule).delegatecall(abi.encodeWithSignature("registerInterface(bytes32,bool)", submoduleHash, false)); + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + + // register the submodule hash + globalStorage.submoduleAddress[submoduleHash] = submoduleAddr; + + // register interface from new submodule + (bool success, bytes memory data) = + (submoduleAddr).delegatecall(abi.encodeWithSignature("registerInterface(bytes32,bool)", submoduleHash, true)); + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + + function version() public view returns (string memory) { + return "1.1.0"; + } + + fallback() external payable { + address targetSubmodule = functionHandler(msg.sig); + if (targetSubmodule != address(0)) { + // code cloned from EIP-2535 + assembly { + // copy function selector and any arguments + calldatacopy(0, 0, calldatasize()) + // execute function call using the targetSubmodule + let result := delegatecall(gas(), targetSubmodule, 0, calldatasize(), 0, 0) + // get any return value + returndatacopy(0, 0, returndatasize()) + // return any return value or error back to the caller + switch result + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } else { + revert("msg.sig is not assigned to submodule"); + } + } +} diff --git a/src/Zap.sol b/src/Zap.sol new file mode 100644 index 0000000..139f1bb --- /dev/null +++ b/src/Zap.sol @@ -0,0 +1,338 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// ERC721 +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; + +// Harvest System +import "./inheritance/GovernableInit.sol"; + +// Storage for this UniVault +import "./UniVaultStorageV1.sol"; + +contract Zap { + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + + bytes32 internal constant _DATA_CONTRACT_SLOT = 0x4c2252f3318958b38b23790562cc9d391075b8dadbfe0e707aed11afe13228b7; + address internal constant _SWAP_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + uint128 internal constant _LIQUIDITY_CONSTANT = 1e18; + + /** + * @dev Constructs the contract and asserts the right slot for the data contract. + * This slot has to match the slot in the vault, otherwise the delegatecalls + * will not be able to finds it. + */ + constructor() { + assert(_DATA_CONTRACT_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.dataContract")) - 1)); + } + + /** + * @dev Convenience getter for the current sqrtPriceX96 of the Uniswap pool. + */ + function getSqrtPriceX96() public view returns (uint160) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } + + /** + * @dev Zap takes in what users provided and calculates an approximately balanced amount for + * liquidity provision. It then swaps one token to another to make the tokens balanced. + * The safeguarding against slippage is left to the caller. + * @param _originalAmount0 the current amount for token0 available for zapping + * @param _originalAmount1 the current amount for token1 available for zapping + */ + function zap( + uint160 _sqrtPriceLimitX96, + uint256 _originalAmount0, + uint256 _originalAmount1, + uint256 _amount0OutMin, + uint256 _amount1OutMin + ) public returns (uint256, uint256) { + require(_originalAmount0 > 0 || _originalAmount1 > 0, "Cannot be both 0"); + if (_originalAmount0 == 0) { + return _zapZero0(_originalAmount1, _amount0OutMin, _sqrtPriceLimitX96); + } + if (_originalAmount1 == 0) { + return _zapZero1(_originalAmount0, _amount1OutMin, _sqrtPriceLimitX96); + } + + uint160 sqrtRatioX96 = getSqrtPriceX96(); // current price + uint160 sqrtRatioXA96 = TickMath.getSqrtRatioAtTick(getStorage().tickLower()); // "price" at the lower tick + uint160 sqrtRatioXB96 = TickMath.getSqrtRatioAtTick(getStorage().tickUpper()); // "price" at the upper tick + + if (sqrtRatioX96 <= sqrtRatioXA96) { + // we are below the low range, swap amount1 to token0 + // none of the amounts is 0, because the execution would be transferred + return swap(_sqrtPriceLimitX96, 0, _originalAmount1, 0, _amount1OutMin); + } else if (sqrtRatioX96 >= sqrtRatioXB96) { + // we are above range, swapping token0 to token1 + // none of the amounts is 0, because the execution would be transferred + return swap(_sqrtPriceLimitX96, _originalAmount0, 0, _amount0OutMin, 0); + } + + // we are within the range + uint128 liquidity0 = LiquidityAmounts.getLiquidityForAmount0(sqrtRatioX96, sqrtRatioXB96, _originalAmount0); + uint128 liquidity1 = LiquidityAmounts.getLiquidityForAmount1(sqrtRatioXA96, sqrtRatioX96, _originalAmount1); + + if (liquidity0 < liquidity1) { + // we are swapping token1 for token0 + return swapSufficientAmountOfToken1( + liquidity0, [sqrtRatioX96, sqrtRatioXA96, sqrtRatioXB96, _sqrtPriceLimitX96], _originalAmount1, _amount0OutMin + ); + } else { + // we are swapping token0 for token1 + return swapSufficientAmountOfToken0( + liquidity1, [sqrtRatioX96, sqrtRatioXA96, sqrtRatioXB96, _sqrtPriceLimitX96], _originalAmount0, _amount1OutMin + ); + } + } + + function swapSufficientAmountOfToken1( + uint128 liquidity0, + uint160[4] memory ratios, + uint256 _originalAmount1, + uint256 _amount0OutMin + ) internal returns (uint256, uint256) { + // obtains the ratio of assets across the entire uniswap liquidity + // the ratio will be used as a spot price for expressing the amount of + // token1 that came into the swap in the units of token0 + (uint256 amountForPrice0, uint256 amountForPrice1) = LiquidityAmounts.getAmountsForLiquidity( + ratios[0], + TickMath.getSqrtRatioAtTick(TickMath.MIN_TICK), + TickMath.getSqrtRatioAtTick(TickMath.MAX_TICK), + _LIQUIDITY_CONSTANT + ); + + // calculate how much of each token we actually need to get the liquidity + // the rest of the token1 amount will be treated as excess and swapped to token0 + // if there is no token0, so liquidity0 == 0, we still need some ratio + // for scaling the excess based on amount1/amount0, so we use _LIQUIDITY_CONSTANT + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + ratios[0], ratios[1], ratios[2], liquidity0 == 0 ? _LIQUIDITY_CONSTANT : liquidity0 + ); + + uint256 _leftover1; + if (liquidity0 == 0) { + // we have no token0, so the entire amount of token1 should be used for swapping + _leftover1 = _originalAmount1; + } else { + // we have and need amount0 of token0, this will be matched by amount1 of token1 + // the rest of token1 is _leftover1 and it will be swapped to the proper rate + // we know that _originalAmount1 >= amount1 because the caller checks + // that liquidity0 < liquidity1 + _leftover1 = _originalAmount1.sub(amount1); + } + + // converts the amount of token0 into the common unit of token1 + uint256 _amount0InToken1 = amount0.mul(amountForPrice1).div(amountForPrice0); + // calculates how much of the token1 excess should stay in token1 + uint256 toKeepInToken1 = _leftover1.mul(amount1).div(_amount0InToken1.add(amount1)); + // swaps the excess of token1 that should be converted to token0 + return swap(ratios[3], 0, _leftover1.sub(toKeepInToken1), _amount0OutMin, 0); + } + + function swapSufficientAmountOfToken0( + uint128 liquidity1, + uint160[4] memory ratios, + uint256 _originalAmount0, + uint256 _amount1OutMin + ) internal returns (uint256, uint256) { + // obtains the ratio of assets across the entire uniswap liquidity + // the ratio will be used as a spot price for expressing the amount of + // token0 that came into the swap in the units of token1 + (uint256 amountForPrice0, uint256 amountForPrice1) = LiquidityAmounts.getAmountsForLiquidity( + ratios[0], + TickMath.getSqrtRatioAtTick(TickMath.MIN_TICK), + TickMath.getSqrtRatioAtTick(TickMath.MAX_TICK), + _LIQUIDITY_CONSTANT + ); + + // calculate how much of each token we actually need to get the liquidity + // the rest of the token0 amount will be treated as excess and swapped to token1 + // if there is no token1, so liquidity1 == 0, we still need some ratio + // for scaling the excess based on amount1/amount0, so we use _LIQUIDITY_CONSTANT + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + ratios[0], ratios[1], ratios[2], liquidity1 == 0 ? _LIQUIDITY_CONSTANT : liquidity1 + ); + uint256 _leftover0; + if (liquidity1 == 0) { + // we have no token1, so the entire amount of token0 should be used for swapping + _leftover0 = _originalAmount0; + } else { + // we have and need amount1 of token1, this will be matched by amount0 of token0 + // the rest of token0 is _leftover0 and it will be swapped to the proper rate + // we know that _originalAmount0 >= amount0 because the caller checks + // that liquidity1 > liquidity0 + _leftover0 = _originalAmount0.sub(amount0); + } + + // converts the amount of token1 into the common unit of token0 + uint256 _amount1InToken0 = amount1.mul(amountForPrice0).div(amountForPrice1); + // calculates how much of the token0 excess should stay in token0 + uint256 toKeepInToken0 = _leftover0.mul(amount0).div(_amount1InToken0.add(amount0)); + // swaps the excess of token0 that should be converted to token1 + return swap(ratios[3], _leftover0.sub(toKeepInToken0), 0, 0, _amount1OutMin); + } + + // for compatibility + function zap(uint256 _originalAmount0, uint256 _originalAmount1) external { + zap(0, _originalAmount0, _originalAmount1, 0, 0); + } + + // for compatibility + function swap(uint256 _amount0In, uint256 _amount1In) external { + swap(0, _amount0In, _amount1In, 0, 0); + } + + /** + * @dev Swaps token0 amount for token1 or the other way round. One of the parameters + * always has to be 0. The safeguarding against slippage is left to the caller. + * @param _amount0In the amount to swap for token1 + * @param _amount1In the amount to swap for token0 + */ + function swap(uint160 _sqrtPriceLimitX96, uint256 _amount0In, uint256 _amount1In, uint256 _amount0Out, uint256 _amount1Out) + public + returns (uint256, uint256) + { + require(_amount1In == 0 || _amount0In == 0, "Choose one token"); + address tokenIn; + address tokenOut; + uint256 amountIn; + uint256 amountOut; + if (_amount0In == 0) { + tokenIn = getStorage().token1(); + tokenOut = getStorage().token0(); + amountIn = _amount1In; + amountOut = _amount0Out; + } else { + tokenIn = getStorage().token0(); + tokenOut = getStorage().token1(); + amountIn = _amount0In; + amountOut = _amount1Out; + } + + IERC20Upgradeable(tokenIn).safeApprove(_SWAP_ROUTER, 0); + IERC20Upgradeable(tokenIn).safeApprove(_SWAP_ROUTER, amountIn); + uint256 actualAmountOut = ISwapRouter(_SWAP_ROUTER).exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: tokenIn, + tokenOut: tokenOut, + fee: getStorage().fee(), + recipient: address(this), + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: amountOut, + sqrtPriceLimitX96: _sqrtPriceLimitX96 + }) + ); + + if (_amount0In == 0) { + return (actualAmountOut, 0); + } + return (0, actualAmountOut); + } + + /** + * @dev Handles zap for the case when we have 0 token0s + */ + function _zapZero0(uint256 _originalAmount1, uint256 _amount0OutMin, uint160 _sqrtPriceLimitX96) + internal + returns (uint256, uint256) + { + uint160 sqrtRatioX96 = getSqrtPriceX96(); + uint160 sqrtRatioXA96 = TickMath.getSqrtRatioAtTick(getStorage().tickLower()); + uint160 sqrtRatioXB96 = TickMath.getSqrtRatioAtTick(getStorage().tickUpper()); + + if (sqrtRatioX96 <= sqrtRatioXA96) { + // we are below the low range, swap _originalAmount1 to token0 + // the amount is not 0, because this was checked upstream + return swap(_sqrtPriceLimitX96, 0, _originalAmount1, _amount0OutMin, 0); + } else if (sqrtRatioX96 >= sqrtRatioXB96) { + // we are above range, should be swapping token0 to token1, but we do not have any + // so this is a no-op + return (0, 0); + } + + return swapSufficientAmountOfToken1( + 0, [sqrtRatioX96, sqrtRatioXA96, sqrtRatioXB96, _sqrtPriceLimitX96], _originalAmount1, _amount0OutMin + ); + } + + /** + * @dev Handles zap for the case when we have 0 token1s + */ + function _zapZero1(uint256 _originalAmount0, uint256 _amount1OutMin, uint160 _sqrtPriceLimitX96) + internal + returns (uint256, uint256) + { + uint160 sqrtRatioX96 = getSqrtPriceX96(); + uint160 sqrtRatioXA96 = TickMath.getSqrtRatioAtTick(getStorage().tickLower()); + uint160 sqrtRatioXB96 = TickMath.getSqrtRatioAtTick(getStorage().tickUpper()); + + if (sqrtRatioX96 <= sqrtRatioXA96) { + return (0, 0); + // we are below the low range, swap amount1 to token0 but that amount is 0 + // this is a no-op + } else if (sqrtRatioX96 >= sqrtRatioXB96) { + // the amount is not 0, because this was checked upstream + return swap(_sqrtPriceLimitX96, _originalAmount0, 0, 0, _amount1OutMin); + } + + return swapSufficientAmountOfToken0( + 0, [sqrtRatioX96, sqrtRatioXA96, sqrtRatioXB96, _sqrtPriceLimitX96], _originalAmount0, _amount1OutMin + ); + } + + /** + * @dev Convenience getter for the data contract. + */ + function getStorage() public view returns (UniVaultStorageV1) { + return UniVaultStorageV1(getAddress(_DATA_CONTRACT_SLOT)); + } + + /** + * @dev Convenience getter for token0. + */ + function token0() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token0()); + } + + /** + * @dev Convenience getter for token1. + */ + function token1() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token1()); + } + + /** + * @dev Reads an address from a slot. + */ + function getAddress(bytes32 slot) internal view returns (address str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } +} diff --git a/src/beacon/UniVaultBeacon.sol b/src/beacon/UniVaultBeacon.sol new file mode 100644 index 0000000..db18b7f --- /dev/null +++ b/src/beacon/UniVaultBeacon.sol @@ -0,0 +1,52 @@ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/proxy/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +import "@openzeppelin/contracts/utils/EnumerableSet.sol"; + +contract UniVaultBeacon is UpgradeableBeacon { + using EnumerableSet for EnumerableSet.AddressSet; + using SafeMathUpgradeable for uint256; + + uint256 public nextImplementationDelay = 0; + mapping(address => uint256) public whitelistedImplementationStartTime; + EnumerableSet.AddressSet private whitelistedImplementationSet; + + event ImplementationAdded(address newImplementation, uint256 startTime); + + constructor() public UpgradeableBeacon(0xe41E27CD5c99bf93466fCe3F797cf038efc3C37d) { + // all the existing implementations + whitelistedImplementationSet.add(0x3833b631B454AE659a2Ca11104854823009969D4); + whitelistedImplementationSet.add(0xdd40f85251AAAB470EE93AC445DE52F512235Caf); + whitelistedImplementationSet.add(0x392a5c02635DCDBd4C945785Ce530a9a69DDA6c2); + whitelistedImplementationSet.add(0xbF1ca4bab407d5A9FcA67D0aF93F4f37C155dE21); + whitelistedImplementationSet.add(0xe41E27CD5c99bf93466fCe3F797cf038efc3C37d); + } + + function upgradeTo(address newImplementation) public override onlyOwner { + require(whitelistedImplementationSet.contains(newImplementation), "implementation not whitelisted"); + require(block.timestamp >= whitelistedImplementationStartTime[newImplementation], "timelock not expired"); + super.upgradeTo(newImplementation); + } + + function addImplementation(address newImplementation) public onlyOwner { + require(Address.isContract(newImplementation), "UpgradeableBeacon: implementation is not a contract"); + whitelistedImplementationSet.add(newImplementation); + uint256 startTime = nextImplementationDelay.add(block.timestamp); + whitelistedImplementationStartTime[newImplementation] = startTime; + emit ImplementationAdded(newImplementation, startTime); + } + + function increaseUpgradeDelay(uint256 delay) external onlyOwner { + nextImplementationDelay = nextImplementationDelay.add(delay); + } + + function whitelistedImplementationsLength() public view returns (uint256) { + return whitelistedImplementationSet.length(); + } + + function whitelistedImplementations(uint256 index) public view returns (address) { + return whitelistedImplementationSet.at(index); + } +} diff --git a/src/beacon/UniVaultBeaconProxyImplementation.sol b/src/beacon/UniVaultBeaconProxyImplementation.sol new file mode 100644 index 0000000..26e9e02 --- /dev/null +++ b/src/beacon/UniVaultBeaconProxyImplementation.sol @@ -0,0 +1,51 @@ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/proxy/BeaconProxy.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "../lib/BytesLib.sol"; + +contract UniVaultBeaconProxyImplementation is BeaconProxy { + address public constant BEACON_ADDRESS = 0x0f42336E988Ba07d5c6c1B686844208A2Dec46Ad; + + using BytesLib for bytes; + + constructor() BeaconProxy(BEACON_ADDRESS, "") {} + + /** + * @dev Initializes the contract. Used by the proxy pattern. + */ + function initialize( + uint256, // _tokenId, + address, // _dataContract, + address, // _storage, + address // _zapContract + ) public { + _setBeacon(BEACON_ADDRESS, ""); + (bool success, bytes memory data) = (_implementation()).delegatecall(msg.data); + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + + /** + * @dev Initializes the contract (for initializing the implementation) + */ + function initializeEmpty() public { + _setBeacon(BEACON_ADDRESS, ""); + (bool success, bytes memory data) = (_implementation()).delegatecall(msg.data); + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + + /** + * @dev sets the beacon address to the current beacon + * this needs to happen once for each vault + * once the beacon is set, calling finalizeUpgrade() + * on the beacon's current implementation + */ + function finalizeUpgrade() external { + _setBeacon(BEACON_ADDRESS, ""); + (bool success, bytes memory data) = (_implementation()).delegatecall(msg.data); + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } +} diff --git a/src/factory/UniV3VaultFactory.sol b/src/factory/UniV3VaultFactory.sol new file mode 100644 index 0000000..ae4a1e1 --- /dev/null +++ b/src/factory/UniV3VaultFactory.sol @@ -0,0 +1,150 @@ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; +import "../UniVaultProxy.sol"; +import "../inheritance/GovernableInit.sol"; +import "../UniVaultStorageV1.sol"; +import "../interface/IUniVaultV1.sol"; +import "../interface/IUniVaultStorageV1.sol"; + +import "../submodules/UniVaultSubModuleChangeRangeV2Managed.sol"; +import "../submodules/interface/IUniVaultSubmoduleChangeRangeV2Managed.sol"; + +contract UniV3VaultFactory is Ownable { + address public uniVaultBeaconProxyImplementation = 0xEB779f9ed1bac88b83993b2856a2957A14cF92D0; + address public zap = 0x9B47D39cb1b02Bb77A517fEA39963534a6243Df3; + address public positionManager = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address public depositSubmodule = 0x406370ceFa004a3cb5D4f2730b679D488F4E7568; + address public feeRewardForwarder = 0x153C544f72329c1ba521DDf5086cf2fA98C86676; + uint256 public profitSharingRatio = 2000; + address public platformFeeCollector = 0xd00FCE4966821Da1EdD1221a02aF0AFc876365e4; + uint256 public platformFeeRatio = 1000; + address public lastDeployedAddress = address(0); + + // managed vault + address public depositSubmoduleManaged = address(0); + address public changeRangeSubmoduleManaged = address(0); + + mapping(address => bool) public whitelist; + + modifier onlyWhitelisted() { + require(whitelist[msg.sender] || msg.sender == owner(), "not allowed"); + _; + } + + function setWhitelist(address target, bool isWhitelisted) external onlyOwner { + whitelist[target] = isWhitelisted; + } + + function overrideDepositSubmodule(address newAddress) external onlyOwner { + depositSubmodule = newAddress; + } + + function overrideChangeRangeSubmoduleManaged(address newAddress) external onlyOwner { + changeRangeSubmoduleManaged = newAddress; + } + + function overrideDepositSubmoduleManaged(address newAddress) external onlyOwner { + depositSubmoduleManaged = newAddress; + } + + function governance() external view returns (address) { + // fake governance + return address(this); + } + + function isGovernance(address addr) external view returns (bool) { + return addr == address(this); + } + + function deploy(address store, uint256 univ3PoolId) external onlyWhitelisted returns (address vault) { + INonfungiblePositionManager uniV3posManager = INonfungiblePositionManager(positionManager); + + UniVaultProxy vaultProxy = new UniVaultProxy( + uniVaultBeaconProxyImplementation, "" + ); + + uniV3posManager.approve(address(vaultProxy), univ3PoolId); + + UniVaultStorageV1 vaultStorage = new UniVaultStorageV1(address(vaultProxy)); + + IUniVaultV1 vault = IUniVaultV1(address(vaultProxy)); + vault.initialize( + univ3PoolId, + address(vaultStorage), + address(this), // using the fake storage at first + zap + ); + + vault.configureVault(depositSubmodule, feeRewardForwarder, profitSharingRatio, platformFeeCollector, platformFeeRatio); + + GovernableInit(address(vault)).setStorage(store); // setting to the right storage + lastDeployedAddress = address(vault); + return lastDeployedAddress; + } + + function deployManaged(address store, uint256[] calldata univ3PoolIds) external onlyWhitelisted returns (address vault) { + require(depositSubmoduleManaged != address(0), "depositSubmoduleManaged not set"); + require(changeRangeSubmoduleManaged != address(0), "changeRangeSubmoduleManaged not set"); + INonfungiblePositionManager uniV3posManager = INonfungiblePositionManager(positionManager); + + UniVaultProxy vaultProxy = new UniVaultProxy( + uniVaultBeaconProxyImplementation, "" + ); + + uniV3posManager.approve(address(vaultProxy), univ3PoolIds[0]); + + UniVaultStorageV1 vaultStorage = new UniVaultStorageV1(address(vaultProxy)); + + IUniVaultV1 vault = IUniVaultV1(address(vaultProxy)); + vault.initialize( + univ3PoolIds[0], + address(vaultStorage), + address(this), // using the fake storage at first + zap + ); + + vault.configureVault( + depositSubmoduleManaged, feeRewardForwarder, profitSharingRatio, platformFeeCollector, platformFeeRatio + ); + + require(changeRangeSubmoduleManaged != address(0), "change range submodule not defined"); + vault.setSubmodule(bytes32(keccak256("changeRange")), changeRangeSubmoduleManaged); + + // transferring other position tokens + for (uint256 i = 1; i < univ3PoolIds.length; i++) { + uniV3posManager.transferFrom(address(this), address(vaultProxy), univ3PoolIds[i]); + } + IUniVaultSubmoduleChangeRangeV2Managed(address(vault)).addPositionIds(univ3PoolIds); + + GovernableInit(address(vault)).setStorage(store); // setting to the right storage + lastDeployedAddress = address(vault); + return lastDeployedAddress; + } + + function info(address vault) + external + view + returns (address[] memory Underlying, address NewVault, address DataContract, uint256 FeeAmount, uint256 PosId) + { + DataContract = address(IUniVaultV1(vault).getStorage()); + PosId = IUniVaultStorageV1(DataContract).posId(); + address token0; + address token1; + uint24 fee; + INonfungiblePositionManager uniV3posManager = INonfungiblePositionManager(positionManager); + (,, token0, token1, fee,,,,,,,) = uniV3posManager.positions(PosId); + + Underlying = new address[](2); + Underlying[0] = token0; + Underlying[1] = token1; + NewVault = vault; + FeeAmount = uint256(fee); + } + + function withdrawPosition(uint256 univ3PoolId, address destination) external onlyOwner { + INonfungiblePositionManager uniV3posManager = INonfungiblePositionManager(positionManager); + uniV3posManager.transferFrom(address(this), destination, univ3PoolId); + } +} diff --git a/src/inheritance/ControllableInit.sol b/src/inheritance/ControllableInit.sol new file mode 100644 index 0000000..2ebdf4a --- /dev/null +++ b/src/inheritance/ControllableInit.sol @@ -0,0 +1,29 @@ +pragma solidity 0.7.6; + +import "./GovernableInit.sol"; + +// A clone of Governable supporting the Initializable interface and pattern +contract ControllableInit is GovernableInit { + constructor() {} + + function initialize(address _storage) public initializer { + GovernableInit.initializeGovernable(_storage); + } + + modifier onlyController() { + require(Storage(_storage()).isController(msg.sender), "Not a controller"); + _; + } + + modifier onlyControllerOrGovernance() { + require( + (Storage(_storage()).isController(msg.sender) || Storage(_storage()).isGovernance(msg.sender)), + "The caller must be controller or governance" + ); + _; + } + + function controller() public view returns (address) { + return Storage(_storage()).controller(); + } +} diff --git a/src/inheritance/GovernableInit.sol b/src/inheritance/GovernableInit.sol new file mode 100644 index 0000000..c4521fa --- /dev/null +++ b/src/inheritance/GovernableInit.sol @@ -0,0 +1,47 @@ +pragma solidity 0.7.6; + +import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; +import "./Storage.sol"; + +// A clone of Governable supporting the Initializable interface and pattern +contract GovernableInit is Initializable { + bytes32 internal constant _STORAGE_SLOT = 0xa7ec62784904ff31cbcc32d09932a58e7f1e4476e1d041995b37c917990b16dc; + + modifier onlyGovernance() { + require(Storage(_storage()).isGovernance(msg.sender), "Not governance"); + _; + } + + constructor() { + assert(_STORAGE_SLOT == bytes32(uint256(keccak256("eip1967.governableInit.storage")) - 1)); + } + + function initializeGovernable(address _store) public initializer { + _setStorage(_store); + } + + function _setStorage(address newStorage) private { + bytes32 slot = _STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, newStorage) + } + } + + function setStorage(address _store) public onlyGovernance { + require(_store != address(0), "new storage shouldn't be empty"); + _setStorage(_store); + } + + function _storage() internal view returns (address str) { + bytes32 slot = _STORAGE_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + function governance() public view returns (address) { + return Storage(_storage()).governance(); + } +} diff --git a/src/inheritance/Storage.sol b/src/inheritance/Storage.sol new file mode 100644 index 0000000..2c30533 --- /dev/null +++ b/src/inheritance/Storage.sol @@ -0,0 +1,33 @@ +pragma solidity 0.7.6; + +contract Storage { + address public governance; + address public controller; + + constructor() { + governance = msg.sender; + } + + modifier onlyGovernance() { + require(isGovernance(msg.sender), "Not governance"); + _; + } + + function setGovernance(address _governance) public onlyGovernance { + require(_governance != address(0), "new governance shouldn't be empty"); + governance = _governance; + } + + function setController(address _controller) public onlyGovernance { + require(_controller != address(0), "new controller shouldn't be empty"); + controller = _controller; + } + + function isGovernance(address account) public view returns (bool) { + return account == governance; + } + + function isController(address account) public view returns (bool) { + return account == controller; + } +} diff --git a/src/interface/IController.sol b/src/interface/IController.sol new file mode 100644 index 0000000..644d8e2 --- /dev/null +++ b/src/interface/IController.sol @@ -0,0 +1,6 @@ +pragma solidity 0.7.6; + +interface IController { + function addVaultAndStrategy(address _vault, address _strategy) external; + function doHardWork(address _vault) external; +} diff --git a/src/interface/IFeeRewardForwarderV6.sol b/src/interface/IFeeRewardForwarderV6.sol new file mode 100644 index 0000000..ac41cf9 --- /dev/null +++ b/src/interface/IFeeRewardForwarderV6.sol @@ -0,0 +1,9 @@ +pragma solidity 0.7.6; + +interface IFeeRewardForwarderV6 { + function poolNotifyFixedTarget(address _token, uint256 _amount) external; + function notifyFeeAndBuybackAmounts(uint256 _feeAmount, address _pool, uint256 _buybackAmount) external; + function notifyFeeAndBuybackAmounts(address _token, uint256 _feeAmount, address _pool, uint256 _buybackAmount) external; + function profitSharingPool() external view returns (address); + function configureLiquidation(address[] calldata _path, bytes32[] calldata _dexes) external; +} diff --git a/src/interface/IRewardPool.sol b/src/interface/IRewardPool.sol new file mode 100644 index 0000000..26beb0d --- /dev/null +++ b/src/interface/IRewardPool.sol @@ -0,0 +1,7 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; + +interface IRewardPool { + function exit() external; + function stakedBalanceOf(address) external view returns (uint256); +} diff --git a/src/interface/IUniVaultStorageV1.sol b/src/interface/IUniVaultStorageV1.sol new file mode 100644 index 0000000..4880170 --- /dev/null +++ b/src/interface/IUniVaultStorageV1.sol @@ -0,0 +1,49 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; + +interface IUniVaultStorageV1 { + function initializeByVault( + address _token0, + address _token1, + uint24 _fee, + int24 _tickLower, + int24 _tickUpper, + address _zapContract + ) external; + + function posId() external view returns (uint256); + function token0() external view returns (address); + function token1() external view returns (address); + function zapContract() external view returns (address); + function vault() external view returns (address); + function feeRewardForwarder() external view returns (address); + function profitShareRatio() external view returns (uint256); + function platformTarget() external view returns (address); + function platformRatio() external view returns (uint256); + + function tickUpper() external view returns (int24); + function tickLower() external view returns (int24); + function fee() external view returns (uint24); + + function nextImplementation() external view returns (address); + function nextImplementationTimestamp() external view returns (uint256); + function nextImplementationDelay() external view returns (uint256); + + function setZapContract(address _zap) external; + function setToken0(address _token) external; + function setToken1(address _token) external; + function setFee(uint24 _fee) external; + function setTickUpper(int24 _tick) external; + function setTickLower(int24 _tick) external; + + function setPosId(uint256 _posId) external; + function _setVault(address _vault) external; + function setFeeRewardForwarder(address _feeRewardForwarder) external; + + function configureFees(address _feeRewardForwarder, uint256 _feeRatio, address _platformTarget, uint256 _platformRatio) + external; + + function setNextImplementation(address _impl) external; + function setNextImplementationTimestamp(uint256 _timestamp) external; + function setNextImplementationDelay(uint256 _delay) external; +} diff --git a/src/interface/IUniVaultV1.sol b/src/interface/IUniVaultV1.sol new file mode 100644 index 0000000..21647db --- /dev/null +++ b/src/interface/IUniVaultV1.sol @@ -0,0 +1,42 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "./IUniVaultStorageV1.sol"; +import "../submodules/interface/IUniVaultSubmoduleDepositV1.sol"; +import "../submodules/interface/IUniVaultSubmoduleRewardV1LiquidateOnly.sol"; + +interface IUniVaultV1 is IUniVaultSubmoduleDepositV1, IUniVaultSubmoduleRewardV1LiquidateOnly, IERC20Upgradeable { + function initialize(uint256 _tokenId, address _dataContract, address _storage, address _zapContract) external; + + function initializeEmpty() external; + + function doHardWork() external; + + function configureVault( + address submoduleDeposit, + address _feeRewardForwarder, + uint256 _feeRatio, + address _platformTarget, + uint256 _platformRatio + ) external; + + function configureFees(address _feeRewardForwarder, uint256 _feeRatio, address _platformTarget, uint256 _platformRatio) + external; + + function underlyingBalanceWithInvestment() external view returns (uint256); + function getPricePerFullShare() external view returns (uint256); + function getStorage() external view returns (IUniVaultStorageV1); + function token0() external view returns (IERC20Upgradeable); + function token1() external view returns (IERC20Upgradeable); + + function getSqrtPriceX96() external view returns (uint160); + function scheduleUpgrade(address impl) external; + function shouldUpgrade() external view returns (bool, address); + function finalizeUpgrade() external; + function increaseUpgradeDelay(uint256 delay) external; + function submoduleAddress(bytes32 submoduleHash) external view returns (address); + function setSubmodule(bytes32 submoduleHash, address submoduleAddr) external; + + function setZapContract(address newZap) external; +} diff --git a/src/interface/IUniVaultV2.sol b/src/interface/IUniVaultV2.sol new file mode 100644 index 0000000..4624bc9 --- /dev/null +++ b/src/interface/IUniVaultV2.sol @@ -0,0 +1,41 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "./IUniVaultStorageV1.sol"; +import "../submodules/interface/IUniVaultSubmoduleDepositV2.sol"; + +interface IUniVaultV2 is IUniVaultSubmoduleDepositV2, IERC20Upgradeable { + function initialize(uint256 _tokenId, address _dataContract, address _storage, address _zapContract) external; + + function initializeEmpty() external; + + function doHardWork() external; + + function configureVault( + address submoduleDeposit, + address _feeRewardForwarder, + uint256 _feeRatio, + address _platformTarget, + uint256 _platformRatio + ) external; + + function configureFees(address _feeRewardForwarder, uint256 _feeRatio, address _platformTarget, uint256 _platformRatio) + external; + + function underlyingBalanceWithInvestment() external view returns (uint256); + function getPricePerFullShare() external view returns (uint256); + function getStorage() external view returns (IUniVaultStorageV1); + function token0() external view returns (IERC20Upgradeable); + function token1() external view returns (IERC20Upgradeable); + + function getSqrtPriceX96() external view returns (uint160); + function scheduleUpgrade(address impl) external; + function shouldUpgrade() external view returns (bool, address); + function finalizeUpgrade() external; + function increaseUpgradeDelay(uint256 delay) external; + function submoduleAddress(bytes32 submoduleHash) external view returns (address); + function setSubmodule(bytes32 submoduleHash, address submoduleAddr) external; + + function setZapContract(address newZap) external; +} diff --git a/src/interface/IUniversalLiquidator.sol b/src/interface/IUniversalLiquidator.sol new file mode 100644 index 0000000..aec4722 --- /dev/null +++ b/src/interface/IUniversalLiquidator.sol @@ -0,0 +1,27 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +interface IUniversalLiquidator { + event Swap( + address indexed buyToken, + address indexed sellToken, + address indexed target, + address initiator, + uint256 amountIn, + uint256 slippage + ); + + function swapTokenOnMultipleDEXes( + uint256 amountIn, + uint256 amountOutMin, + address target, + bytes32[] memory dexes, + address[] memory path + ) external returns (uint256); + + function swapTokenOnDEX(uint256 amountIn, uint256 amountOutMin, address target, bytes32 dexName, address[] memory path) + external; + + function getAllDexes() external view returns (bytes32[] memory); +} diff --git a/src/interface/IUniversalLiquidatorRegistry.sol b/src/interface/IUniversalLiquidatorRegistry.sol new file mode 100644 index 0000000..54d49c2 --- /dev/null +++ b/src/interface/IUniversalLiquidatorRegistry.sol @@ -0,0 +1,7 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +interface IUniversalLiquidatorRegistry { + function universalLiquidator() external returns (address); +} diff --git a/src/interface/WETH9.sol b/src/interface/WETH9.sol new file mode 100644 index 0000000..f22f867 --- /dev/null +++ b/src/interface/WETH9.sol @@ -0,0 +1,12 @@ +pragma solidity 0.7.6; + +interface WETH9 { + function balanceOf(address target) external view returns (uint256); + + function deposit() external payable; + function withdraw(uint256 wad) external; + function totalSupply() external view returns (uint256); + function approve(address guy, uint256 wad) external returns (bool); + function transfer(address dst, uint256 wad) external returns (bool); + function transferFrom(address src, address dst, uint256 wad) external returns (bool); +} diff --git a/src/interface/oracle/IDexPriceAggregator.sol b/src/interface/oracle/IDexPriceAggregator.sol new file mode 100644 index 0000000..88e6be3 --- /dev/null +++ b/src/interface/oracle/IDexPriceAggregator.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.0; + +/// @title DexPriceAggregator interface +/// @notice Provides interface for querying an asset's price from one or more DEXes +interface IDexPriceAggregator { + /// @notice Given a token and its amount, return the equivalent value in another token + /// @param tokenIn Address of an ERC20 token contract to be converted + /// @param amountIn Amount of tokenIn to be converted + /// @param tokenOut Address of an ERC20 token contract to convert into + /// @param twapPeriod Number of seconds in the past to consider for the TWAP rate, if applicable + /// @return amountOut Amount of tokenOut received for amountIn of tokenIn + function assetToAsset(address tokenIn, uint256 amountIn, address tokenOut, uint256 twapPeriod) + external + view + returns (uint256 amountOut); +} diff --git a/src/interface/uniswap/IUniswapV3Pool.sol b/src/interface/uniswap/IUniswapV3Pool.sol new file mode 100644 index 0000000..4ade072 --- /dev/null +++ b/src/interface/uniswap/IUniswapV3Pool.sol @@ -0,0 +1,5 @@ +// The purpose of the file is to facilitate test and have hardhat +// compile it and provide an artifact for the tests. +pragma solidity 0.7.6; + +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; diff --git a/src/interface/uniswap/IUniswapV3Staker.sol b/src/interface/uniswap/IUniswapV3Staker.sol new file mode 100644 index 0000000..72bcf02 --- /dev/null +++ b/src/interface/uniswap/IUniswapV3Staker.sol @@ -0,0 +1,167 @@ +// 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 { + address rewardToken; + address 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 + /// @return reward The amount of reward tokens claimed + function claimReward(address 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 + view + 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/src/interface/uniswapV2/IUniswapV2Factory.sol b/src/interface/uniswapV2/IUniswapV2Factory.sol new file mode 100644 index 0000000..3138cdd --- /dev/null +++ b/src/interface/uniswapV2/IUniswapV2Factory.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.0; + +interface IUniswapV2Factory { + event PairCreated(address indexed token0, address indexed token1, address pair, uint256); + + function getPair(address tokenA, address tokenB) external view returns (address pair); + function allPairs(uint256) external view returns (address pair); + function allPairsLength() external view returns (uint256); + + function feeTo() external view returns (address); + function feeToSetter() external view returns (address); + + function createPair(address tokenA, address tokenB) external returns (address pair); +} diff --git a/src/interface/uniswapV2/IUniswapV2Migrator.sol b/src/interface/uniswapV2/IUniswapV2Migrator.sol new file mode 100644 index 0000000..d2cb2ba --- /dev/null +++ b/src/interface/uniswapV2/IUniswapV2Migrator.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.0; + +interface IUniswapV2Migrator { + function migrate(address token, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) external; +} diff --git a/src/interface/uniswapV2/IUniswapV2Pair.sol b/src/interface/uniswapV2/IUniswapV2Pair.sol new file mode 100644 index 0000000..c5e1934 --- /dev/null +++ b/src/interface/uniswapV2/IUniswapV2Pair.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +/** + * Submitted for verification at Etherscan.io on 2020-05-05 + */ + +// File: contracts/interfaces/IUniswapV2Pair.sol + +pragma solidity >=0.5.0; + +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; +} diff --git a/src/interface/uniswapV2/IUniswapV2Router01.sol b/src/interface/uniswapV2/IUniswapV2Router01.sol new file mode 100644 index 0000000..2e29781 --- /dev/null +++ b/src/interface/uniswapV2/IUniswapV2Router01.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.0; + +interface IUniswapV2Router01 { + function factory() external pure returns (address); + function WETH() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity); + function addLiquidityETH( + address token, + uint256 amountTokenDesired, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity); + function removeLiquidity( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); + function removeLiquidityETH( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountToken, uint256 amountETH); + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountA, uint256 amountB); + function removeLiquidityETHWithPermit( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountToken, uint256 amountETH); + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + function swapExactETHForTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) + external + payable + returns (uint256[] memory amounts); + function swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline) + external + returns (uint256[] memory amounts); + function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) + external + returns (uint256[] memory amounts); + function swapETHForExactTokens(uint256 amountOut, address[] calldata path, address to, uint256 deadline) + external + payable + returns (uint256[] memory amounts); + + function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) external pure returns (uint256 amountB); + function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) external pure returns (uint256 amountOut); + function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) external pure returns (uint256 amountIn); + function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); + function getAmountsIn(uint256 amountOut, address[] calldata path) external view returns (uint256[] memory amounts); +} diff --git a/src/interface/uniswapV2/IUniswapV2Router02.sol b/src/interface/uniswapV2/IUniswapV2Router02.sol new file mode 100644 index 0000000..26c8541 --- /dev/null +++ b/src/interface/uniswapV2/IUniswapV2Router02.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.0; + +import "./IUniswapV2Router01.sol"; + +interface IUniswapV2Router02 { + function factory() external pure returns (address); + function WETH() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint256 amountADesired, + uint256 amountBDesired, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity); + function addLiquidityETH( + address token, + uint256 amountTokenDesired, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity); + function removeLiquidity( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline + ) external returns (uint256 amountA, uint256 amountB); + function removeLiquidityETH( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountToken, uint256 amountETH); + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint256 liquidity, + uint256 amountAMin, + uint256 amountBMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountA, uint256 amountB); + function removeLiquidityETHWithPermit( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountToken, uint256 amountETH); + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + function swapExactETHForTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) + external + payable + returns (uint256[] memory amounts); + function swapTokensForExactETH(uint256 amountOut, uint256 amountInMax, address[] calldata path, address to, uint256 deadline) + external + returns (uint256[] memory amounts); + function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) + external + returns (uint256[] memory amounts); + function swapETHForExactTokens(uint256 amountOut, address[] calldata path, address to, uint256 deadline) + external + payable + returns (uint256[] memory amounts); + + function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) external pure returns (uint256 amountB); + function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) external pure returns (uint256 amountOut); + function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) external pure returns (uint256 amountIn); + function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts); + function getAmountsIn(uint256 amountOut, address[] calldata path) external view returns (uint256[] memory amounts); + + function removeLiquidityETHSupportingFeeOnTransferTokens( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline + ) external returns (uint256 amountETH); + function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( + address token, + uint256 liquidity, + uint256 amountTokenMin, + uint256 amountETHMin, + address to, + uint256 deadline, + bool approveMax, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 amountETH); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external; + function swapExactETHForTokensSupportingFeeOnTransferTokens( + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external payable; + function swapExactTokensForETHSupportingFeeOnTransferTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external; +} diff --git a/src/lib/BytesLib.sol b/src/lib/BytesLib.sol new file mode 100644 index 0000000..785f280 --- /dev/null +++ b/src/lib/BytesLib.sol @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: Unlicense +/* + * @title Solidity Bytes Arrays Utils + * @author Gonçalo Sá + * + * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. + * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. + */ +pragma solidity >=0.5.0 <=0.7.6; + +library BytesLib { + function concat(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bytes memory) { + bytes memory tempBytes; + + assembly { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // Store the length of the first bytes array at the beginning of + // the memory for tempBytes. + let length := mload(_preBytes) + mstore(tempBytes, length) + + // Maintain a memory counter for the current write location in the + // temp bytes array by adding the 32 bytes for the array length to + // the starting location. + let mc := add(tempBytes, 0x20) + // Stop copying when the memory counter reaches the length of the + // first bytes array. + let end := add(mc, length) + + for { + // Initialize a copy counter to the start of the _preBytes data, + // 32 bytes into its memory. + let cc := add(_preBytes, 0x20) + } lt(mc, end) { + // Increase both counters by 32 bytes each iteration. + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // Write the _preBytes data into the tempBytes memory 32 bytes + // at a time. + mstore(mc, mload(cc)) + } + + // Add the length of _postBytes to the current length of tempBytes + // and store it as the new length in the first 32 bytes of the + // tempBytes memory. + length := mload(_postBytes) + mstore(tempBytes, add(length, mload(tempBytes))) + + // Move the memory counter back from a multiple of 0x20 to the + // actual end of the _preBytes data. + mc := end + // Stop copying when the memory counter reaches the new combined + // length of the arrays. + end := add(mc, length) + + for { let cc := add(_postBytes, 0x20) } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { mstore(mc, mload(cc)) } + + // Update the free-memory pointer by padding our last write location + // to 32 bytes: add 31 bytes to the end of tempBytes to move to the + // next 32 byte block, then round down to the nearest multiple of + // 32. If the sum of the length of the two arrays is zero then add + // one before rounding down to leave a blank 32 bytes (the length block with 0). + mstore( + 0x40, + and( + add(add(end, iszero(add(length, mload(_preBytes)))), 31), + not(31) // Round down to the nearest 32 bytes. + ) + ) + } + + return tempBytes; + } + + function concatStorage(bytes storage _preBytes, bytes memory _postBytes) internal { + assembly { + // Read the first 32 bytes of _preBytes storage, which is the length + // of the array. (We don't need to use the offset into the slot + // because arrays use the entire slot.) + let fslot := sload(_preBytes.slot) + // Arrays of 31 bytes or less have an even value in their slot, + // while longer arrays have an odd value. The actual length is + // the slot divided by two for odd values, and the lowest order + // byte divided by two for even values. + // If the slot is even, bitwise and the slot with 255 and divide by + // two to get the length. If the slot is odd, bitwise and the slot + // with -1 and divide by two. + let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2) + let mlength := mload(_postBytes) + let newlength := add(slength, mlength) + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + switch add(lt(slength, 32), lt(newlength, 32)) + case 2 { + // Since the new array still fits in the slot, we just need to + // update the contents of the slot. + // uint256(bytes_storage) = uint256(bytes_storage) + uint256(bytes_memory) + new_length + sstore( + _preBytes.slot, + // all the modifications to the slot are inside this + // next block + add( + // we can just add to the slot contents because the + // bytes we want to change are the LSBs + fslot, + add( + mul( + div( + // load the bytes from memory + mload(add(_postBytes, 0x20)), + // zero all bytes to the right + exp(0x100, sub(32, mlength)) + ), + // and now shift left the number of bytes to + // leave space for the length in the slot + exp(0x100, sub(32, newlength)) + ), + // increase length by the double of the memory + // bytes length + mul(mlength, 2) + ) + ) + ) + } + case 1 { + // The stored value fits in the slot, but the combined value + // will exceed it. + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes.slot, add(mul(newlength, 2), 1)) + + // The contents of the _postBytes array start 32 bytes into + // the structure. Our first read should obtain the `submod` + // bytes that can fit into the unused space in the last word + // of the stored array. To get this, we read 32 bytes starting + // from `submod`, so the data we read overlaps with the array + // contents by `submod` bytes. Masking the lowest-order + // `submod` bytes allows us to add that value directly to the + // stored value. + + let submod := sub(32, slength) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore( + sc, add(and(fslot, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00), and(mload(mc), mask)) + ) + + for { + mc := add(mc, 0x20) + sc := add(sc, 1) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { sstore(sc, mload(mc)) } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + default { + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + // Start copying to the last used word of the stored array. + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes.slot, add(mul(newlength, 2), 1)) + + // Copy over the first `submod` bytes of the new data as in + // case 1 above. + let slengthmod := mod(slength, 32) + let mlengthmod := mod(mlength, 32) + let submod := sub(32, slengthmod) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore(sc, add(sload(sc), and(mload(mc), mask))) + + for { + sc := add(sc, 1) + mc := add(mc, 0x20) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { sstore(sc, mload(mc)) } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + } + } + + function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) { + require(_length + 31 >= _length, "slice_overflow"); + require(_start + _length >= _start, "slice_overflow"); + require(_bytes.length >= _start + _length, "slice_outOfBounds"); + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { mstore(mc, mload(cc)) } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_start + 20 >= _start, "toAddress_overflow"); + require(_bytes.length >= _start + 20, "toAddress_outOfBounds"); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } + + function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) { + require(_start + 1 >= _start, "toUint8_overflow"); + require(_bytes.length >= _start + 1, "toUint8_outOfBounds"); + uint8 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x1), _start)) + } + + return tempUint; + } + + function toUint16(bytes memory _bytes, uint256 _start) internal pure returns (uint16) { + require(_start + 2 >= _start, "toUint16_overflow"); + require(_bytes.length >= _start + 2, "toUint16_outOfBounds"); + uint16 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x2), _start)) + } + + return tempUint; + } + + function toUint32(bytes memory _bytes, uint256 _start) internal pure returns (uint32) { + require(_start + 4 >= _start, "toUint32_overflow"); + require(_bytes.length >= _start + 4, "toUint32_outOfBounds"); + uint32 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x4), _start)) + } + + return tempUint; + } + + function toUint64(bytes memory _bytes, uint256 _start) internal pure returns (uint64) { + require(_start + 8 >= _start, "toUint64_overflow"); + require(_bytes.length >= _start + 8, "toUint64_outOfBounds"); + uint64 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x8), _start)) + } + + return tempUint; + } + + function toUint96(bytes memory _bytes, uint256 _start) internal pure returns (uint96) { + require(_start + 12 >= _start, "toUint96_overflow"); + require(_bytes.length >= _start + 12, "toUint96_outOfBounds"); + uint96 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0xc), _start)) + } + + return tempUint; + } + + function toUint128(bytes memory _bytes, uint256 _start) internal pure returns (uint128) { + require(_start + 16 >= _start, "toUint128_overflow"); + require(_bytes.length >= _start + 16, "toUint128_outOfBounds"); + uint128 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x10), _start)) + } + + return tempUint; + } + + function toUint256(bytes memory _bytes, uint256 _start) internal pure returns (uint256) { + require(_start + 32 >= _start, "toUint256_overflow"); + require(_bytes.length >= _start + 32, "toUint256_outOfBounds"); + uint256 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x20), _start)) + } + + return tempUint; + } + + function toBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32) { + require(_start + 32 >= _start, "toBytes32_overflow"); + require(_bytes.length >= _start + 32, "toBytes32_outOfBounds"); + bytes32 tempBytes32; + + assembly { + tempBytes32 := mload(add(add(_bytes, 0x20), _start)) + } + + return tempBytes32; + } + + function equal(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) { + bool success = true; + + assembly { + let length := mload(_preBytes) + + // if lengths don't match the arrays are not equal + switch eq(length, mload(_postBytes)) + case 1 { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + let mc := add(_preBytes, 0x20) + let end := add(mc, length) + + for { let cc := add(_postBytes, 0x20) } + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + eq(add(lt(mc, end), cb), 2) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // if any of these checks fails then arrays are not equal + if iszero(eq(mload(mc), mload(cc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } + + function equalStorage(bytes storage _preBytes, bytes memory _postBytes) internal view returns (bool) { + bool success = true; + + assembly { + // we know _preBytes_offset is 0 + let fslot := sload(_preBytes.slot) + // Decode the length of the stored array like in concatStorage(). + let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2) + let mlength := mload(_postBytes) + + // if lengths don't match the arrays are not equal + switch eq(slength, mlength) + case 1 { + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + if iszero(iszero(slength)) { + switch lt(slength, 32) + case 1 { + // blank the last byte which is the length + fslot := mul(div(fslot, 0x100), 0x100) + + if iszero(eq(fslot, mload(add(_postBytes, 0x20)))) { + // unsuccess: + success := 0 + } + } + default { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + let sc := keccak256(0x0, 0x20) + + let mc := add(_postBytes, 0x20) + let end := add(mc, mlength) + + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + for {} eq(add(lt(mc, end), cb), 2) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + if iszero(eq(sload(sc), mload(mc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } + + /// @dev Get the revert message from a call + /// @notice This is needed in order to get the human-readable revert message from a call + /// @param _res Response of the call + /// @return Revert message string + function _getRevertMsgFromRes(bytes memory _res) internal pure returns (string memory) { + // If the _res length is less than 68, then the transaction failed silently (without a revert message) + if (_res.length < 68) return "Transaction reverted silently"; + bytes memory revertData = slice(_res, 4, _res.length - 4); // Remove the selector which is the first 4 bytes + return abi.decode(revertData, (string)); // All that remains is the revert string + } +} diff --git a/src/mock/BoredSwapper.sol b/src/mock/BoredSwapper.sol new file mode 100644 index 0000000..d23a5b5 --- /dev/null +++ b/src/mock/BoredSwapper.sol @@ -0,0 +1,59 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; + +// This is a mock contract to create fees in UniswapV3 pair. +contract BoredSwapper { + address internal constant _SWAP_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + + constructor() {} + + function accrueFees(address originToken, address intermediateToken, uint24 fee, uint256 originTokenAmountIn) public { + IERC20Upgradeable(originToken).approve(_SWAP_ROUTER, uint256(-1)); + IERC20Upgradeable(intermediateToken).approve(_SWAP_ROUTER, uint256(-1)); + + uint256 intermediateBalanceBefore = IERC20Upgradeable(intermediateToken).balanceOf(address(this)); + + require( + IERC20Upgradeable(originToken).balanceOf(address(this)) >= originTokenAmountIn, + "funds needed to simulate the increment of fees!" + ); + + ISwapRouter(_SWAP_ROUTER).exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: originToken, + tokenOut: intermediateToken, + fee: fee, + recipient: address(this), + deadline: block.timestamp, + amountIn: originTokenAmountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }) + ); + + uint256 intermediateBalanceAfter = IERC20Upgradeable(intermediateToken).balanceOf(address(this)); + uint256 intermediateBalanceDiff = intermediateBalanceAfter - intermediateBalanceBefore; + + ISwapRouter(_SWAP_ROUTER).exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: intermediateToken, + tokenOut: originToken, + fee: fee, + recipient: address(this), + deadline: block.timestamp, + amountIn: intermediateBalanceDiff, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }) + ); + } + + function withdraw(address token) public { + uint256 balance = IERC20Upgradeable(token).balanceOf(address(this)); + IERC20Upgradeable(token).transfer(msg.sender, balance); + } +} diff --git a/src/mock/PositionMinter.sol b/src/mock/PositionMinter.sol new file mode 100644 index 0000000..0d68ca1 --- /dev/null +++ b/src/mock/PositionMinter.sol @@ -0,0 +1,82 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// ERC721 +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +// reentrancy guard +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; + +contract PositionMinter is ERC721HolderUpgradeable { + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + + address internal constant _NFT_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + uint256 internal constant _UNDERLYING_UNIT = 1e18; + + uint256 public posId; + + /** + * @dev Creates the contract and ensures that the slot matches the hash + */ + constructor() {} + + // creates a new position with the designated range + function mintPosition( + address _token0, + address _token1, + uint24 _fee, + int24 _tickLower, + int24 _tickUpper, + uint256 _initAmount0, + uint256 _initAmount1 + ) public { + IERC20Upgradeable(_token0).safeTransferFrom(msg.sender, address(this), _initAmount0); + IERC20Upgradeable(_token1).safeTransferFrom(msg.sender, address(this), _initAmount1); + IERC20Upgradeable(_token0).safeApprove(_NFT_POSITION_MANAGER, 0); + IERC20Upgradeable(_token0).safeApprove(_NFT_POSITION_MANAGER, _initAmount0); + IERC20Upgradeable(_token1).safeApprove(_NFT_POSITION_MANAGER, 0); + IERC20Upgradeable(_token1).safeApprove(_NFT_POSITION_MANAGER, _initAmount1); + + (uint256 _tokenId, uint128 _initialLiquidity,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).mint( + INonfungiblePositionManager.MintParams({ + token0: address(_token0), + token1: address(_token1), + fee: _fee, + tickLower: _tickLower, + tickUpper: _tickUpper, + amount0Desired: _initAmount0, // amount0Desired + amount1Desired: _initAmount1, // amount1Desired + amount0Min: 0, // amount0Min, first small deposit, can stay 0 + amount1Min: 0, // amount1Min, first small deposit, can stay 0 + recipient: address(this), + deadline: block.timestamp // will be done on this block. + }) + ); + posId = _tokenId; + } + + function approvePosition(address _to) public { + IERC721Upgradeable(_NFT_POSITION_MANAGER).approve(_to, posId); + } + + function transferPosition(address _to) public { + IERC721Upgradeable(_NFT_POSITION_MANAGER).transferFrom(address(this), _to, posId); + } +} diff --git a/src/mock/UniVaultSubModuleDepsoitV1Debug.sol b/src/mock/UniVaultSubModuleDepsoitV1Debug.sol new file mode 100644 index 0000000..24faf18 --- /dev/null +++ b/src/mock/UniVaultSubModuleDepsoitV1Debug.sol @@ -0,0 +1,641 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// ERC721 +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +// reentrancy guard +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +// Harvest System +import "../interface/IFeeRewardForwarderV6.sol"; +import "../inheritance/ControllableInit.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; + +// Storage for this UniVault +import "../interface/IUniVaultStorageV1.sol"; +import "../interface/IUniVaultV1.sol"; + +// UniswapV2 +import "../interface/uniswapV2/IUniswapV2Pair.sol"; +import "../interface/uniswapV2/IUniswapV2Factory.sol"; +import "../interface/uniswapV2/IUniswapV2Router02.sol"; + +// BytesLib +import "../lib/BytesLib.sol"; + +import "../submodules/interface/IUniVaultSubmoduleDepositV1.sol"; +import "hardhat/console.sol"; + +contract UniVaultSubmoduleDepositV1Debug is + ERC20Upgradeable, + ERC721HolderUpgradeable, + ReentrancyGuardUpgradeable, + ControllableInit, + IUniVaultSubmoduleDepositV1 +{ + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + using BytesLib for bytes; + + bytes32 internal constant _GLOBAL_STORAGE_SLOT = 0xea3b316d3f7b97449bd56fdd0c7f95b3a874cb501e4c5d85e01bf23443a180c8; + bytes32 internal constant _DATA_CONTRACT_SLOT = 0x4c2252f3318958b38b23790562cc9d391075b8dadbfe0e707aed11afe13228b7; + bytes32 internal constant _LAST_FEE_COLLECTION_SLOT = 0xf7ab4724ec8615b241f089fd2c85cb68f4f3ebaf0381312d3b6137db479257a9; + address internal constant _NFT_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + uint256 internal constant _UNDERLYING_UNIT = 1e18; + address internal constant _V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address internal constant _V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + address internal constant _UNI_STAKER = 0x1f98407aaB862CdDeF78Ed252D6f557aA5b0f00d; + bytes32 internal constant _VAULT_PAUSE_SLOT = 0xde039a7c768eade9368187c932ab7b9ca8d5872604278b5ebfe45ab5eaf85140; + + event SwapFeeClaimed(uint256 token0Fee, uint256 token1Fee, uint256 timestamp); + event RewardAmountClaimed(uint256 token0Fee, uint256 token1Fee, uint256 timestamp); + event Deposit(address indexed user, uint256 token0Amount, uint256 token1Amount); + event Withdraw(address indexed user, uint256 token0Amount, uint256 token1Amount); + event SharePriceChangeTrading(uint256 oldPrice, uint256 newPrice, uint256 previousTimestamp, uint256 newTimestamp); + + // This is the storage for the mother level vault + // it is used here to register / deregister interface + struct GlobalStorage { + // direct mapping from function signature to submodule address + mapping(bytes4 => address) functionToSubmodule; + // Mapping from submodule hash to submodule, this is used for internal delegate calls as in doHardwork + // where we know what submodules we are calling, but those functions are not open to the public + mapping(bytes32 => address) submoduleAddress; + } + + /** + * @dev Ensures that the price between submitting the transaction and when + * the transaction gets mined did not move (checks slippage) + */ + modifier checkSqrtPriceX96(uint256 offChainSqrtPriceX96, uint256 tolerance) { + uint256 current = uint256(getSqrtPriceX96()); + uint256 step = offChainSqrtPriceX96.mul(tolerance).div(1000); + require(current < offChainSqrtPriceX96.add(step), "Price too high"); + require(current > offChainSqrtPriceX96.sub(step), "Price too low"); + _; + } + + modifier checkVaultNotPaused() { + require(!getBoolean(_VAULT_PAUSE_SLOT), "Vault is paused"); + _; + } + + modifier onlySelf() { + require(msg.sender == address(this), "only for internal"); + _; + } + + constructor() { + assert(_LAST_FEE_COLLECTION_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.lastHardWorkTimestamp")) - 1)); + assert(_VAULT_PAUSE_SLOT == bytes32(uint256(keccak256("eip1967.univault.pause")) - 1)); + } + + function getGlobalStorage() internal pure returns (GlobalStorage storage globalStorage) { + assembly { + globalStorage.slot := _GLOBAL_STORAGE_SLOT + } + } + + function registerInterface(bytes32 submoduleHash, bool register) public { + GlobalStorage storage globalStorage = getGlobalStorage(); + address currentSubmoduleAddress = globalStorage.submoduleAddress[submoduleHash]; + + uint256 functionNum = 9; + bytes4[9] memory functionLists = [ + bytes4(keccak256("migrateToNftFromV2(uint256,uint256,uint256,bool,uint256,uint256,uint256,uint256,uint160)")), + bytes4(keccak256("deposit(uint256,uint256,bool,uint256,uint256,uint256,uint256,uint160)")), + bytes4(keccak256("withdraw(uint256,bool,bool,uint256,uint256,uint256,uint256,uint160)")), + bytes4(keccak256("_collectFeesAndCompound()")), + bytes4(keccak256("_compoundRewards()")), + bytes4(keccak256("sweepDust()")), + // Exposing Legacy interface + bytes4(keccak256("migrateToNftFromV2(uint256,uint256,uint256,bool,uint256,uint256)")), + bytes4(keccak256("deposit(uint256,uint256,bool,bool,uint256,uint256)")), + bytes4(keccak256("withdraw(uint256,bool,bool,uint256,uint256)")) + ]; + + address registeringSubmodule = (register) ? currentSubmoduleAddress : address(0); + for (uint256 i = 0; i < functionNum; i++) { + globalStorage.functionToSubmodule[functionLists[i]] = registeringSubmodule; + } + } + + function migrateToNftFromV2( + uint256 _amount, + uint256 _minAmountForRemoveLiquidity0, + uint256 _minAmountForRemoveLiquidity1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance + ) public override checkSqrtPriceX96(_sqrtRatioX96, _tolerance) returns (uint256, uint256) { + return _migrateToNftFromV2(_amount, _minAmountForRemoveLiquidity0, _minAmountForRemoveLiquidity1, _zapFunds, 0, 0, 0); + } + + function migrateToNftFromV2( + uint256 _amount, + uint256 _minAmountForRemoveLiquidity0, + uint256 _minAmountForRemoveLiquidity1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _amount0OutMinForZap, + uint256 _amount1OutMinForZap, + uint160 _sqrtPriceLimitX96 + ) public override checkSqrtPriceX96(_sqrtRatioX96, _tolerance) returns (uint256, uint256) { + return _migrateToNftFromV2( + _amount, + _minAmountForRemoveLiquidity0, + _minAmountForRemoveLiquidity1, + _zapFunds, + _amount0OutMinForZap, + _amount1OutMinForZap, + _sqrtPriceLimitX96 + ); + } + + function _migrateToNftFromV2( + uint256 _amount, + uint256 _minAmountForRemoveLiquidity0, + uint256 _minAmountForRemoveLiquidity1, + bool _zapFunds, + uint256 _amount0OutMinForZap, + uint256 _amount1OutMinForZap, + uint160 _zapSqrtPriceLimitX96 + ) internal returns (uint256, uint256) { + address pair = IUniswapV2Factory(_V2_FACTORY).getPair(address(token0()), address(token1())); + IERC20Upgradeable(pair).safeTransferFrom(msg.sender, address(this), _amount); + IERC20Upgradeable(pair).safeApprove(_V2_ROUTER, 0); + IERC20Upgradeable(pair).safeApprove(_V2_ROUTER, _amount); + IUniswapV2Router02(_V2_ROUTER).removeLiquidity( + address(token0()), + address(token1()), + _amount, + _minAmountForRemoveLiquidity0, + _minAmountForRemoveLiquidity1, + address(this), + block.timestamp + ); + // deposit tokens + return _deposit(msg.sender, _zapFunds, _amount0OutMinForZap, _amount1OutMinForZap, _zapSqrtPriceLimitX96); + } + + function deposit(uint256 _amount0, uint256 _amount1, bool _zapFunds, bool _sweep, uint256 _sqrtRatioX96, uint256 _tolerance) + public + override + checkSqrtPriceX96(_sqrtRatioX96, _tolerance) + nonReentrant + checkVaultNotPaused + returns (uint256, uint256) + { + return _pullFundsAndDeposit(_amount0, _amount1, _zapFunds, 0, 0, 0); + } + + function deposit( + uint256 _amount0, + uint256 _amount1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _zapAmount0OutMin, + uint256 _zapAmount1OutMin, + uint160 _zapSqrtPriceLimitX96 + ) public override checkSqrtPriceX96(_sqrtRatioX96, _tolerance) nonReentrant checkVaultNotPaused returns (uint256, uint256) { + return _pullFundsAndDeposit(_amount0, _amount1, _zapFunds, _zapAmount0OutMin, _zapAmount1OutMin, _zapSqrtPriceLimitX96); + } + + function _pullFundsAndDeposit( + uint256 _amount0, + uint256 _amount1, + bool _zapFunds, + uint256 _zapAmount0OutMin, + uint256 _zapAmount1OutMin, + uint160 _zapSqrtPriceLimitX96 + ) internal returns (uint256, uint256) { + // it is possible for _amount0 and _amount1 to be 0, so no need for requirement statement. + token0().safeTransferFrom(msg.sender, address(this), _amount0); + token1().safeTransferFrom(msg.sender, address(this), _amount1); + // deposit tokens + return _deposit(msg.sender, _zapFunds, _zapAmount0OutMin, _zapAmount1OutMin, _zapSqrtPriceLimitX96); + } + + /** + * @dev Handles the deposit + * @param _to the address of who the shares should be minted to + */ + function _deposit( + address _to, + bool _zapFunds, + uint256 _zapAmount0OutMin, + uint256 _zapAmount1OutMin, + uint160 _zapSqrtPriceLimitX96 + ) internal returns (uint256, uint256) { + // zap funds if desired + (uint256 zapActualAmountOut0, uint256 zapActualAmountOut1) = (0, 0); + if (_zapFunds) { + (zapActualAmountOut0, zapActualAmountOut1) = _zap(_zapAmount0OutMin, _zapAmount1OutMin, _zapSqrtPriceLimitX96); + } + + // the assumes that the deposits are already in + // they were transferred in by the deposit method, and potentially zap-swapped + uint256 previousUnderlyingBalanceWithInvestment = IUniVaultV1(address(this)).underlyingBalanceWithInvestment(); + uint256 _amount0 = token0().balanceOf(address(this)); + uint256 _amount1 = token1().balanceOf(address(this)); + // approvals for the liquidity increase + token0().safeApprove(_NFT_POSITION_MANAGER, 0); + token0().safeApprove(_NFT_POSITION_MANAGER, _amount0); + token1().safeApprove(_NFT_POSITION_MANAGER, 0); + token1().safeApprove(_NFT_POSITION_MANAGER, _amount1); + // increase the liquidity + (uint128 _liquidity,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: getStorage().posId(), + amount0Desired: _amount0, + amount1Desired: _amount1, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + emit Deposit(_to, _amount0, _amount1); + // mint shares of the vault to the recipient + uint256 toMint = IERC20Upgradeable(address(this)).totalSupply() == 0 + ? uint256(_liquidity) + : (uint256(_liquidity)).mul(IERC20Upgradeable(address(this)).totalSupply()).div(previousUnderlyingBalanceWithInvestment); + _mint(_to, toMint); + _transferLeftOverTo(msg.sender); + return (zapActualAmountOut0, zapActualAmountOut1); + } + + // The function is not registered in the interface of mother vault, + // thus regular users cannot invoke this. + function compoundRewards() public {} + + // _compoundRewards is registered as an interface in the mother vault, + // but using the onlySelf modifier, this acts as an internal function. + // the reason to expose this and called only be this address via external call + // is to enable the try catch structure + function _compoundRewards() external override onlySelf {} + + // Not registered, only callable in the mother vault level. + function collectFeesAndCompound() public { + try this._collectFeesAndCompound() {} + catch { + emit SharePriceChangeTrading( + IUniVaultV1(address(this)).getPricePerFullShare(), + IUniVaultV1(address(this)).getPricePerFullShare(), + getUint256(_LAST_FEE_COLLECTION_SLOT), + block.timestamp + ); + setUint256(_LAST_FEE_COLLECTION_SLOT, block.timestamp); + } + } + + // registered but guarded by onlySelf to act as internal + function _collectFeesAndCompound() external override onlySelf { + require(getStorage().feeRewardForwarder() != address(0), "feeRewardForwarder not set"); + require(getStorage().platformTarget() != address(0), "platformTarget not set"); + + uint256 token0FromReward = token0().balanceOf(address(this)); + uint256 token1FromReward = token1().balanceOf(address(this)); + emit RewardAmountClaimed(token0FromReward, token1FromReward, block.timestamp); + + // collect the earned fees + (uint256 _collected0, uint256 _collected1) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).collect( + INonfungiblePositionManager.CollectParams({ + tokenId: getStorage().posId(), + recipient: address(this), + amount0Max: uint128(-1), // collect all token0 + amount1Max: uint128(-1) // collect all token1 + }) + ); + emit SwapFeeClaimed(_collected0, _collected1, block.timestamp); + + _collected0 = token0FromReward.add(_collected0); + _collected1 = token1FromReward.add(_collected1); + + // handles profit sharing calculations and notifications + uint256 _profitRatio0 = _collected0.mul(getStorage().profitShareRatio()).div(10000); + uint256 _profitRatio1 = _collected1.mul(getStorage().profitShareRatio()).div(10000); + uint256 _platformRatio0 = _collected0.mul(getStorage().platformRatio()).div(10000); + uint256 _platformRatio1 = _collected1.mul(getStorage().platformRatio()).div(10000); + if (_profitRatio0 != 0) { + token0().safeApprove(getStorage().feeRewardForwarder(), 0); + token0().safeApprove(getStorage().feeRewardForwarder(), _profitRatio0); + IFeeRewardForwarderV6(getStorage().feeRewardForwarder()).poolNotifyFixedTarget(getStorage().token0(), _profitRatio0); + } + if (_profitRatio1 != 0) { + token1().safeApprove(getStorage().feeRewardForwarder(), 0); + token1().safeApprove(getStorage().feeRewardForwarder(), _profitRatio1); + + IFeeRewardForwarderV6(getStorage().feeRewardForwarder()).poolNotifyFixedTarget(getStorage().token1(), _profitRatio1); + } + if (_platformRatio0 != 0) { + token0().safeTransfer(getStorage().platformTarget(), _platformRatio0); + } + if (_platformRatio1 != 0) { + token1().safeTransfer(getStorage().platformTarget(), _platformRatio1); + } + + // zap the tokens to the proper ratio + _zap(0, 0, 0); + + // increase liquidity + // this will increase the underlyingBalanceWithInvestment and getPricePerFullShare + // while totalSupply remains the same. + + token0().safeApprove(_NFT_POSITION_MANAGER, 0); + token0().safeApprove(_NFT_POSITION_MANAGER, token0().balanceOf(address(this))); + token1().safeApprove(_NFT_POSITION_MANAGER, 0); + token1().safeApprove(_NFT_POSITION_MANAGER, token1().balanceOf(address(this))); + + uint256 sharePriceBefore = IUniVaultV1(address(this)).getPricePerFullShare(); + INonfungiblePositionManager(_NFT_POSITION_MANAGER).increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: getStorage().posId(), + amount0Desired: token0().balanceOf(address(this)), + amount1Desired: token1().balanceOf(address(this)), + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + + emit SharePriceChangeTrading( + sharePriceBefore, + IUniVaultV1(address(this)).getPricePerFullShare(), + getUint256(_LAST_FEE_COLLECTION_SLOT), + block.timestamp + ); + + // Set new timestamp + setUint256(_LAST_FEE_COLLECTION_SLOT, block.timestamp); + } + + function withdraw(uint256 _numberOfShares, bool _token0, bool _token1, uint256 _sqrtRatioX96, uint256 _tolerance) + public + override + checkSqrtPriceX96(_sqrtRatioX96, _tolerance) + nonReentrant + checkVaultNotPaused + returns (uint256, uint256) + { + return _withdraw(_numberOfShares, _token0, _token1, 0, 0, 0); + } + + function withdraw( + uint256 _numberOfShares, + bool _token0, + bool _token1, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _amount0OutMin, + uint256 _amount1OutMin, + uint160 _sqrtPriceLimitX96 + ) public override checkSqrtPriceX96(_sqrtRatioX96, _tolerance) nonReentrant checkVaultNotPaused returns (uint256, uint256) { + return _withdraw(_numberOfShares, _token0, _token1, _amount0OutMin, _amount1OutMin, _sqrtPriceLimitX96); + } + + function _withdraw( + uint256 _numberOfShares, + bool _token0, + bool _token1, + uint256 _amount0OutMin, + uint256 _amount1OutMin, + uint160 _sqrtPriceLimitX96 + ) internal returns (uint256, uint256) { + require(_token0 || _token1, "At least one side must be wanted"); + // calculates the liquidity before burning any shares + uint128 liquidityShare = uint128( + (IUniVaultV1(address(this)).underlyingBalanceWithInvestment()).mul(_numberOfShares).div( + IERC20Upgradeable(address(this)).totalSupply() + ) + ); + // burn the respective shares + // guards the balance via safe math in burn + _burn(msg.sender, _numberOfShares); + // withdraw liquidity from the NFT + (uint256 _receivedToken0, uint256 _receivedToken1) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: getStorage().posId(), + liquidity: liquidityShare, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + // collect the amount fetched above + INonfungiblePositionManager(_NFT_POSITION_MANAGER).collect( + INonfungiblePositionManager.CollectParams({ + tokenId: getStorage().posId(), + recipient: address(this), + amount0Max: uint128(_receivedToken0), // collect all token0 accounted for the liquidity + amount1Max: uint128(_receivedToken1) // collect all token1 accounted for the liquidity + }) + ); + + (uint256 zapActualAmountOut0, uint256 zapActualAmountOut1) = (0, 0); + + // make swaps as desired + if (!_token0) { + uint256 balance0 = token0().balanceOf(address(this)); + if (balance0 > 0) { + (zapActualAmountOut0, zapActualAmountOut1) = _swap(balance0, 0, _amount0OutMin, 0, _sqrtPriceLimitX96); + } + } + if (!_token1) { + uint256 balance1 = token1().balanceOf(address(this)); + if (balance1 > 0) { + (zapActualAmountOut0, zapActualAmountOut1) = _swap(0, balance1, 0, _amount1OutMin, _sqrtPriceLimitX96); + } + } + emit Withdraw(msg.sender, _receivedToken0, _receivedToken1); + // transfer everything we have in the contract to msg.sender + _transferLeftOverTo(msg.sender); + return (zapActualAmountOut0, zapActualAmountOut1); + } + + /** + * @dev Zap takes in what users provided and calculates an approximately balanced amount for + * liquidity provision. It then swaps one token to another to make the tokens balanced. + * To reduce the size of the bytecode for this contract, zap is an independent contract + * that we make a delegatecall to. + */ + function _zap(uint256 _amount0OutMin, uint256 _amount1OutMin, uint160 _sqrtPriceLimitX96) + internal + returns (uint256, uint256) + { + uint256 _originalAmount0 = token0().balanceOf(address(this)); + uint256 _originalAmount1 = token1().balanceOf(address(this)); + (bool success, bytes memory data) = getStorage().zapContract().delegatecall( + abi.encodeWithSignature( + "zap(uint160,uint256,uint256,uint256,uint256)", + _sqrtPriceLimitX96, + _originalAmount0, + _originalAmount1, + _amount0OutMin, + _amount1OutMin + ) + ); + if (success) { + return abi.decode(data, (uint256, uint256)); + } + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + return (0, 0); + } + + /** + * @dev Swap takes in token amount and swap it to another. This code resides in the zap contract. + * To reduce the size of the bytecode for this contract, zap is an independent contract + * that we make a delegatecall to. + */ + function _swap( + uint256 _amount0In, + uint256 _amount1In, + uint256 _amount0OutMin, + uint256 _amount1OutMin, + uint256 _sqrtPriceLimitX96 + ) internal returns (uint256, uint256) { + (bool success, bytes memory data) = getStorage().zapContract().delegatecall( + abi.encodeWithSignature( + "swap(uint160,uint256,uint256,uint256,uint256)", + _sqrtPriceLimitX96, + _amount0In, + _amount1In, + _amount0OutMin, + _amount1OutMin + ) + ); + if (success) { + return abi.decode(data, (uint256, uint256)); + } + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + return (0, 0); + } + + /** + * @dev Convenience getter for the data contract. + */ + function getStorage() public view returns (IUniVaultStorageV1) { + return IUniVaultStorageV1(getAddress(_DATA_CONTRACT_SLOT)); + } + + /** + * @dev Convenience getter for token0. + */ + function token0() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token0()); + } + + /** + * @dev Convenience getter for token1. + */ + function token1() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token1()); + } + + function sweepDust() external override onlyControllerOrGovernance { + _transferLeftOverTo(msg.sender); + } + + /** + * @dev Handles transferring the leftovers + */ + function _transferLeftOverTo(address _to) internal { + uint256 balance0 = token0().balanceOf(address(this)); + uint256 balance1 = token1().balanceOf(address(this)); + if (balance0 > 0) { + token0().safeTransfer(_to, balance0); + } + if (balance1 > 0) { + token1().safeTransfer(_to, balance1); + } + } + + /** + * @dev Sets an address to a slot. + */ + function setAddress(bytes32 slot, address _address) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _address) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getAddress(bytes32 slot) internal view returns (address str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Sets a value to a slot. + */ + function setUint256(bytes32 slot, uint256 _value) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _value) + } + } + + /** + * @dev Reads an uint from a slot. + */ + function getUint256(bytes32 slot) internal view returns (uint256 str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Sets an boolean to a slot. + */ + function setBoolean(bytes32 slot, bool _flag) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _flag) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getBoolean(bytes32 slot) internal view returns (bool str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Convenience getter for the current sqrtPriceX96 of the Uniswap pool. + */ + function getSqrtPriceX96() public view returns (uint160) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } +} diff --git a/src/mock/UniVaultUpgradeableV1Debug.sol b/src/mock/UniVaultUpgradeableV1Debug.sol new file mode 100644 index 0000000..9b26797 --- /dev/null +++ b/src/mock/UniVaultUpgradeableV1Debug.sol @@ -0,0 +1,415 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +import "hardhat/console.sol"; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// ERC721 +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +// UniswapV3 staker +import "../interface/uniswap/IUniswapV3Staker.sol"; + +// reentrancy guard +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; + +// Harvest System +import "../inheritance/ControllableInit.sol"; + +// Storage for this UniVault +import "../interface/IUniVaultStorageV1.sol"; + +// BytesLib +import "../lib/BytesLib.sol"; + +contract UniVaultUpgradeableV1Debug is ERC20Upgradeable, ERC721HolderUpgradeable, ReentrancyGuardUpgradeable, ControllableInit { + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + using BytesLib for bytes; + + bytes32 internal constant _DATA_CONTRACT_SLOT = 0x4c2252f3318958b38b23790562cc9d391075b8dadbfe0e707aed11afe13228b7; + address internal constant _NFT_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + uint256 internal constant _UNDERLYING_UNIT = 1e18; + address internal constant _V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address internal constant _V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + address internal constant _UNI_STAKER = 0x1f98407aaB862CdDeF78Ed252D6f557aA5b0f00d; + + bytes32 internal constant _VAULT_SUBMODULE_DEPOSIT_SLOT = 0xc5c3d06316be7a352fe891a86b6ce1aaea8a598ad7fe60f2036cf375a853829d; + bytes32 internal constant _VAULT_SUBMODULE_REWARD_SLOT = 0xd5578a737a3565a899d75eebe454c1e08e53168708b84739f7bbf71557b1f5ba; + + event SwapFeeClaimed(uint256 token0Fee, uint256 token1Fee, uint256 timestamp); + event Deposit(address indexed user, uint256 token0Amount, uint256 token1Amount); + event Withdraw(address indexed user, uint256 token0Amount, uint256 token1Amount); + + /** + * @dev Ensures that the price between submitting the transaction and when + * the transaction gets mined did not move (checks slippage) + */ + modifier checkSqrtPriceX96(uint256 offChainSqrtPriceX96, uint256 tolerance) { + uint256 current = uint256(getSqrtPriceX96()); + uint256 step = offChainSqrtPriceX96.mul(tolerance).div(1000); + require(current < offChainSqrtPriceX96.add(step), "Price too high"); + require(current > offChainSqrtPriceX96.sub(step), "Price too low"); + _; + } + + modifier checkSubmoduleConfig() { + require( + getAddress(_VAULT_SUBMODULE_DEPOSIT_SLOT) != address(0) && getAddress(_VAULT_SUBMODULE_REWARD_SLOT) != address(0), + "submodules not configured" + ); + _; + } + + /** + * @dev Creates the contract and ensures that the slot matches the hash + */ + constructor() { + assert(_DATA_CONTRACT_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.dataContract")) - 1)); + assert(_VAULT_SUBMODULE_DEPOSIT_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.vaultSubmodule.deposit")) - 1)); + assert(_VAULT_SUBMODULE_REWARD_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.vaultSubmodule.reward")) - 1)); + } + + /** + * @dev Initializes the contract. Used by the proxy pattern. + */ + function initialize(uint256 _tokenId, address _dataContract, address _storage, address _zapContract) public initializer { + // initializing the governance for the contract + ControllableInit.initialize(_storage); + // sets data storage contract to the right slot and initializes it + setAddress(_DATA_CONTRACT_SLOT, _dataContract); + + IERC721Upgradeable(_NFT_POSITION_MANAGER).transferFrom(msg.sender, address(this), _tokenId); + + // fetch info from nft. + (,, address _token0, address _token1, uint24 _fee, int24 _tickLower, int24 _tickUpper, uint256 _initialLiquidity,,,,) = + INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(_tokenId); + + IUniVaultStorageV1(_dataContract).initializeByVault(_token0, _token1, _fee, _tickLower, _tickUpper, _zapContract); + // initializing the vault token. By default, it has 18 decimals. + __ERC20_init_unchained( + string(abi.encodePacked("fUniV3_", ERC20Upgradeable(_token0).symbol(), "_", ERC20Upgradeable(_token1).symbol())), + string(abi.encodePacked("fUniV3_", ERC20Upgradeable(_token0).symbol(), "_", ERC20Upgradeable(_token1).symbol())) + ); + + // set the NFT ID in the data contract + getStorage().setPosId(_tokenId); + // mint the initial shares and send them to the specified recipient, as well as overflow of the tokens + + _mint(msg.sender, uint256(_initialLiquidity)); + } + + function configureVault( + address _submoduleDeposit, + address _submoduleReward, + address _feeRewardForwarder, + uint256 _feeRatio, + address _platformTarget, + uint256 _platformRatio + ) public onlyGovernance { + _setSubmodules(_submoduleDeposit, _submoduleReward); + + getStorage().configureFees(_feeRewardForwarder, _feeRatio, _platformTarget, _platformRatio); + } + + function setSubmodules(address _submoduleDeposit, address _submoduleReward) public onlyGovernance { + _setSubmodules(_submoduleDeposit, _submoduleReward); + } + + function _setSubmodules(address _submoduleDeposit, address _submoduleReward) internal { + setAddress(_VAULT_SUBMODULE_DEPOSIT_SLOT, _submoduleDeposit); + setAddress(_VAULT_SUBMODULE_REWARD_SLOT, _submoduleReward); + } + + /** + * Removes the liquidity from v2 and deposits it into v3 + * @param _amount the amount of the V2 tokens to migrate + * @param _minAmount0 the amount of token0 that needs to be extracted from the v2 pair + * @param _minAmount1 the amount of token1 that needs to be extracted from the v2 pair + * @param _zapFunds indicates if the tokens should be traded to the required ratio + * @param _sweep indicates if the tokens left over should be send to the msg.sender + * @param _sqrtRatioX96 the sqrtRatioX96 when the transaction is being submitted + * @param _tolerance indicates the maximum accepted deviation in percents (100% is 1000) from the ratio + */ + function migrateToNftFromV2( + uint256 _amount, + uint256 _minAmount0, + uint256 _minAmount1, + bool _zapFunds, + bool _sweep, + uint256 _sqrtRatioX96, + uint256 _tolerance + ) public checkSqrtPriceX96(_sqrtRatioX96, _tolerance) nonReentrant checkSubmoduleConfig { + (bool success, bytes memory data) = (getAddress(_VAULT_SUBMODULE_DEPOSIT_SLOT)).delegatecall(msg.data); + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + + /** + * @dev Makes a deposit for the sender. + * @param _amount0 the amount of token0 to deposit + * @param _amount1 the amount of token1 to deposit + * @param _zapFunds indicates if the tokens should be traded to the required ratio + * @param _sweep indicates if the tokens left over should be send to the msg.sender + * @param _sqrtRatioX96 the sqrtRatioX96 when the transaction is being submitted + * @param _tolerance indicates the maximum accepted deviation in percents (100% is 1000) from the ratio + */ + function deposit(uint256 _amount0, uint256 _amount1, bool _zapFunds, bool _sweep, uint256 _sqrtRatioX96, uint256 _tolerance) + public + checkSqrtPriceX96(_sqrtRatioX96, _tolerance) + nonReentrant + checkSubmoduleConfig + { + (bool success, bytes memory data) = (getAddress(_VAULT_SUBMODULE_DEPOSIT_SLOT)).delegatecall(msg.data); + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + + /** + * @dev Makes a deposit for the sender by extracting funds from a matching NFT. + * @param _tokenId the ID of the NFT to extract funds from + * @param _zapFunds indicates if the tokens should be traded to the required ratio + * @param _sweep indicates if the tokens left over should be send to the msg.sender + * @param _sqrtRatioX96 the sqrtRatioX96 when the transaction is being submitted + * @param _tolerance indicates the maximum accepted deviation in percents (100% is 1000) from the ratio + */ + function depositNFT(uint256 _tokenId, bool _zapFunds, bool _sweep, uint256 _sqrtRatioX96, uint256 _tolerance) + public + checkSqrtPriceX96(_sqrtRatioX96, _tolerance) + nonReentrant + checkSubmoduleConfig + { + (bool success, bytes memory data) = (getAddress(_VAULT_SUBMODULE_DEPOSIT_SLOT)).delegatecall(msg.data); + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + + /** + * @dev Withdraws shares from the vault in the form of the underlying tokens + * @param _numberOfShares how many shares should be burned and withdrawn + * @param _token0 false if the sender wants only token1's + * @param _token1 false if the sender wants only token0's + * @param _sqrtRatioX96 the sqrtRatioX96 when the transaction is being submitted + * @param _tolerance indicates the maximum accepted deviation in percents (100% is 1000) from the ratio + */ + function withdraw(uint256 _numberOfShares, bool _token0, bool _token1, uint256 _sqrtRatioX96, uint256 _tolerance) + public + checkSqrtPriceX96(_sqrtRatioX96, _tolerance) + nonReentrant + checkSubmoduleConfig + { + (bool success, bytes memory data) = (getAddress(_VAULT_SUBMODULE_DEPOSIT_SLOT)).delegatecall(msg.data); + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + + /** + * @dev Withdraws shares from the vault in the form of the underlying tokens. Both the tokens + * will be withdrawn as returned by Uniswap. The method is safeguarded in the modifier + * in the underlying method. + * @param _numberOfShares how many shares should be burned and withdrawn + * @param _sqrtRatioX96 the sqrtRatioX96 when the transaction is being submitted + * @param _tolerance indicates the maximum accepted deviation in percents (100% is 1000) from the ratio + */ + function withdraw(uint256 _numberOfShares, uint256 _sqrtRatioX96, uint256 _tolerance) external nonReentrant { + withdraw(_numberOfShares, true, true, _sqrtRatioX96, _tolerance); + } + + /** + * Does the hard work. Only whitelisted chads. + */ + function doHardWork() external onlyControllerOrGovernance checkSubmoduleConfig { + console.log("do hard work"); + console.log(getAddress(_VAULT_SUBMODULE_DEPOSIT_SLOT)); + // Deposit submodule collect fees & zap & increase liquidity : collectFeesAndCompound() + (bool success, bytes memory data) = + (getAddress(_VAULT_SUBMODULE_DEPOSIT_SLOT)).delegatecall(abi.encodeWithSignature("collectFeesAndCompound()")); + console.log("collectFeesAndCompound completed"); + string memory revertMsg = data._getRevertMsgFromRes(); + console.log("read revertMsg"); + console.log(revertMsg); + if (success) { + console.log("succeeded"); + } else { + console.log("Failed"); + } + require(success, revertMsg); + } + + /** + * @dev Allows governance to get dust that stays behind, or accidental token transfers. + * No tokens should stay in this contract. + */ + function sweepDust() external onlyControllerOrGovernance checkSubmoduleConfig { + (bool success, bytes memory data) = (getAddress(_VAULT_SUBMODULE_DEPOSIT_SLOT)).delegatecall(msg.data); + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + } + + /** + * @dev Returns the total liquidity stored in the position + * Dev Note: need to turn on the solc optimizer otherwise the compiler + * will throw stack too deep on this function + */ + function underlyingBalanceWithInvestment() public view returns (uint256) { + // note that the liquidity is not a token, so there is no local balance added + (,,,,,,, uint128 liquidity,,,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(getStorage().posId()); + return liquidity; + } + + /** + * @dev A dummy method to get the controller working + */ + function setStrategy(address unused) public onlyControllerOrGovernance { + // intentionally a no-op + } + + /** + * @dev A dummy method to get the controller working + */ + function strategy() public view returns (address) { + return address(this); + } + + /** + * @dev Returns the price per full share, scaled to 1e18 + */ + function getPricePerFullShare() public view returns (uint256) { + return totalSupply() == 0 ? _UNDERLYING_UNIT : _UNDERLYING_UNIT.mul(underlyingBalanceWithInvestment()).div(totalSupply()); + } + + /** + * @dev Convenience getter for the data contract. + */ + function getStorage() public view returns (IUniVaultStorageV1) { + return IUniVaultStorageV1(getAddress(_DATA_CONTRACT_SLOT)); + } + + /** + * @dev Convenience getter for token0. + */ + function token0() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token0()); + } + + /** + * @dev Convenience getter for token1. + */ + function token1() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token1()); + } + + /** + * @dev Convenience getter for the current sqrtPriceX96 of the Uniswap pool. + */ + function getSqrtPriceX96() public view returns (uint160) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } + + /** + * @dev Sets an address to a slot. + */ + function setAddress(bytes32 slot, address _address) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _address) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getAddress(bytes32 slot) internal view returns (address str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * V1 Data contract & Upgradeability + */ + + // Keeping this for the legacy interface + /** + * @dev Configure fees + * @param _feeRewardForwarder the address of the fee reward forwarder + * @param _feeRatio the profit sharing fee percentage (100% = 10000) + * @param _platformTarget the address for gas fees contributions + * @param _platformRatio the percentage for gas fee contributions (100% = 10000) + */ + function configureFees(address _feeRewardForwarder, uint256 _feeRatio, address _platformTarget, uint256 _platformRatio) + external + onlyGovernance + { + getStorage().configureFees(_feeRewardForwarder, _feeRatio, _platformTarget, _platformRatio); + } + + /** + * @dev Schedules an upgrade to the new implementation for the proxy. + */ + function scheduleUpgrade(address impl) public onlyGovernance { + getStorage().setNextImplementation(impl); + getStorage().setNextImplementationTimestamp(getStorage().nextImplementationDelay().add(block.timestamp)); + } + + /** + * @dev Tells the proxy if an upgrade is scheduled and the timelock elapsed. + */ + function shouldUpgrade() external view returns (bool, address) { + return ( + getStorage().nextImplementationTimestamp() != 0 && block.timestamp > getStorage().nextImplementationTimestamp() + && getStorage().nextImplementation() != address(0), + getStorage().nextImplementation() + ); + } + + /** + * @dev Completes the upgrade and resets the upgrade state to no upgrade. + */ + function finalizeUpgrade() external onlyGovernance { + getStorage().setNextImplementation(address(0)); + getStorage().setNextImplementationTimestamp(0); + } + + /** + * @dev Increases the upgrade timelock. Governance only. + */ + function increaseUpgradeDelay(uint256 delay) external onlyGovernance { + getStorage().setNextImplementationDelay(delay.add(getStorage().nextImplementationDelay())); + } + + /** + * Staking Logic + */ + + function isStaked() public view returns (bool) {} + + function depositPosToStaker() public onlyGovernance {} + + function setIncentiveStorage(address _incStorage) public onlyGovernance {} + + function removeStake(uint256 incentiveIndex, bool requireSuccess) public onlyGovernance {} + + function addNewStake(IUniswapV3Staker.IncentiveKey memory key) public onlyGovernance {} + + function withdrawPosFromStaker(bool requireUnstakeSuccess) public onlyGovernance {} +} diff --git a/src/mock/ZapDebug.sol b/src/mock/ZapDebug.sol new file mode 100644 index 0000000..3fe3342 --- /dev/null +++ b/src/mock/ZapDebug.sol @@ -0,0 +1,344 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// ERC721 +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; + +// Harvest System +import "../inheritance/GovernableInit.sol"; + +// Storage for this UniVault +import "../UniVaultStorageV1.sol"; +import "hardhat/console.sol"; + +contract ZapDebug { + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + + bytes32 internal constant _DATA_CONTRACT_SLOT = 0x4c2252f3318958b38b23790562cc9d391075b8dadbfe0e707aed11afe13228b7; + address internal constant _SWAP_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + uint128 internal constant _LIQUIDITY_CONSTANT = 1e18; + + /** + * @dev Constructs the contract and asserts the right slot for the data contract. + * This slot has to match the slot in the vault, otherwise the delegatecalls + * will not be able to finds it. + */ + constructor() { + assert(_DATA_CONTRACT_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.dataContract")) - 1)); + } + + /** + * @dev Convenience getter for the current sqrtPriceX96 of the Uniswap pool. + */ + function getSqrtPriceX96() public view returns (uint160) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } + + /** + * @dev Zap takes in what users provided and calculates an approximately balanced amount for + * liquidity provision. It then swaps one token to another to make the tokens balanced. + * The safeguarding against slippage is left to the caller. + * @param _originalAmount0 the current amount for token0 available for zapping + * @param _originalAmount1 the current amount for token1 available for zapping + */ + function zap( + uint160 _sqrtPriceLimitX96, + uint256 _originalAmount0, + uint256 _originalAmount1, + uint256 _amount0OutMin, + uint256 _amount1OutMin + ) public returns (uint256, uint256) { + require(_originalAmount0 > 0 || _originalAmount1 > 0, "Cannot be both 0"); + if (_originalAmount0 == 0) { + return _zapZero0(_originalAmount1, _amount0OutMin, _sqrtPriceLimitX96); + } + if (_originalAmount1 == 0) { + return _zapZero1(_originalAmount0, _amount1OutMin, _sqrtPriceLimitX96); + } + + uint160 sqrtRatioX96 = getSqrtPriceX96(); // current price + uint160 sqrtRatioXA96 = TickMath.getSqrtRatioAtTick(getStorage().tickLower()); // "price" at the lower tick + uint160 sqrtRatioXB96 = TickMath.getSqrtRatioAtTick(getStorage().tickUpper()); // "price" at the upper tick + + if (sqrtRatioX96 <= sqrtRatioXA96) { + // we are below the low range, swap amount1 to token0 + // none of the amounts is 0, because the execution would be transferred + return swap(_sqrtPriceLimitX96, 0, _originalAmount1, 0, _amount1OutMin); + } else if (sqrtRatioX96 >= sqrtRatioXB96) { + // we are above range, swapping token0 to token1 + // none of the amounts is 0, because the execution would be transferred + return swap(_sqrtPriceLimitX96, _originalAmount0, 0, _amount0OutMin, 0); + } + + // we are within the range + uint128 liquidity0 = LiquidityAmounts.getLiquidityForAmount0(sqrtRatioX96, sqrtRatioXB96, _originalAmount0); + uint128 liquidity1 = LiquidityAmounts.getLiquidityForAmount1(sqrtRatioXA96, sqrtRatioX96, _originalAmount1); + + if (liquidity0 < liquidity1) { + // we are swapping token1 for token0 + return swapSufficientAmountOfToken1( + liquidity0, [sqrtRatioX96, sqrtRatioXA96, sqrtRatioXB96, _sqrtPriceLimitX96], _originalAmount1, _amount0OutMin + ); + } else { + // we are swapping token0 for token1 + return swapSufficientAmountOfToken0( + liquidity1, [sqrtRatioX96, sqrtRatioXA96, sqrtRatioXB96, _sqrtPriceLimitX96], _originalAmount0, _amount1OutMin + ); + } + } + + function swapSufficientAmountOfToken1( + uint128 liquidity0, + uint160[4] memory ratios, + uint256 _originalAmount1, + uint256 _amount0OutMin + ) internal returns (uint256, uint256) { + // obtains the ratio of assets across the entire uniswap liquidity + // the ratio will be used as a spot price for expressing the amount of + // token1 that came into the swap in the units of token0 + (uint256 amountForPrice0, uint256 amountForPrice1) = LiquidityAmounts.getAmountsForLiquidity( + ratios[0], + TickMath.getSqrtRatioAtTick(TickMath.MIN_TICK), + TickMath.getSqrtRatioAtTick(TickMath.MAX_TICK), + _LIQUIDITY_CONSTANT + ); + + // calculate how much of each token we actually need to get the liquidity + // the rest of the token1 amount will be treated as excess and swapped to token0 + // if there is no token0, so liquidity0 == 0, we still need some ratio + // for scaling the excess based on amount1/amount0, so we use _LIQUIDITY_CONSTANT + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + ratios[0], ratios[1], ratios[2], liquidity0 == 0 ? _LIQUIDITY_CONSTANT : liquidity0 + ); + + uint256 _leftover1; + if (liquidity0 == 0) { + // we have no token0, so the entire amount of token1 should be used for swapping + _leftover1 = _originalAmount1; + } else { + // we have and need amount0 of token0, this will be matched by amount1 of token1 + // the rest of token1 is _leftover1 and it will be swapped to the proper rate + // we know that _originalAmount1 >= amount1 because the caller checks + // that liquidity0 < liquidity1 + _leftover1 = _originalAmount1.sub(amount1); + } + + // converts the amount of token0 into the common unit of token1 + uint256 _amount0InToken1 = amount0.mul(amountForPrice1).div(amountForPrice0); + // calculates how much of the token1 excess should stay in token1 + uint256 toKeepInToken1 = _leftover1.mul(amount1).div(_amount0InToken1.add(amount1)); + // swaps the excess of token1 that should be converted to token0 + return swap(ratios[3], 0, _leftover1.sub(toKeepInToken1), _amount0OutMin, 0); + } + + function swapSufficientAmountOfToken0( + uint128 liquidity1, + uint160[4] memory ratios, + uint256 _originalAmount0, + uint256 _amount1OutMin + ) internal returns (uint256, uint256) { + // obtains the ratio of assets across the entire uniswap liquidity + // the ratio will be used as a spot price for expressing the amount of + // token0 that came into the swap in the units of token1 + (uint256 amountForPrice0, uint256 amountForPrice1) = LiquidityAmounts.getAmountsForLiquidity( + ratios[0], + TickMath.getSqrtRatioAtTick(TickMath.MIN_TICK), + TickMath.getSqrtRatioAtTick(TickMath.MAX_TICK), + _LIQUIDITY_CONSTANT + ); + + // calculate how much of each token we actually need to get the liquidity + // the rest of the token0 amount will be treated as excess and swapped to token1 + // if there is no token1, so liquidity1 == 0, we still need some ratio + // for scaling the excess based on amount1/amount0, so we use _LIQUIDITY_CONSTANT + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + ratios[0], ratios[1], ratios[2], liquidity1 == 0 ? _LIQUIDITY_CONSTANT : liquidity1 + ); + uint256 _leftover0; + if (liquidity1 == 0) { + // we have no token1, so the entire amount of token0 should be used for swapping + _leftover0 = _originalAmount0; + } else { + // we have and need amount1 of token1, this will be matched by amount0 of token0 + // the rest of token0 is _leftover0 and it will be swapped to the proper rate + // we know that _originalAmount0 >= amount0 because the caller checks + // that liquidity1 > liquidity0 + _leftover0 = _originalAmount0.sub(amount0); + } + + // converts the amount of token1 into the common unit of token0 + uint256 _amount1InToken0 = amount1.mul(amountForPrice0).div(amountForPrice1); + // calculates how much of the token0 excess should stay in token0 + uint256 toKeepInToken0 = _leftover0.mul(amount0).div(_amount1InToken0.add(amount0)); + // swaps the excess of token0 that should be converted to token1 + return swap(ratios[3], _leftover0.sub(toKeepInToken0), 0, 0, _amount1OutMin); + } + + // for compatibility + function zap(uint256 _originalAmount0, uint256 _originalAmount1) external { + zap(0, _originalAmount0, _originalAmount1, 0, 0); + } + + // for compatibility + function swap(uint256 _amount0In, uint256 _amount1In) external { + swap(0, _amount0In, _amount1In, 0, 0); + } + + /** + * @dev Swaps token0 amount for token1 or the other way round. One of the parameters + * always has to be 0. The safeguarding against slippage is left to the caller. + * @param _amount0In the amount to swap for token1 + * @param _amount1In the amount to swap for token0 + */ + function swap(uint160 _sqrtPriceLimitX96, uint256 _amount0In, uint256 _amount1In, uint256 _amount0Out, uint256 _amount1Out) + public + returns (uint256, uint256) + { + require(_amount1In == 0 || _amount0In == 0, "Choose one token"); + address tokenIn; + address tokenOut; + uint256 amountIn; + uint256 amountOut; + if (_amount0In == 0) { + tokenIn = getStorage().token1(); + tokenOut = getStorage().token0(); + amountIn = _amount1In; + amountOut = _amount0Out; + } else { + tokenIn = getStorage().token0(); + tokenOut = getStorage().token1(); + amountIn = _amount0In; + amountOut = _amount1Out; + } + console.log("in function swap. "); + console.log("amountOutMinimum: ", amountOut); + console.log("tokenOutAmountBefore: ", IERC20Upgradeable(tokenOut).balanceOf(address(this))); + IERC20Upgradeable(tokenIn).safeApprove(_SWAP_ROUTER, 0); + IERC20Upgradeable(tokenIn).safeApprove(_SWAP_ROUTER, amountIn); + uint256 actualAmountOut = ISwapRouter(_SWAP_ROUTER).exactInputSingle( + ISwapRouter.ExactInputSingleParams({ + tokenIn: tokenIn, + tokenOut: tokenOut, + fee: getStorage().fee(), + recipient: address(this), + deadline: block.timestamp, + amountIn: amountIn, + amountOutMinimum: amountOut, + sqrtPriceLimitX96: _sqrtPriceLimitX96 + }) + ); + + console.log("actualAmountOut: ", actualAmountOut); + console.log("tokenOutAmountAfter: ", IERC20Upgradeable(tokenOut).balanceOf(address(this))); + + if (_amount0In == 0) { + return (actualAmountOut, 0); + } + return (0, actualAmountOut); + } + + /** + * @dev Handles zap for the case when we have 0 token0s + */ + function _zapZero0(uint256 _originalAmount1, uint256 _amount0OutMin, uint160 _sqrtPriceLimitX96) + internal + returns (uint256, uint256) + { + uint160 sqrtRatioX96 = getSqrtPriceX96(); + uint160 sqrtRatioXA96 = TickMath.getSqrtRatioAtTick(getStorage().tickLower()); + uint160 sqrtRatioXB96 = TickMath.getSqrtRatioAtTick(getStorage().tickUpper()); + + if (sqrtRatioX96 <= sqrtRatioXA96) { + // we are below the low range, swap _originalAmount1 to token0 + // the amount is not 0, because this was checked upstream + return swap(_sqrtPriceLimitX96, 0, _originalAmount1, _amount0OutMin, 0); + } else if (sqrtRatioX96 >= sqrtRatioXB96) { + // we are above range, should be swapping token0 to token1, but we do not have any + // so this is a no-op + return (0, 0); + } + + return swapSufficientAmountOfToken1( + 0, [sqrtRatioX96, sqrtRatioXA96, sqrtRatioXB96, _sqrtPriceLimitX96], _originalAmount1, _amount0OutMin + ); + } + + /** + * @dev Handles zap for the case when we have 0 token1s + */ + function _zapZero1(uint256 _originalAmount0, uint256 _amount1OutMin, uint160 _sqrtPriceLimitX96) + internal + returns (uint256, uint256) + { + uint160 sqrtRatioX96 = getSqrtPriceX96(); + uint160 sqrtRatioXA96 = TickMath.getSqrtRatioAtTick(getStorage().tickLower()); + uint160 sqrtRatioXB96 = TickMath.getSqrtRatioAtTick(getStorage().tickUpper()); + + if (sqrtRatioX96 <= sqrtRatioXA96) { + return (0, 0); + // we are below the low range, swap amount1 to token0 but that amount is 0 + // this is a no-op + } else if (sqrtRatioX96 >= sqrtRatioXB96) { + // the amount is not 0, because this was checked upstream + return swap(_sqrtPriceLimitX96, _originalAmount0, 0, 0, _amount1OutMin); + } + + return swapSufficientAmountOfToken0( + 0, [sqrtRatioX96, sqrtRatioXA96, sqrtRatioXB96, _sqrtPriceLimitX96], _originalAmount0, _amount1OutMin + ); + } + + /** + * @dev Convenience getter for the data contract. + */ + function getStorage() public view returns (UniVaultStorageV1) { + return UniVaultStorageV1(getAddress(_DATA_CONTRACT_SLOT)); + } + + /** + * @dev Convenience getter for token0. + */ + function token0() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token0()); + } + + /** + * @dev Convenience getter for token1. + */ + function token1() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token1()); + } + + /** + * @dev Reads an address from a slot. + */ + function getAddress(bytes32 slot) internal view returns (address str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } +} diff --git a/src/submodules/UniVaultSubModuleChangeRangeV1.sol b/src/submodules/UniVaultSubModuleChangeRangeV1.sol new file mode 100644 index 0000000..2291dbb --- /dev/null +++ b/src/submodules/UniVaultSubModuleChangeRangeV1.sol @@ -0,0 +1,267 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// ERC721 +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +// reentrancy guard +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; + +// Harvest System +import "../interface/IFeeRewardForwarderV6.sol"; +import "../inheritance/ControllableInit.sol"; + +// Storage for this UniVault +import "../interface/IUniVaultStorageV1.sol"; + +// BytesLib +import "../lib/BytesLib.sol"; + +// UL +import "../interface/IUniversalLiquidatorRegistry.sol"; +import "../interface/IUniversalLiquidator.sol"; + +contract UniVaultSubmoduleChangeRangeV1 is ReentrancyGuardUpgradeable, ControllableInit { + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + using BytesLib for bytes; + + // We expect all reward submodule to have this reward token list + bytes32 internal constant _GLOBAL_STORAGE_SLOT = 0xea3b316d3f7b97449bd56fdd0c7f95b3a874cb501e4c5d85e01bf23443a180c8; + bytes32 internal constant _DATA_CONTRACT_SLOT = 0x4c2252f3318958b38b23790562cc9d391075b8dadbfe0e707aed11afe13228b7; + bytes32 internal constant _VAULT_PAUSE_SLOT = 0xde039a7c768eade9368187c932ab7b9ca8d5872604278b5ebfe45ab5eaf85140; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + + address internal constant _NFT_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + + // This is the storage for the mother level vault + // it is used here to register / deregister interface + struct GlobalStorage { + // direct mapping from function signature to submodule address + mapping(bytes4 => address) functionToSubmodule; + // Mapping from submodule hash to submodule, this is used for internal delegate calls as in doHardwork + // where we know what submodules we are calling, but those functions are not open to the public + mapping(bytes32 => address) submoduleAddress; + } + + modifier checkVaultNotPaused() { + require(!getBoolean(_VAULT_PAUSE_SLOT), "Vault is paused"); + _; + } + + modifier onlySelf() { + require(msg.sender == address(this), "only for internal"); + _; + } + + function getGlobalStorage() internal pure returns (GlobalStorage storage globalStorage) { + assembly { + globalStorage.slot := _GLOBAL_STORAGE_SLOT + } + } + + function registerInterface(bytes32 submoduleHash, bool register) public virtual { + GlobalStorage storage globalStorage = getGlobalStorage(); + address currentSubmoduleAddress = globalStorage.submoduleAddress[submoduleHash]; + + uint256 functionNum = 1; + bytes4[1] memory functionLists = [bytes4(keccak256("changeRange(uint256)"))]; + + address registeringSubmodule = (register) ? currentSubmoduleAddress : address(0); + for (uint256 i = 0; i < functionNum; i++) { + globalStorage.functionToSubmodule[functionLists[i]] = registeringSubmodule; + } + } + + function _changeRange(uint256 newPosId) internal { + // If there is no such position, this will also fail. + require(IERC721Upgradeable(_NFT_POSITION_MANAGER).ownerOf(newPosId) == address(this), "pos owner mismatch"); + + // making sure that we are pointing to the same thing. + (,,,,,,, uint128 _oldLiquidity,,,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(getStorage().posId()); + // fetching information from new position + (,, address _t0, address _t1, uint24 _fee, int24 _newTickLower, int24 _newTickUpper,,,,,) = + INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(newPosId); + + require(_t0 == address(token0()) && _t1 == address(token1()), "pool mismatch"); + getStorage().setFee(_fee); + + uint256 oldPosId = getStorage().posId(); + + // remove liquidity from old + (uint256 _receivedToken0, uint256 _receivedToken1) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: oldPosId, + liquidity: _oldLiquidity, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + + // collect the amount fetched above + previous accumulated fees + INonfungiblePositionManager(_NFT_POSITION_MANAGER).collect( + INonfungiblePositionManager.CollectParams({ + tokenId: oldPosId, + recipient: address(this), + amount0Max: uint128(-1), // collect all token0 (since we are changing ranges) + amount1Max: uint128(-1) // collect all token1 (since we are changing ranges) + }) + ); + + // set new ranges + getStorage().setTickLower(_newTickLower); + getStorage().setTickUpper(_newTickUpper); + + // Zap will balance it to the new range + _zap(0, 0, 0); // zap the tokens to the proper ratio + + // point the storge to the new Pos Id + getStorage().setPosId(newPosId); + + // we should now put them all into the new position. + + uint256 _amount0 = token0().balanceOf(address(this)); + uint256 _amount1 = token1().balanceOf(address(this)); + // approvals for the liquidity increase + token0().safeApprove(_NFT_POSITION_MANAGER, 0); + token0().safeApprove(_NFT_POSITION_MANAGER, _amount0); + token1().safeApprove(_NFT_POSITION_MANAGER, 0); + token1().safeApprove(_NFT_POSITION_MANAGER, _amount1); + // increase the liquidity + (uint128 _liquidity,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: getStorage().posId(), + amount0Desired: _amount0, + amount1Desired: _amount1, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + } + + function changeRange(uint256 newPosId) public onlyGovernance { + _changeRange(newPosId); + _transferLeftOverTo(msg.sender); + } + + /** + * @dev Convenience getter for token0. + */ + function token0() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token0()); + } + + /** + * @dev Convenience getter for token1. + */ + function token1() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token1()); + } + + /** + * @dev Convenience getter for the data contract. + */ + function getStorage() public view returns (IUniVaultStorageV1) { + return IUniVaultStorageV1(getAddress(_DATA_CONTRACT_SLOT)); + } + + /** + * @dev Reads an address from a slot. + */ + function getAddress(bytes32 slot) internal view returns (address str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Sets an boolean to a slot. + */ + function setBoolean(bytes32 slot, bool _flag) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _flag) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getBoolean(bytes32 slot) internal view returns (bool str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Convenience getter for the current sqrtPriceX96 of the Uniswap pool. + */ + function getSqrtPriceX96() public view returns (uint160) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } + + /** + * @dev Zap takes in what users provided and calculates an approximately balanced amount for + * liquidity provision. It then swaps one token to another to make the tokens balanced. + * To reduce the size of the bytecode for this contract, zap is an independent contract + * that we make a delegatecall to. + */ + function _zap(uint256 _amount0OutMin, uint256 _amount1OutMin, uint160 _sqrtPriceLimitX96) + internal + returns (uint256, uint256) + { + uint256 _originalAmount0 = token0().balanceOf(address(this)); + uint256 _originalAmount1 = token1().balanceOf(address(this)); + (bool success, bytes memory data) = getStorage().zapContract().delegatecall( + abi.encodeWithSignature( + "zap(uint160,uint256,uint256,uint256,uint256)", + _sqrtPriceLimitX96, + _originalAmount0, + _originalAmount1, + _amount0OutMin, + _amount1OutMin + ) + ); + if (success) { + return abi.decode(data, (uint256, uint256)); + } + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + return (0, 0); + } + + /** + * @dev Handles transferring the leftovers + */ + function _transferLeftOverTo(address _to) internal { + uint256 balance0 = token0().balanceOf(address(this)); + uint256 balance1 = token1().balanceOf(address(this)); + if (balance0 > 0) { + token0().safeTransfer(_to, balance0); + } + if (balance1 > 0) { + token1().safeTransfer(_to, balance1); + } + } +} diff --git a/src/submodules/UniVaultSubModuleChangeRangeV2Managed.sol b/src/submodules/UniVaultSubModuleChangeRangeV2Managed.sol new file mode 100644 index 0000000..f74d44b --- /dev/null +++ b/src/submodules/UniVaultSubModuleChangeRangeV2Managed.sol @@ -0,0 +1,274 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; +import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; + +import "./UniVaultSubModuleChangeRangeV1.sol"; +import "../interface/uniswapV2/IUniswapV2Router02.sol"; + +contract UniVaultSubmoduleChangeRangeV2Managed is UniVaultSubmoduleChangeRangeV1 { + using SafeMathUpgradeable for uint256; + + uint256 internal constant INDEX_NOT_FOUND = uint256(-1); + bytes32 internal constant _LOCAL_STORAGE_SLOT_V2_MANAGED = 0x06bc87915c202efffe30f9e888a1400ac812e3fce7b75d676c422e2b1ca89608; + + event RangeChanged(uint256 oldPosId, uint256 newPosId, uint256 original0, uint256 original1, uint256 final0, uint256 final1); + + struct LocalStorageV2Managed { + uint256[] positionIds; + mapping(address => bool) rangeManagers; + mapping(address => bool) rangeSwitchers; + uint256 rangeChangedTime; + uint256 rangeChangeDelayTime; + uint256 maxLossRatio; + address[] sellPath; + } + + modifier onlyRangeSwitcher() { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + require(localStorage.rangeSwitchers[msg.sender] || msg.sender == governance(), "only range switcher"); + _; + } + + modifier onlyRangeManager() { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + require(localStorage.rangeManagers[msg.sender] || msg.sender == governance(), "only range manager"); + _; + } + + constructor() { + assert( + _LOCAL_STORAGE_SLOT_V2_MANAGED + == bytes32(uint256(keccak256("eip1967.uniVault.changeRangeSubmodule.localStorageV2Managed")) - 1) + ); + } + + function getLocalStorageV2Managed() internal pure returns (LocalStorageV2Managed storage localStorage) { + assembly { + localStorage.slot := _LOCAL_STORAGE_SLOT_V2_MANAGED + } + } + + // returns position ids of the available ranges + function getRangePositionIds() public view returns (uint256[] memory) { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + return localStorage.positionIds; + } + + function getCurrentRangePositionId() public view returns (uint256) { + return getStorage().posId(); + } + + function setSellPath(address[] memory newSellPath) public onlyGovernance { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + localStorage.sellPath = newSellPath; + } + + function isManaged() public view returns (bool) { + return true; + } + + function registerInterface(bytes32 submoduleHash, bool register) public override { + GlobalStorage storage globalStorage = getGlobalStorage(); + address currentSubmoduleAddress = globalStorage.submoduleAddress[submoduleHash]; + + uint256 functionNum = 16; + bytes4[16] memory functionLists = [ + bytes4(keccak256("isManaged()")), + bytes4(keccak256("getCurrentRangePositionId()")), + bytes4(keccak256("changeRangeGuarded(uint256,uint256,uint256)")), + bytes4(keccak256("isInRange(uint256)")), + bytes4(keccak256("getRangeChangedTime()")), + bytes4(keccak256("addPositionIds(uint256[])")), + bytes4(keccak256("removePositionId(uint256)")), + bytes4(keccak256("getAmountsForPosition(uint256)")), + bytes4(keccak256("isCurrentPositionInRange()")), + bytes4(keccak256("getPositionIds()")), + bytes4(keccak256("getPositionIdsThatAreInRange()")), + bytes4(keccak256("changeRangeUnguarded(uint256,uint256,uint256)")), + bytes4(keccak256("setMaxLossRatio(uint256)")), + bytes4(keccak256("setSellPath(address[])")), + bytes4(keccak256("getRangePositionIds()")), + bytes4(keccak256("findIndexOfPositionId(uint256")) + ]; + + address registeringSubmodule = (register) ? currentSubmoduleAddress : address(0); + for (uint256 i = 0; i < functionNum; i++) { + globalStorage.functionToSubmodule[functionLists[i]] = registeringSubmodule; + } + + // custom initialization for inner storage + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + localStorage.rangeChangedTime = block.timestamp; + localStorage.maxLossRatio = 50; // 0.5% + } + + function getSqrtPriceX96(address _token0, address _token1, uint24 _fee) public view returns (uint160) { + address poolAddr = IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(_token0, _token1, _fee); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } + + function getAmountsForPosition(uint256 posId) public view returns (uint256 amount0, uint256 amount1) { + (,, address _token0, address _token1, uint24 _fee, int24 _tickLower, int24 _tickUpper, uint128 _liquidity,,,,) = + INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(posId); + uint160 sqrtRatioX96 = getSqrtPriceX96(_token0, _token1, _fee); + uint160 sqrtRatioXA96 = TickMath.getSqrtRatioAtTick(_tickLower); + uint160 sqrtRatioXB96 = TickMath.getSqrtRatioAtTick(_tickUpper); + (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtRatioX96, sqrtRatioXA96, sqrtRatioXB96, _liquidity); + } + + function isInRange(uint256 positionId) public view returns (bool) { + (,, address _token0, address _token1, uint24 _fee, int24 _tickLower, int24 _tickUpper, uint128 _liquidity,,,,) = + INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(positionId); + uint160 sqrtRatioX96 = getSqrtPriceX96(_token0, _token1, _fee); + uint160 sqrtRatioXA96 = TickMath.getSqrtRatioAtTick(_tickLower); + uint160 sqrtRatioXB96 = TickMath.getSqrtRatioAtTick(_tickUpper); + return (sqrtRatioX96 >= sqrtRatioXA96) && (sqrtRatioX96 <= sqrtRatioXB96); + } + + // returns timestamp since last position change + function getRangeChangedTime() public view returns (uint256) { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + return localStorage.rangeChangedTime; + } + + function findIndexOfPositionId(uint256 positionId) public view returns (uint256) { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + for (uint256 i = 0; i < localStorage.positionIds.length; i++) { + if (localStorage.positionIds[i] == positionId) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + function setMaxLossRatio(uint256 _newLossRatio) public onlyGovernance { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + require(_newLossRatio <= 10000, "max ratio is 10000"); + localStorage.maxLossRatio = _newLossRatio; + } + + // adds a new range. Checks that the assets are matching + function addPositionIds(uint256[] calldata newPositionIds) external onlyRangeManager { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + for (uint256 i = 0; i < newPositionIds.length; i++) { + require(IERC721Upgradeable(_NFT_POSITION_MANAGER).ownerOf(newPositionIds[i]) == address(this), "pos owner mismatch"); + require(findIndexOfPositionId(newPositionIds[i]) == INDEX_NOT_FOUND, "position aleady exists"); + localStorage.positionIds.push(newPositionIds[i]); + } + } + + // removes a range position id. Cannot remove the current range + function removePositionId(uint256 existingPositionId) external onlyRangeManager { + require(getCurrentRangePositionId() != existingPositionId, "Cannot remove current position token id"); + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + require(localStorage.positionIds.length > 1, "Cannot remove the last position token"); + uint256 existingPositionIndex = findIndexOfPositionId(existingPositionId); + require(existingPositionIndex != INDEX_NOT_FOUND, "Position id not found"); + localStorage.positionIds[existingPositionIndex] = localStorage.positionIds[localStorage.positionIds.length - 1]; + localStorage.positionIds.pop(); + } + + // returns true if the current position is in range + function isCurrentPositionInRange() public view returns (bool) { + return isInRange(getCurrentRangePositionId()); + } + + // returns all supported positions + function getPositionIds() public view returns (uint256[] memory result) { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + return localStorage.positionIds; + } + + // iterates over all the positions and returns the positions that are in range at the moment + function getPositionIdsThatAreInRange() public view returns (uint256[] memory) { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + uint256 relevantNumber = 0; + for (uint256 i = 0; i < localStorage.positionIds.length; i++) { + if (isInRange(localStorage.positionIds[i])) { + relevantNumber++; + } + } + uint256 index = 0; + uint256[] memory result = new uint256[](relevantNumber); + for (uint256 i = 0; i < localStorage.positionIds.length; i++) { + if (isInRange(localStorage.positionIds[i])) { + result[index++] = localStorage.positionIds[i]; + } + } + return result; + } + + function changeRangeGuarded(uint256 newPosId, uint256 minFinalAmount0, uint256 minFinalAmount1) + public + onlyRangeSwitcher + returns (uint256 original0, uint256 original1, uint256 final0, uint256 final1) + { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + require(isInRange(newPosId), "new position must be in range for the switching"); + require(block.timestamp.sub(localStorage.rangeChangedTime) >= localStorage.rangeChangeDelayTime, "Not enough time passed"); + (original0, original1, final0, final1) = _changeRangeUnguarded(newPosId, minFinalAmount0, minFinalAmount1); + } + + function _changeRangeUnguarded(uint256 newPosId, uint256 minFinalAmount0, uint256 minFinalAmount1) + internal + returns (uint256 original0, uint256 original1, uint256 final0, uint256 final1) + { + uint256 oldPosId = getCurrentRangePositionId(); + (original0, original1) = getAmountsForPosition(oldPosId); + original0 = original0.add(token0().balanceOf(address(this))); + original1 = original1.add(token1().balanceOf(address(this))); + require(findIndexOfPositionId(newPosId) != INDEX_NOT_FOUND, "Position id not supported"); + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + localStorage.rangeChangedTime = block.timestamp; + _changeRange(newPosId); + (final0, final1) = getAmountsForPosition(newPosId); + final0 = final0.add(token0().balanceOf(address(this))); + final1 = final1.add(token1().balanceOf(address(this))); + require(final0 >= minFinalAmount0, "Amount of token0 after range switch is too low"); + require(final1 >= minFinalAmount1, "Amount of token1 after range switch is too low"); + + address[] memory finalSellPath; + if (localStorage.sellPath.length == 0) { + finalSellPath = new address[](2); + finalSellPath[0] = address(token0()); + finalSellPath[1] = address(token1()); + } else { + finalSellPath = localStorage.sellPath; + } + + // converting final0 into token1 + uint256[] memory amounts; + + uint256 allFinalTokensConvertedIntoToken1 = final1; + if (final0 > 0) { + amounts = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D).getAmountsOut(final0, finalSellPath); + allFinalTokensConvertedIntoToken1 = final1.add(amounts[1]); + } + // converting original0 into token1 + uint256 allOriginalTokensConvertedIntoToken1 = original1; + if (original0 > 0) { + amounts = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D).getAmountsOut(original0, finalSellPath); + allOriginalTokensConvertedIntoToken1 = original1.add(amounts[1]); + } + + // if there is loss, check if it is too much + if (allFinalTokensConvertedIntoToken1 < allOriginalTokensConvertedIntoToken1) { + uint256 loss = allOriginalTokensConvertedIntoToken1.sub(allFinalTokensConvertedIntoToken1); + require(allOriginalTokensConvertedIntoToken1.mul(localStorage.maxLossRatio).div(10000) >= loss, "Too much loss"); + } + + emit RangeChanged(oldPosId, newPosId, original0, original1, final0, final1); + } + + function changeRangeUnguarded(uint256 newPosId, uint256 minFinalAmount0, uint256 minFinalAmount1) + public + onlyRangeManager + returns (uint256 original0, uint256 original1, uint256 final0, uint256 final1) + { + return _changeRangeUnguarded(newPosId, minFinalAmount0, minFinalAmount1); + } +} diff --git a/src/submodules/UniVaultSubModuleDepositV1.sol b/src/submodules/UniVaultSubModuleDepositV1.sol new file mode 100644 index 0000000..c9f1c37 --- /dev/null +++ b/src/submodules/UniVaultSubModuleDepositV1.sol @@ -0,0 +1,659 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// ERC721 +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +// reentrancy guard +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +// Harvest System +import "../interface/IFeeRewardForwarderV6.sol"; +import "../inheritance/ControllableInit.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; + +// Storage for this UniVault +import "../interface/IUniVaultStorageV1.sol"; +import "../interface/IUniVaultV1.sol"; + +// UniswapV2 +import "../interface/uniswapV2/IUniswapV2Pair.sol"; +import "../interface/uniswapV2/IUniswapV2Factory.sol"; +import "../interface/uniswapV2/IUniswapV2Router02.sol"; + +// BytesLib +import "../lib/BytesLib.sol"; + +import "./interface/IUniVaultSubmoduleDepositV1.sol"; + +contract UniVaultSubmoduleDepositV1 is + ERC20Upgradeable, + ERC721HolderUpgradeable, + ReentrancyGuardUpgradeable, + ControllableInit, + IUniVaultSubmoduleDepositV1 +{ + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + using BytesLib for bytes; + + bytes32 internal constant _GLOBAL_STORAGE_SLOT = 0xea3b316d3f7b97449bd56fdd0c7f95b3a874cb501e4c5d85e01bf23443a180c8; + bytes32 internal constant _DATA_CONTRACT_SLOT = 0x4c2252f3318958b38b23790562cc9d391075b8dadbfe0e707aed11afe13228b7; + bytes32 internal constant _LAST_FEE_COLLECTION_SLOT = 0xf7ab4724ec8615b241f089fd2c85cb68f4f3ebaf0381312d3b6137db479257a9; + address internal constant _NFT_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + uint256 internal constant _UNDERLYING_UNIT = 1e18; + address internal constant _V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address internal constant _V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + address internal constant _UNI_STAKER = 0x1f98407aaB862CdDeF78Ed252D6f557aA5b0f00d; + bytes32 internal constant _VAULT_PAUSE_SLOT = 0xde039a7c768eade9368187c932ab7b9ca8d5872604278b5ebfe45ab5eaf85140; + + event SwapFeeClaimed(uint256 token0Fee, uint256 token1Fee, uint256 timestamp); + event RewardAmountClaimed(uint256 token0Fee, uint256 token1Fee, uint256 timestamp); + event Deposit(address indexed user, uint256 token0Amount, uint256 token1Amount); + event Withdraw(address indexed user, uint256 token0Amount, uint256 token1Amount); + event SharePriceChangeTrading(uint256 oldPrice, uint256 newPrice, uint256 previousTimestamp, uint256 newTimestamp); + + // This is the storage for the mother level vault + // it is used here to register / deregister interface + struct GlobalStorage { + // direct mapping from function signature to submodule address + mapping(bytes4 => address) functionToSubmodule; + // Mapping from submodule hash to submodule, this is used for internal delegate calls as in doHardwork + // where we know what submodules we are calling, but those functions are not open to the public + mapping(bytes32 => address) submoduleAddress; + } + + /** + * @dev Ensures that the price between submitting the transaction and when + * the transaction gets mined did not move (checks slippage) + */ + modifier checkSqrtPriceX96(uint256 offChainSqrtPriceX96, uint256 tolerance) { + uint256 current = uint256(getSqrtPriceX96()); + uint256 step = offChainSqrtPriceX96.mul(tolerance).div(1000); + require(current < offChainSqrtPriceX96.add(step), "Price too high"); + require(current > offChainSqrtPriceX96.sub(step), "Price too low"); + _; + } + + modifier checkVaultNotPaused() { + require(!getBoolean(_VAULT_PAUSE_SLOT), "Vault is paused"); + _; + } + + modifier onlySelf() { + require(msg.sender == address(this), "only for internal"); + _; + } + + constructor() { + assert(_LAST_FEE_COLLECTION_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.lastHardWorkTimestamp")) - 1)); + assert(_VAULT_PAUSE_SLOT == bytes32(uint256(keccak256("eip1967.univault.pause")) - 1)); + } + + function getGlobalStorage() internal pure returns (GlobalStorage storage globalStorage) { + assembly { + globalStorage.slot := _GLOBAL_STORAGE_SLOT + } + } + + function registerInterface(bytes32 submoduleHash, bool register) public virtual { + GlobalStorage storage globalStorage = getGlobalStorage(); + address currentSubmoduleAddress = globalStorage.submoduleAddress[submoduleHash]; + + uint256 functionNum = 9; + bytes4[9] memory functionLists = [ + bytes4(keccak256("migrateToNftFromV2(uint256,uint256,uint256,bool,uint256,uint256,uint256,uint256,uint160)")), + bytes4(keccak256("deposit(uint256,uint256,bool,uint256,uint256,uint256,uint256,uint160)")), + bytes4(keccak256("withdraw(uint256,bool,bool,uint256,uint256,uint256,uint256,uint160)")), + bytes4(keccak256("_collectFeesAndCompound()")), + bytes4(keccak256("_compoundRewards()")), + bytes4(keccak256("sweepDust()")), + // Exposing Legacy interface + bytes4(keccak256("migrateToNftFromV2(uint256,uint256,uint256,bool,uint256,uint256)")), + bytes4(keccak256("deposit(uint256,uint256,bool,bool,uint256,uint256)")), + bytes4(keccak256("withdraw(uint256,bool,bool,uint256,uint256)")) + ]; + + address registeringSubmodule = (register) ? currentSubmoduleAddress : address(0); + for (uint256 i = 0; i < functionNum; i++) { + globalStorage.functionToSubmodule[functionLists[i]] = registeringSubmodule; + } + } + + function migrateToNftFromV2( + uint256 _amount, + uint256 _minAmountForRemoveLiquidity0, + uint256 _minAmountForRemoveLiquidity1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance + ) public virtual override checkSqrtPriceX96(_sqrtRatioX96, _tolerance) returns (uint256, uint256) { + return _migrateToNftFromV2(_amount, _minAmountForRemoveLiquidity0, _minAmountForRemoveLiquidity1, _zapFunds, 0, 0, 0); + } + + function migrateToNftFromV2( + uint256 _amount, + uint256 _minAmountForRemoveLiquidity0, + uint256 _minAmountForRemoveLiquidity1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _amount0OutMinForZap, + uint256 _amount1OutMinForZap, + uint160 _sqrtPriceLimitX96 + ) public virtual override checkSqrtPriceX96(_sqrtRatioX96, _tolerance) returns (uint256, uint256) { + return _migrateToNftFromV2( + _amount, + _minAmountForRemoveLiquidity0, + _minAmountForRemoveLiquidity1, + _zapFunds, + _amount0OutMinForZap, + _amount1OutMinForZap, + _sqrtPriceLimitX96 + ); + } + + function _migrateToNftFromV2( + uint256 _amount, + uint256 _minAmountForRemoveLiquidity0, + uint256 _minAmountForRemoveLiquidity1, + bool _zapFunds, + uint256 _amount0OutMinForZap, + uint256 _amount1OutMinForZap, + uint160 _zapSqrtPriceLimitX96 + ) internal returns (uint256, uint256) { + address pair = IUniswapV2Factory(_V2_FACTORY).getPair(address(token0()), address(token1())); + IERC20Upgradeable(pair).safeTransferFrom(msg.sender, address(this), _amount); + IERC20Upgradeable(pair).safeApprove(_V2_ROUTER, 0); + IERC20Upgradeable(pair).safeApprove(_V2_ROUTER, _amount); + IUniswapV2Router02(_V2_ROUTER).removeLiquidity( + address(token0()), + address(token1()), + _amount, + _minAmountForRemoveLiquidity0, + _minAmountForRemoveLiquidity1, + address(this), + block.timestamp + ); + // deposit tokens + return _deposit(msg.sender, _zapFunds, _amount0OutMinForZap, _amount1OutMinForZap, _zapSqrtPriceLimitX96); + } + + function deposit(uint256 _amount0, uint256 _amount1, bool _zapFunds, bool _sweep, uint256 _sqrtRatioX96, uint256 _tolerance) + public + virtual + override + checkSqrtPriceX96(_sqrtRatioX96, _tolerance) + nonReentrant + checkVaultNotPaused + returns (uint256, uint256) + { + return _pullFundsAndDeposit(_amount0, _amount1, _zapFunds, 0, 0, 0); + } + + function deposit( + uint256 _amount0, + uint256 _amount1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _zapAmount0OutMin, + uint256 _zapAmount1OutMin, + uint160 _zapSqrtPriceLimitX96 + ) + public + virtual + override + checkSqrtPriceX96(_sqrtRatioX96, _tolerance) + nonReentrant + checkVaultNotPaused + returns (uint256, uint256) + { + return _pullFundsAndDeposit(_amount0, _amount1, _zapFunds, _zapAmount0OutMin, _zapAmount1OutMin, _zapSqrtPriceLimitX96); + } + + function _pullFundsAndDeposit( + uint256 _amount0, + uint256 _amount1, + bool _zapFunds, + uint256 _zapAmount0OutMin, + uint256 _zapAmount1OutMin, + uint160 _zapSqrtPriceLimitX96 + ) internal returns (uint256, uint256) { + // it is possible for _amount0 and _amount1 to be 0, so no need for requirement statement. + token0().safeTransferFrom(msg.sender, address(this), _amount0); + token1().safeTransferFrom(msg.sender, address(this), _amount1); + // deposit tokens + return _deposit(msg.sender, _zapFunds, _zapAmount0OutMin, _zapAmount1OutMin, _zapSqrtPriceLimitX96); + } + + /** + * @dev Handles the deposit + * @param _to the address of who the shares should be minted to + */ + function _deposit( + address _to, + bool _zapFunds, + uint256 _zapAmount0OutMin, + uint256 _zapAmount1OutMin, + uint160 _zapSqrtPriceLimitX96 + ) internal returns (uint256, uint256) { + // zap funds if desired + (uint256 zapActualAmountOut0, uint256 zapActualAmountOut1) = (0, 0); + if (_zapFunds) { + (zapActualAmountOut0, zapActualAmountOut1) = _zap(_zapAmount0OutMin, _zapAmount1OutMin, _zapSqrtPriceLimitX96); + } + + // the assumes that the deposits are already in + // they were transferred in by the deposit method, and potentially zap-swapped + uint256 previousUnderlyingBalanceWithInvestment = IUniVaultV1(address(this)).underlyingBalanceWithInvestment(); + uint256 _amount0 = token0().balanceOf(address(this)); + uint256 _amount1 = token1().balanceOf(address(this)); + // approvals for the liquidity increase + token0().safeApprove(_NFT_POSITION_MANAGER, 0); + token0().safeApprove(_NFT_POSITION_MANAGER, _amount0); + token1().safeApprove(_NFT_POSITION_MANAGER, 0); + token1().safeApprove(_NFT_POSITION_MANAGER, _amount1); + // increase the liquidity + (uint128 _liquidity,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: getStorage().posId(), + amount0Desired: _amount0, + amount1Desired: _amount1, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + emit Deposit(_to, _amount0, _amount1); + // mint shares of the vault to the recipient + uint256 toMint = IERC20Upgradeable(address(this)).totalSupply() == 0 + ? uint256(_liquidity) + : (uint256(_liquidity)).mul(IERC20Upgradeable(address(this)).totalSupply()).div(previousUnderlyingBalanceWithInvestment); + _mint(_to, toMint); + _transferLeftOverTo(msg.sender); + return (zapActualAmountOut0, zapActualAmountOut1); + } + + // The function is not registered in the interface of mother vault, + // thus regular users cannot invoke this. + function compoundRewards() public {} + + // _compoundRewards is registered as an interface in the mother vault, + // but using the onlySelf modifier, this acts as an internal function. + // the reason to expose this and called only be this address via external call + // is to enable the try catch structure + function _compoundRewards() external override onlySelf {} + + // Not registered, only callable in the mother vault level. + function collectFeesAndCompound() public { + try this._collectFeesAndCompound() {} + catch { + emit SharePriceChangeTrading( + IUniVaultV1(address(this)).getPricePerFullShare(), + IUniVaultV1(address(this)).getPricePerFullShare(), + getUint256(_LAST_FEE_COLLECTION_SLOT), + block.timestamp + ); + setUint256(_LAST_FEE_COLLECTION_SLOT, block.timestamp); + } + } + + // registered but guarded by onlySelf to act as internal + function _collectFeesAndCompound() external override onlySelf { + require(getStorage().feeRewardForwarder() != address(0), "feeRewardForwarder not set"); + require(getStorage().platformTarget() != address(0), "platformTarget not set"); + + uint256 token0FromReward = token0().balanceOf(address(this)); + uint256 token1FromReward = token1().balanceOf(address(this)); + emit RewardAmountClaimed(token0FromReward, token1FromReward, block.timestamp); + + // collect the earned fees + (uint256 _collected0, uint256 _collected1) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).collect( + INonfungiblePositionManager.CollectParams({ + tokenId: getStorage().posId(), + recipient: address(this), + amount0Max: uint128(-1), // collect all token0 + amount1Max: uint128(-1) // collect all token1 + }) + ); + emit SwapFeeClaimed(_collected0, _collected1, block.timestamp); + + _collected0 = token0FromReward.add(_collected0); + _collected1 = token1FromReward.add(_collected1); + + // handles profit sharing calculations and notifications + uint256 _profitRatio0 = _collected0.mul(getStorage().profitShareRatio()).div(10000); + uint256 _profitRatio1 = _collected1.mul(getStorage().profitShareRatio()).div(10000); + uint256 _platformRatio0 = _collected0.mul(getStorage().platformRatio()).div(10000); + uint256 _platformRatio1 = _collected1.mul(getStorage().platformRatio()).div(10000); + if (_profitRatio0 != 0) { + token0().safeApprove(getStorage().feeRewardForwarder(), 0); + token0().safeApprove(getStorage().feeRewardForwarder(), _profitRatio0); + IFeeRewardForwarderV6(getStorage().feeRewardForwarder()).poolNotifyFixedTarget(getStorage().token0(), _profitRatio0); + } + if (_profitRatio1 != 0) { + token1().safeApprove(getStorage().feeRewardForwarder(), 0); + token1().safeApprove(getStorage().feeRewardForwarder(), _profitRatio1); + + IFeeRewardForwarderV6(getStorage().feeRewardForwarder()).poolNotifyFixedTarget(getStorage().token1(), _profitRatio1); + } + if (_platformRatio0 != 0) { + token0().safeTransfer(getStorage().platformTarget(), _platformRatio0); + } + if (_platformRatio1 != 0) { + token1().safeTransfer(getStorage().platformTarget(), _platformRatio1); + } + + // zap the tokens to the proper ratio + _zap(0, 0, 0); + + // increase liquidity + // this will increase the underlyingBalanceWithInvestment and getPricePerFullShare + // while totalSupply remains the same. + + token0().safeApprove(_NFT_POSITION_MANAGER, 0); + token0().safeApprove(_NFT_POSITION_MANAGER, token0().balanceOf(address(this))); + token1().safeApprove(_NFT_POSITION_MANAGER, 0); + token1().safeApprove(_NFT_POSITION_MANAGER, token1().balanceOf(address(this))); + + uint256 sharePriceBefore = IUniVaultV1(address(this)).getPricePerFullShare(); + INonfungiblePositionManager(_NFT_POSITION_MANAGER).increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: getStorage().posId(), + amount0Desired: token0().balanceOf(address(this)), + amount1Desired: token1().balanceOf(address(this)), + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + + emit SharePriceChangeTrading( + sharePriceBefore, + IUniVaultV1(address(this)).getPricePerFullShare(), + getUint256(_LAST_FEE_COLLECTION_SLOT), + block.timestamp + ); + + // Set new timestamp + setUint256(_LAST_FEE_COLLECTION_SLOT, block.timestamp); + } + + function withdraw(uint256 _numberOfShares, bool _token0, bool _token1, uint256 _sqrtRatioX96, uint256 _tolerance) + public + virtual + override + checkSqrtPriceX96(_sqrtRatioX96, _tolerance) + nonReentrant + checkVaultNotPaused + returns (uint256, uint256) + { + return _withdraw(_numberOfShares, _token0, _token1, 0, 0, 0); + } + + function withdraw( + uint256 _numberOfShares, + bool _token0, + bool _token1, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _amount0OutMin, + uint256 _amount1OutMin, + uint160 _sqrtPriceLimitX96 + ) + public + virtual + override + checkSqrtPriceX96(_sqrtRatioX96, _tolerance) + nonReentrant + checkVaultNotPaused + returns (uint256, uint256) + { + return _withdraw(_numberOfShares, _token0, _token1, _amount0OutMin, _amount1OutMin, _sqrtPriceLimitX96); + } + + function _withdraw( + uint256 _numberOfShares, + bool _token0, + bool _token1, + uint256 _amount0OutMin, + uint256 _amount1OutMin, + uint160 _sqrtPriceLimitX96 + ) internal returns (uint256, uint256) { + require(_token0 || _token1, "At least one side must be wanted"); + // calculates the liquidity before burning any shares + uint128 liquidityShare = uint128( + (IUniVaultV1(address(this)).underlyingBalanceWithInvestment()).mul(_numberOfShares).div( + IERC20Upgradeable(address(this)).totalSupply() + ) + ); + // burn the respective shares + // guards the balance via safe math in burn + _burn(msg.sender, _numberOfShares); + // withdraw liquidity from the NFT + (uint256 _receivedToken0, uint256 _receivedToken1) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: getStorage().posId(), + liquidity: liquidityShare, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + // collect the amount fetched above + INonfungiblePositionManager(_NFT_POSITION_MANAGER).collect( + INonfungiblePositionManager.CollectParams({ + tokenId: getStorage().posId(), + recipient: address(this), + amount0Max: uint128(_receivedToken0), // collect all token0 accounted for the liquidity + amount1Max: uint128(_receivedToken1) // collect all token1 accounted for the liquidity + }) + ); + + (uint256 zapActualAmountOut0, uint256 zapActualAmountOut1) = (0, 0); + + // make swaps as desired + if (!_token0) { + uint256 balance0 = token0().balanceOf(address(this)); + if (balance0 > 0) { + (zapActualAmountOut0, zapActualAmountOut1) = _swap(balance0, 0, 0, _amount1OutMin, _sqrtPriceLimitX96); + } + } + if (!_token1) { + uint256 balance1 = token1().balanceOf(address(this)); + if (balance1 > 0) { + (zapActualAmountOut0, zapActualAmountOut1) = _swap(0, balance1, _amount0OutMin, 0, _sqrtPriceLimitX96); + } + } + + emit Withdraw(msg.sender, _receivedToken0, _receivedToken1); + // transfer everything we have in the contract to msg.sender + _transferLeftOverTo(msg.sender); + return (zapActualAmountOut0, zapActualAmountOut1); + } + + /** + * @dev Zap takes in what users provided and calculates an approximately balanced amount for + * liquidity provision. It then swaps one token to another to make the tokens balanced. + * To reduce the size of the bytecode for this contract, zap is an independent contract + * that we make a delegatecall to. + */ + function _zap(uint256 _amount0OutMin, uint256 _amount1OutMin, uint160 _sqrtPriceLimitX96) + internal + returns (uint256, uint256) + { + uint256 _originalAmount0 = token0().balanceOf(address(this)); + uint256 _originalAmount1 = token1().balanceOf(address(this)); + (bool success, bytes memory data) = getStorage().zapContract().delegatecall( + abi.encodeWithSignature( + "zap(uint160,uint256,uint256,uint256,uint256)", + _sqrtPriceLimitX96, + _originalAmount0, + _originalAmount1, + _amount0OutMin, + _amount1OutMin + ) + ); + if (success) { + return abi.decode(data, (uint256, uint256)); + } + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + return (0, 0); + } + + /** + * @dev Swap takes in token amount and swap it to another. This code resides in the zap contract. + * To reduce the size of the bytecode for this contract, zap is an independent contract + * that we make a delegatecall to. + */ + function _swap( + uint256 _amount0In, + uint256 _amount1In, + uint256 _amount0OutMin, + uint256 _amount1OutMin, + uint256 _sqrtPriceLimitX96 + ) internal returns (uint256, uint256) { + (bool success, bytes memory data) = getStorage().zapContract().delegatecall( + abi.encodeWithSignature( + "swap(uint160,uint256,uint256,uint256,uint256)", + _sqrtPriceLimitX96, + _amount0In, + _amount1In, + _amount0OutMin, + _amount1OutMin + ) + ); + if (success) { + return abi.decode(data, (uint256, uint256)); + } + string memory revertMsg = data._getRevertMsgFromRes(); + require(success, revertMsg); + return (0, 0); + } + + /** + * @dev Convenience getter for the data contract. + */ + function getStorage() public view returns (IUniVaultStorageV1) { + return IUniVaultStorageV1(getAddress(_DATA_CONTRACT_SLOT)); + } + + /** + * @dev Convenience getter for token0. + */ + function token0() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token0()); + } + + /** + * @dev Convenience getter for token1. + */ + function token1() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token1()); + } + + function sweepDust() external override onlyControllerOrGovernance { + _transferLeftOverTo(msg.sender); + } + + /** + * @dev Handles transferring the leftovers + */ + function _transferLeftOverTo(address _to) internal { + uint256 balance0 = token0().balanceOf(address(this)); + uint256 balance1 = token1().balanceOf(address(this)); + if (balance0 > 0) { + token0().safeTransfer(_to, balance0); + } + if (balance1 > 0) { + token1().safeTransfer(_to, balance1); + } + } + + /** + * @dev Sets an address to a slot. + */ + function setAddress(bytes32 slot, address _address) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _address) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getAddress(bytes32 slot) internal view returns (address str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Sets a value to a slot. + */ + function setUint256(bytes32 slot, uint256 _value) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _value) + } + } + + /** + * @dev Reads an uint from a slot. + */ + function getUint256(bytes32 slot) internal view returns (uint256 str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Sets an boolean to a slot. + */ + function setBoolean(bytes32 slot, bool _flag) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _flag) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getBoolean(bytes32 slot) internal view returns (bool str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Convenience getter for the current sqrtPriceX96 of the Uniswap pool. + */ + function getSqrtPriceX96() public view returns (uint160) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } +} diff --git a/src/submodules/UniVaultSubModuleDepositV2Managed.sol b/src/submodules/UniVaultSubModuleDepositV2Managed.sol new file mode 100644 index 0000000..38c5344 --- /dev/null +++ b/src/submodules/UniVaultSubModuleDepositV2Managed.sol @@ -0,0 +1,206 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +import "../interface/oracle/IDexPriceAggregator.sol"; +import "./UniVaultSubModuleDepositV1.sol"; +import "./interface/IUniStatusViewer.sol"; + +contract UniVaultSubmoduleDepositV2Managed is UniVaultSubmoduleDepositV1 { + bytes32 internal constant _LOCAL_STORAGE_SLOT_V2_MANAGED = 0xdd1c3014cf51b49660316473e7024b762c61f45d6f2f14c569c080603d95a229; + address internal constant DEX_PRICE_AGGREGATOR = 0x813A5C304b8E37fA98F43A33DCCf60fA5cDb8739; + + using SafeMathUpgradeable for uint256; + + struct LocalStorageV2Managed { + bool depositCapReached; + uint256 withdrawalTimestamp; + uint256 capInCapToken; + address capToken; // token for capInCapToken + uint256 capTokenIndex; // 0 for capToken = token0 and 1 for capToken = token1 + address otherToken; // the "other" token: if capToken is token0, then otherToken is token1 + uint256 otherTokenIndex; // 0 if otherToken = token0, and 1 if otherToken = token1 + } + + modifier checkTimestamp() { + require(block.timestamp >= getWithdrawalTimestamp(), "Locked with timestamp"); + _; + } + + modifier checkDepositCap() { + require(!depositCapReached(), "Deposit cap reached"); + _; + if (checkCap()) { + setDepositCapReached(); + } + } + + constructor() { + assert( + _LOCAL_STORAGE_SLOT_V2_MANAGED + == bytes32(uint256(keccak256("eip1967.uniVault.depositSubmodule.localStorageV2Managed")) - 1) + ); + } + + function getLocalStorageV2Managed() internal pure returns (LocalStorageV2Managed storage localStorage) { + assembly { + localStorage.slot := _LOCAL_STORAGE_SLOT_V2_MANAGED + } + } + + function registerInterface(bytes32 submoduleHash, bool register) public override { + super.registerInterface(submoduleHash, register); + address currentSubmoduleAddress = getGlobalStorage().submoduleAddress[submoduleHash]; + + uint256 functionNum = 8; + bytes4[8] memory functionLists = [ + bytes4(keccak256("setWithdrawalTimestamp(uint256)")), + bytes4(keccak256("resetDepositCapReached(bool)")), + bytes4(keccak256("checkCap()")), + bytes4(keccak256("getCap()")), + bytes4(keccak256("currentValueInCapToken()")), + bytes4(keccak256("setDepositCap(uint256,address)")), + bytes4(keccak256("getWithdrawalTimestamp()")), + bytes4(keccak256("depositCapReached()")) + ]; + + address registeringSubmodule = (register) ? currentSubmoduleAddress : address(0); + for (uint256 i = 0; i < functionNum; i++) { + getGlobalStorage().functionToSubmodule[functionLists[i]] = registeringSubmodule; + } + } + + function withdraw(uint256, bool, bool, uint256, uint256) public override returns (uint256, uint256) { + revert("please use new interface for withdraw()"); + } + + function withdraw( + uint256 _numberOfShares, + bool _token0, + bool _token1, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _amount0OutMin, + uint256 _amount1OutMin, + uint160 _sqrtPriceLimitX96 + ) public override checkTimestamp returns (uint256, uint256) { + return super.withdraw( + _numberOfShares, _token0, _token1, _sqrtRatioX96, _tolerance, _amount0OutMin, _amount1OutMin, _sqrtPriceLimitX96 + ); + } + + function setWithdrawalTimestamp(uint256 _timestamp) public onlyGovernance { + require(block.timestamp >= getWithdrawalTimestamp() + 24 hours, "Wait for this lock to expire"); + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + localStorage.withdrawalTimestamp = _timestamp; + } + + function setDepositCap(uint256 _capInCapToken, address _capToken) public onlyGovernance { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + require(_capToken == address(token0()) || _capToken == address(token1()), "_capToken must be token0 or token1"); + localStorage.capInCapToken = _capInCapToken; + localStorage.capToken = _capToken; + localStorage.capTokenIndex = _capToken == address(token0()) ? 0 : 1; + localStorage.otherToken = _capToken == address(token0()) ? address(token1()) : address(token0()); + localStorage.otherTokenIndex = _capToken == address(token1()) ? 0 : 1; + } + + function getWithdrawalTimestamp() public view returns (uint256) { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + return localStorage.withdrawalTimestamp; + } + + function depositCapReached() public view returns (bool) { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + return localStorage.depositCapReached; + } + + function getCap() public view returns (uint256 cap, address capToken) { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + cap = localStorage.capInCapToken; + capToken = localStorage.capToken; + } + + function setDepositCapReached() internal { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + localStorage.depositCapReached = true; + } + + function resetDepositCapReached(bool value) public onlyGovernance { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + localStorage.depositCapReached = value; + } + + function currentValueInCapToken() public view returns (uint256) { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + require(localStorage.capInCapToken > 0, "Cap not configured"); + (uint256 current0, uint256 current1) = + IUniStatusViewer(0x6e87AbD51CCB2290EE1ff65C3749C9420a0cD36E).getAmountsForPosition(getStorage().posId()); + uint256[2] memory currentBalance = + [current0.add(token0().balanceOf(address(this))), current1.add(token1().balanceOf(address(this)))]; + + uint256 outputAmount = 0; + if (currentBalance[localStorage.otherTokenIndex] > 0) { + outputAmount = IDexPriceAggregator(DEX_PRICE_AGGREGATOR).assetToAsset( + localStorage.otherToken, currentBalance[localStorage.otherTokenIndex], localStorage.capToken, 1800 + ); + } + return currentBalance[localStorage.capTokenIndex].add(outputAmount); + } + + function checkCap() public view returns (bool) { + LocalStorageV2Managed storage localStorage = getLocalStorageV2Managed(); + if (localStorage.capInCapToken == 0) { + return true; // the cap is not enforced if 0 + } + // return true IF REACHED or EXCEEDED + return currentValueInCapToken() >= localStorage.capInCapToken; + } + + function deposit(uint256, uint256, bool, bool, uint256, uint256) public override returns (uint256, uint256) { + revert("please use new interface for deposit()"); + } + + function deposit( + uint256 _amount0, + uint256 _amount1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _zapAmount0OutMin, + uint256 _zapAmount1OutMin, + uint160 _zapSqrtPriceLimitX96 + ) public override checkDepositCap returns (uint256, uint256) { + return super.deposit( + _amount0, _amount1, _zapFunds, _sqrtRatioX96, _tolerance, _zapAmount0OutMin, _zapAmount1OutMin, _zapSqrtPriceLimitX96 + ); + } + + function migrateToNftFromV2(uint256, uint256, uint256, bool, uint256, uint256) public override returns (uint256, uint256) { + revert("please use new interface for migrateToNftFromV2"); + } + + function migrateToNftFromV2( + uint256 _amount, + uint256 _minAmountForRemoveLiquidity0, + uint256 _minAmountForRemoveLiquidity1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _amount0OutMinForZap, + uint256 _amount1OutMinForZap, + uint160 _sqrtPriceLimitX96 + ) public override checkDepositCap returns (uint256, uint256) { + return super.migrateToNftFromV2( + _amount, + _minAmountForRemoveLiquidity0, + _minAmountForRemoveLiquidity1, + _zapFunds, + _sqrtRatioX96, + _tolerance, + _amount0OutMinForZap, + _amount1OutMinForZap, + _sqrtPriceLimitX96 + ); + } +} diff --git a/src/submodules/UniVaultSubmoduleRewardV1LiquidateOnly.sol b/src/submodules/UniVaultSubmoduleRewardV1LiquidateOnly.sol new file mode 100644 index 0000000..b6411f8 --- /dev/null +++ b/src/submodules/UniVaultSubmoduleRewardV1LiquidateOnly.sol @@ -0,0 +1,231 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +// reentrancy guard +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +// Harvest System +import "../interface/IFeeRewardForwarderV6.sol"; +import "../inheritance/ControllableInit.sol"; + +// Storage for this UniVault +import "../interface/IUniVaultStorageV1.sol"; + +// BytesLib +import "../lib/BytesLib.sol"; + +// UL +import "../interface/IUniversalLiquidatorRegistry.sol"; +import "../interface/IUniversalLiquidator.sol"; + +contract UniVaultSubmoduleRewardV1LiquidateOnly is ReentrancyGuardUpgradeable, ControllableInit { + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + using BytesLib for bytes; + + // We expect all reward submodule to have this reward token list + bytes32 internal constant _GLOBAL_STORAGE_SLOT = 0xea3b316d3f7b97449bd56fdd0c7f95b3a874cb501e4c5d85e01bf23443a180c8; + bytes32 internal constant _LOCAL_STORAGE_SLOT = 0x35ecaa58d3f4a450860c27672873a69d5a15a6ad65d4531955c5d03adc76b07e; + bytes32 internal constant _DATA_CONTRACT_SLOT = 0x4c2252f3318958b38b23790562cc9d391075b8dadbfe0e707aed11afe13228b7; + bytes32 internal constant _VAULT_PAUSE_SLOT = 0xde039a7c768eade9368187c932ab7b9ca8d5872604278b5ebfe45ab5eaf85140; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + + address internal constant _UL_REGISTRY_ADDRESS = 0x7882172921E99d590E097cD600554339fBDBc480; + + struct LocalStorage { + address[] tokens; + mapping(address => bool) tokenExists; + mapping(address => bytes32[]) dexes; + mapping(address => address[]) path; + } + + // This is the storage for the mother level vault + // it is used here to register / deregister interface + struct GlobalStorage { + // direct mapping from function signature to submodule address + mapping(bytes4 => address) functionToSubmodule; + // Mapping from submodule hash to submodule, this is used for internal delegate calls as in doHardwork + // where we know what submodules we are calling, but those functions are not open to the public + mapping(bytes32 => address) submoduleAddress; + } + + /** + * @dev Ensures that the price between submitting the transaction and when + * the transaction gets mined did not move (checks slippage) + */ + modifier checkSqrtPriceX96(uint256 offChainSqrtPriceX96, uint256 tolerance) { + uint256 current = uint256(getSqrtPriceX96()); + uint256 step = offChainSqrtPriceX96.mul(tolerance).div(1000); + require(current < offChainSqrtPriceX96.add(step), "Price too high"); + require(current > offChainSqrtPriceX96.sub(step), "Price too low"); + _; + } + + modifier checkVaultNotPaused() { + require(!getBoolean(_VAULT_PAUSE_SLOT), "Vault is paused"); + _; + } + + modifier onlySelf() { + require(msg.sender == address(this), "only for internal"); + _; + } + + constructor() { + assert(_LOCAL_STORAGE_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.rewardSubmodule.localStorage")) - 1)); + } + + function getGlobalStorage() internal pure returns (GlobalStorage storage globalStorage) { + assembly { + globalStorage.slot := _GLOBAL_STORAGE_SLOT + } + } + + function registerInterface(bytes32 submoduleHash, bool register) public { + GlobalStorage storage globalStorage = getGlobalStorage(); + address currentSubmoduleAddress = globalStorage.submoduleAddress[submoduleHash]; + + uint256 functionNum = 4; + bytes4[4] memory functionLists = [ + bytes4(keccak256("rewardToken(uint256)")), + bytes4(keccak256("setLiquidationPath(address,bytes32[],address[])")), + bytes4(keccak256("addRewardTokens(address[])")), + bytes4(keccak256("removeRewardToken(uint256)")) + ]; + + address registeringSubmodule = (register) ? currentSubmoduleAddress : address(0); + for (uint256 i = 0; i < functionNum; i++) { + globalStorage.functionToSubmodule[functionLists[i]] = registeringSubmodule; + } + } + + function rewardToken(uint256 i) public view returns (address) { + LocalStorage storage localStorage = getLocalStorage(); + return localStorage.tokens[i]; + } + + // rewardToken to liquidate + // guarded by onlyGovernance + function setLiquidationPath(address rewardToken, bytes32[] memory _dexes, address[] memory _path) public onlyGovernance { + LocalStorage storage localStorage = getLocalStorage(); + localStorage.dexes[rewardToken] = _dexes; + localStorage.path[rewardToken] = _path; + } + + // Guarded by onlyGovernance in the main contract. + function addRewardTokens(address[] memory newTokens) public onlyGovernance { + LocalStorage storage localStorage = getLocalStorage(); + uint256 newTokenLength = newTokens.length; + for (uint256 i = 0; i < newTokenLength; i++) { + require(!localStorage.tokenExists[newTokens[i]], "Token already exists"); + require( + (newTokens[i] != address(getStorage().token0())) && (newTokens[i] != address(getStorage().token1())), + "Reward token cannot be token0/token1" + ); + localStorage.tokens.push(newTokens[i]); + localStorage.tokenExists[newTokens[i]] = true; + } + } + + // Guarded by onlyGovernance in the main contract. + function removeRewardToken(uint256 i) public onlyGovernance { + LocalStorage storage localStorage = getLocalStorage(); + require(i < localStorage.tokens.length, "i out of bounds"); + address removedToken = localStorage.tokens[i]; + localStorage.tokens[i] = localStorage.tokens[localStorage.tokens.length - 1]; + localStorage.tokens.pop(); + localStorage.tokenExists[removedToken] = false; + } + + // For stakewise, there are multiple rewards + // We assume that they are already sent to vault by the bot. + function doHardwork_preprocessForReward() public { + _liquidateRewards(); + } + + function _liquidateRewards() internal { + LocalStorage storage localStorage = getLocalStorage(); + + uint256 rewardCount = localStorage.tokens.length; + uint256 profitShareRatio = getStorage().profitShareRatio(); + + for (uint256 i = 0; i < rewardCount; i++) { + address rewardToken = localStorage.tokens[i]; + uint256 remainingAmount = IERC20Upgradeable(rewardToken).balanceOf(address(this)); + if (remainingAmount > 0) { + address ulAddress = IUniversalLiquidatorRegistry(_UL_REGISTRY_ADDRESS).universalLiquidator(); + + IERC20Upgradeable(rewardToken).safeApprove(ulAddress, 0); + IERC20Upgradeable(rewardToken).safeApprove(ulAddress, remainingAmount); + IUniversalLiquidator(ulAddress).swapTokenOnMultipleDEXes( + remainingAmount, 0, address(this), localStorage.dexes[rewardToken], localStorage.path[rewardToken] + ); + } + } + } + + function doHardwork_postprocessForReward() public {} + + function getLocalStorage() internal pure returns (LocalStorage storage localStorage) { + assembly { + localStorage.slot := _LOCAL_STORAGE_SLOT + } + } + + /** + * @dev Convenience getter for the data contract. + */ + function getStorage() public view returns (IUniVaultStorageV1) { + return IUniVaultStorageV1(getAddress(_DATA_CONTRACT_SLOT)); + } + + /** + * @dev Reads an address from a slot. + */ + function getAddress(bytes32 slot) internal view returns (address str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Sets an boolean to a slot. + */ + function setBoolean(bytes32 slot, bool _flag) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _flag) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getBoolean(bytes32 slot) internal view returns (bool str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Convenience getter for the current sqrtPriceX96 of the Uniswap pool. + */ + function getSqrtPriceX96() public view returns (uint160) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } +} diff --git a/src/submodules/interface/IUniStatusViewer.sol b/src/submodules/interface/IUniStatusViewer.sol new file mode 100644 index 0000000..12f944b --- /dev/null +++ b/src/submodules/interface/IUniStatusViewer.sol @@ -0,0 +1,6 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; + +interface IUniStatusViewer { + function getAmountsForPosition(uint256 posId) external view returns (uint256 amount0, uint256 amount1); +} diff --git a/src/submodules/interface/IUniVaultSubmoduleChangeRangeV1.sol b/src/submodules/interface/IUniVaultSubmoduleChangeRangeV1.sol new file mode 100644 index 0000000..b57b45f --- /dev/null +++ b/src/submodules/interface/IUniVaultSubmoduleChangeRangeV1.sol @@ -0,0 +1,7 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +interface IUniVaultSubmoduleChangeRangeV1 { + function changeRange(uint256 newPosId) external; +} diff --git a/src/submodules/interface/IUniVaultSubmoduleChangeRangeV2Managed.sol b/src/submodules/interface/IUniVaultSubmoduleChangeRangeV2Managed.sol new file mode 100644 index 0000000..949eb54 --- /dev/null +++ b/src/submodules/interface/IUniVaultSubmoduleChangeRangeV2Managed.sol @@ -0,0 +1,8 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +interface IUniVaultSubmoduleChangeRangeV2Managed { + function addPositionIds(uint256[] calldata newPosIds) external; + function getAmountsForPosition(uint256 posId) external view returns (uint256 amount0, uint256 amount1); +} diff --git a/src/submodules/interface/IUniVaultSubmoduleDepositV1.sol b/src/submodules/interface/IUniVaultSubmoduleDepositV1.sol new file mode 100644 index 0000000..4359eb1 --- /dev/null +++ b/src/submodules/interface/IUniVaultSubmoduleDepositV1.sol @@ -0,0 +1,60 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +interface IUniVaultSubmoduleDepositV1 { + function migrateToNftFromV2( + uint256 _amount, + uint256 _minAmountForRemoveLiquidity0, + uint256 _minAmountForRemoveLiquidity1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance + ) external returns (uint256, uint256); + + function migrateToNftFromV2( + uint256 _amount, + uint256 _minAmountForRemoveLiquidity0, + uint256 _minAmountForRemoveLiquidity1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _amount0OutMinForZap, + uint256 _amount1OutMinForZap, + uint160 _sqrtPriceLimitX96 + ) external returns (uint256, uint256); + + function deposit(uint256 _amount0, uint256 _amount1, bool _zapFunds, bool _sweep, uint256 _sqrtRatioX96, uint256 _tolerance) + external + returns (uint256, uint256); + + function deposit( + uint256 _amount0, + uint256 _amount1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _zapAmount0OutMin, + uint256 _zapAmount1OutMin, + uint160 _zapSqrtPriceLimitX96 + ) external returns (uint256, uint256); + + function withdraw(uint256 _numberOfShares, bool _token0, bool _token1, uint256 _sqrtRatioX96, uint256 _tolerance) + external + returns (uint256, uint256); + + function withdraw( + uint256 _numberOfShares, + bool _token0, + bool _token1, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _amount0OutMin, + uint256 _amount1OutMin, + uint160 _sqrtPriceLimitX96 + ) external returns (uint256, uint256); + + function _collectFeesAndCompound() external; + function _compoundRewards() external; + function sweepDust() external; +} diff --git a/src/submodules/interface/IUniVaultSubmoduleDepositV2.sol b/src/submodules/interface/IUniVaultSubmoduleDepositV2.sol new file mode 100644 index 0000000..4694e1e --- /dev/null +++ b/src/submodules/interface/IUniVaultSubmoduleDepositV2.sol @@ -0,0 +1,43 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +interface IUniVaultSubmoduleDepositV2 { + function migrateToNftFromV2( + uint256 _amount, + uint256 _minAmountForRemoveLiquidity0, + uint256 _minAmountForRemoveLiquidity1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _amount0OutMinForZap, + uint256 _amount1OutMinForZap, + uint160 _sqrtPriceLimitX96 + ) external returns (uint256, uint256); + + function deposit( + uint256 _amount0, + uint256 _amount1, + bool _zapFunds, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _zapAmount0OutMin, + uint256 _zapAmount1OutMin, + uint160 _zapSqrtPriceLimitX96 + ) external returns (uint256, uint256); + + function withdraw( + uint256 _numberOfShares, + bool _token0, + bool _token1, + uint256 _sqrtRatioX96, + uint256 _tolerance, + uint256 _amount0OutMin, + uint256 _amount1OutMin, + uint160 _sqrtPriceLimitX96 + ) external returns (uint256, uint256); + + function _collectFeesAndCompound() external; + function _compoundRewards() external; + function sweepDust() external; +} diff --git a/src/submodules/interface/IUniVaultSubmoduleRewardV1LiquidateOnly.sol b/src/submodules/interface/IUniVaultSubmoduleRewardV1LiquidateOnly.sol new file mode 100644 index 0000000..ef82e16 --- /dev/null +++ b/src/submodules/interface/IUniVaultSubmoduleRewardV1LiquidateOnly.sol @@ -0,0 +1,10 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +interface IUniVaultSubmoduleRewardV1LiquidateOnly { + function rewardToken(uint256 i) external view returns (address); + function setLiquidationPath(address rewardToken, bytes32[] memory _dexes, address[] memory _path) external; + function addRewardTokens(address[] memory newTokens) external; + function removeRewardToken(uint256 i) external; +} diff --git a/src/utility/UniVaultUpgradeableV1SoleLPqfCR.sol b/src/utility/UniVaultUpgradeableV1SoleLPqfCR.sol new file mode 100644 index 0000000..33623ad --- /dev/null +++ b/src/utility/UniVaultUpgradeableV1SoleLPqfCR.sol @@ -0,0 +1,409 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +import "hardhat/console.sol"; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// ERC721 +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +// reentrancy guard +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; + +// Harvest System +import "../interface/IFeeRewardForwarderV6.sol"; +import "../inheritance/ControllableInit.sol"; + +// Storage for this UniVault +import "../interface/IUniVaultStorageV1.sol"; + +// quick fix for Changing Range +contract UniVaultUpgradeableV1SoleLPqfCR is + ERC20Upgradeable, + ERC721HolderUpgradeable, + ReentrancyGuardUpgradeable, + ControllableInit +{ + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + + bytes32 internal constant _DATA_CONTRACT_SLOT = 0x4c2252f3318958b38b23790562cc9d391075b8dadbfe0e707aed11afe13228b7; + address internal constant _NFT_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + uint256 internal constant _UNDERLYING_UNIT = 1e18; + + /** + * @dev Ensures that the price between submitting the transaction and when + * the transaction gets mined did not move (checks slippage) + */ + modifier checkSqrtPriceX96(uint256 offChainSqrtPriceX96, uint256 tolerance) { + uint256 current = uint256(getSqrtPriceX96()); + uint256 step = offChainSqrtPriceX96.mul(tolerance).div(1000); + require(current < offChainSqrtPriceX96.add(step), "Price too high"); + require(current > offChainSqrtPriceX96.sub(step), "Price too low"); + _; + } + + /** + * @dev Creates the contract and ensures that the slot matches the hash + */ + constructor() { + assert(_DATA_CONTRACT_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.dataContract")) - 1)); + } + + // Stripped down most implementation to save contract size. + + // creates a new position with the designated range + function mintPosition(int24 _tickLower, int24 _tickUpper, uint256 _initAmount0, uint256 _initAmount1) public onlyGovernance { + token0().safeTransferFrom(msg.sender, address(this), _initAmount0); + token1().safeTransferFrom(msg.sender, address(this), _initAmount1); + token0().safeApprove(_NFT_POSITION_MANAGER, 0); + token0().safeApprove(_NFT_POSITION_MANAGER, _initAmount0); + token1().safeApprove(_NFT_POSITION_MANAGER, 0); + token1().safeApprove(_NFT_POSITION_MANAGER, _initAmount1); + + (uint256 _tokenId, uint128 _initialLiquidity,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).mint( + INonfungiblePositionManager.MintParams({ + token0: address(token0()), + token1: address(token1()), + fee: getStorage().fee(), + tickLower: _tickLower, + tickUpper: _tickUpper, + amount0Desired: _initAmount0, // amount0Desired + amount1Desired: _initAmount1, // amount1Desired + amount0Min: 0, // amount0Min, first small deposit, can stay 0 + amount1Min: 0, // amount1Min, first small deposit, can stay 0 + recipient: address(this), + deadline: block.timestamp // will be done on this block. + }) + ); + } + + function _migrateLiquidity(uint128 liquidityAmount, uint256 newPosId, uint24 swapInPoolwithFee) internal { + // remove liquidity from old + (uint256 _receivedToken0, uint256 _receivedToken1) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: getStorage().posId(), + liquidity: liquidityAmount, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + + // collect the amount fetched above + previous accumulated fees + INonfungiblePositionManager(_NFT_POSITION_MANAGER).collect( + INonfungiblePositionManager.CollectParams({ + tokenId: getStorage().posId(), + recipient: address(this), + amount0Max: uint128(-1), // collect all token0 (since we are changing ranges) + amount1Max: uint128(-1) // collect all token1 (since we are changing ranges) + }) + ); + + console.logInt(int256(getTick())); + // Zap will balance it to the new range + uint24 originalFee = getStorage().fee(); + if (swapInPoolwithFee != 0) { + getStorage().setFee(swapInPoolwithFee); + } + _zap(); + if (swapInPoolwithFee != 0) { + getStorage().setFee(originalFee); + } + console.logInt(int256(getTick())); + + // we should now put them all into the new position. + + uint256 _amount0 = token0().balanceOf(address(this)); + uint256 _amount1 = token1().balanceOf(address(this)); + // approvals for the liquidity increase + token0().safeApprove(_NFT_POSITION_MANAGER, 0); + token0().safeApprove(_NFT_POSITION_MANAGER, _amount0); + token1().safeApprove(_NFT_POSITION_MANAGER, 0); + token1().safeApprove(_NFT_POSITION_MANAGER, _amount1); + + // increase the liquidity + (uint128 _liquidity,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: newPosId, + amount0Desired: _amount0, + amount1Desired: _amount1, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + } + + function injectLiquidity(uint256 inject0, uint256 inject1) public { + if (inject0 != 0) { + token0().safeTransferFrom(msg.sender, address(this), inject0); + } + + if (inject1 != 0) { + token1().safeTransferFrom(msg.sender, address(this), inject1); + } + + uint256 _amount0 = token0().balanceOf(address(this)); + uint256 _amount1 = token1().balanceOf(address(this)); + // approvals for the liquidity increase + token0().safeApprove(_NFT_POSITION_MANAGER, 0); + token0().safeApprove(_NFT_POSITION_MANAGER, _amount0); + token1().safeApprove(_NFT_POSITION_MANAGER, 0); + token1().safeApprove(_NFT_POSITION_MANAGER, _amount1); + + // increase the liquidity + (uint128 _liquidity,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: getStorage().posId(), + amount0Desired: _amount0, + amount1Desired: _amount1, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + _transferLeftOverTo(msg.sender); + } + + // switch to the designated position + function changeRange(uint256 newPosId, uint128 loopNum, uint24 swapInPoolWithFee, uint256 inject0, uint256 inject1) + public + onlyGovernance + { + // If there is no such position, this will also fail. + require(IERC721Upgradeable(_NFT_POSITION_MANAGER).ownerOf(newPosId) == address(this), "pos owner mismatch"); + + // making sure that we are pointing to the same thing. + (,,,,,,, uint128 _oldLiquidity,,,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(getStorage().posId()); + // fetching information from new position + (,, address _t0, address _t1, uint24 _fee, int24 _newTickLower, int24 _newTickUpper,,,,,) = + INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(newPosId); + + require(_t0 == address(token0()) && _t1 == address(token1()) && _fee == getStorage().fee(), "pool mismatch"); + + //uint256 oldPosId = getStorage().posId(); + + // set new ranges so that the zap in migrate will work + getStorage().setTickLower(_newTickLower); + getStorage().setTickUpper(_newTickUpper); + + for (uint256 i = 0; i < loopNum; i++) { + console.log("i: ", loopNum); + _migrateLiquidity(_oldLiquidity / loopNum, newPosId, swapInPoolWithFee); + } + + // point the storge to the new Pos Id + getStorage().setPosId(newPosId); + + // then lets inject liquidity and sweep the rest. + injectLiquidity(inject0, inject1); + } + + /** + * @dev Returns the total liquidity stored in the position + * Dev Note: need to turn on the solc optimizer otherwise the compiler + * will throw stack too deep on this function + */ + function underlyingBalanceWithInvestment() public view returns (uint256) { + // note that the liquidity is not a token, so there is no local balance added + (,,,,,,, uint128 liquidity,,,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(getStorage().posId()); + return liquidity; + } + + /** + * @dev A dummy method to get the controller working + */ + function setStrategy(address unused) public onlyControllerOrGovernance { + // intentionally a no-op + } + + /** + * @dev A dummy method to get the controller working + */ + function strategy() public view returns (address) { + return address(this); + } + + /** + * @dev Returns the price per full share, scaled to 1e18 + */ + function getPricePerFullShare() public view returns (uint256) { + return totalSupply() == 0 ? _UNDERLYING_UNIT : _UNDERLYING_UNIT.mul(underlyingBalanceWithInvestment()).div(totalSupply()); + } + + /** + * @dev Allows governance to get dust that stays behind, or accidental token transfers. + * No tokens should stay in this contract. + */ + function sweepDust() external onlyControllerOrGovernance { + _transferLeftOverTo(msg.sender); + } + + /** + * @dev Handles transferring the leftovers + */ + function _transferLeftOverTo(address _to) internal { + uint256 balance0 = token0().balanceOf(address(this)); + uint256 balance1 = token1().balanceOf(address(this)); + if (balance0 > 0) { + token0().safeTransfer(_to, balance0); + } + if (balance1 > 0) { + token1().safeTransfer(_to, balance1); + } + } + + /** + * @dev Zap takes in what users provided and calculates an approximately balanced amount for + * liquidity provision. It then swaps one token to another to make the tokens balanced. + * To reduce the size of the bytecode for this contract, zap is an independent contract + * that we make a delegatecall to. + */ + function _zap() internal { + uint256 _originalAmount0 = token0().balanceOf(address(this)); + uint256 _originalAmount1 = token1().balanceOf(address(this)); + (bool success, bytes memory data) = getStorage().zapContract().delegatecall( + abi.encodeWithSignature("zap(uint256,uint256)", _originalAmount0, _originalAmount1) + ); + if (!success) { + // there is no return value, we do not want to decode is there is no failure + require(success, abi.decode(data, (string))); + } + } + + /** + * @dev Swap takes in token amount and swap it to another. This code resides in the zap contract. + * To reduce the size of the bytecode for this contract, zap is an independent contract + * that we make a delegatecall to. + */ + function _swap(uint256 _amount0In, uint256 _amount1In) internal { + (bool success, bytes memory data) = + getStorage().zapContract().delegatecall(abi.encodeWithSignature("swap(uint256,uint256)", _amount0In, _amount1In)); + if (!success) { + // there is no return value, we do not want to decode is there is no failure + require(success, abi.decode(data, (string))); + } + } + + /** + * @dev Convenience getter for the data contract. + */ + function getStorage() public view returns (IUniVaultStorageV1) { + return IUniVaultStorageV1(getAddress(_DATA_CONTRACT_SLOT)); + } + + /** + * @dev Convenience getter for token0. + */ + function token0() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token0()); + } + + /** + * @dev Convenience getter for token1. + */ + function token1() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token1()); + } + + /** + * @dev Convenience getter for the current sqrtPriceX96 of the Uniswap pool. + */ + function getSqrtPriceX96() public view returns (uint160) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } + + function getTick() public view returns (int24) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (, int24 curTick,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return curTick; + } + + /** + * @dev Sets an address to a slot. + */ + function setAddress(bytes32 slot, address _address) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _address) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getAddress(bytes32 slot) internal view returns (address str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Configure fees + * @param _feeRewardForwarder the address of the fee reward forwarder + * @param _feeRatio the profit sharing fee percentage (100% = 10000) + * @param _platformTarget the address for gas fees contributions + * @param _platformRatio the percentage for gas fee contributions (100% = 10000) + */ + function configureFees(address _feeRewardForwarder, uint256 _feeRatio, address _platformTarget, uint256 _platformRatio) + external + onlyGovernance + { + getStorage().configureFees(_feeRewardForwarder, _feeRatio, _platformTarget, _platformRatio); + } + + /** + * @dev Schedules an upgrade to the new implementation for the proxy. + */ + function scheduleUpgrade(address impl) public onlyGovernance { + getStorage().setNextImplementation(impl); + getStorage().setNextImplementationTimestamp(getStorage().nextImplementationDelay().add(block.timestamp)); + } + + /** + * @dev Tells the proxy if an upgrade is scheduled and the timelock elapsed. + */ + function shouldUpgrade() external view returns (bool, address) { + return ( + getStorage().nextImplementationTimestamp() != 0 && block.timestamp > getStorage().nextImplementationTimestamp() + && getStorage().nextImplementation() != address(0), + getStorage().nextImplementation() + ); + } + + /** + * @dev Completes the upgrade and resets the upgrade state to no upgrade. + */ + function finalizeUpgrade() external onlyGovernance { + getStorage().setNextImplementation(address(0)); + getStorage().setNextImplementationTimestamp(0); + } + + /** + * @dev Increases the upgrade timelock. Governance only. + */ + function increaseUpgradeDelay(uint256 delay) external onlyGovernance { + getStorage().setNextImplementationDelay(delay.add(getStorage().nextImplementationDelay())); + } +} diff --git a/src/utility/UniVaultUpgradeableV1qfCR.sol b/src/utility/UniVaultUpgradeableV1qfCR.sol new file mode 100644 index 0000000..f90acfa --- /dev/null +++ b/src/utility/UniVaultUpgradeableV1qfCR.sol @@ -0,0 +1,345 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity 0.7.6; +pragma abicoder v2; + +// ERC20 +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; + +// ERC721 +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721HolderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +// UniswapV3 core +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; + +// reentrancy guard +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +// UniswapV3 periphery contracts +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; + +// Harvest System +import "../interface/IFeeRewardForwarderV6.sol"; +import "../inheritance/ControllableInit.sol"; + +// Storage for this UniVault +import "../interface/IUniVaultStorageV1.sol"; + +// quick fix for Changing Range +contract UniVaultUpgradeableV1qfCR is ERC20Upgradeable, ERC721HolderUpgradeable, ReentrancyGuardUpgradeable, ControllableInit { + using SafeERC20Upgradeable for IERC20Upgradeable; + using SafeMathUpgradeable for uint256; + + bytes32 internal constant _DATA_CONTRACT_SLOT = 0x4c2252f3318958b38b23790562cc9d391075b8dadbfe0e707aed11afe13228b7; + address internal constant _NFT_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; + address internal constant _UNI_POOL_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + uint256 internal constant _UNDERLYING_UNIT = 1e18; + + /** + * @dev Ensures that the price between submitting the transaction and when + * the transaction gets mined did not move (checks slippage) + */ + modifier checkSqrtPriceX96(uint256 offChainSqrtPriceX96, uint256 tolerance) { + uint256 current = uint256(getSqrtPriceX96()); + uint256 step = offChainSqrtPriceX96.mul(tolerance).div(1000); + require(current < offChainSqrtPriceX96.add(step), "Price too high"); + require(current > offChainSqrtPriceX96.sub(step), "Price too low"); + _; + } + + /** + * @dev Creates the contract and ensures that the slot matches the hash + */ + constructor() { + assert(_DATA_CONTRACT_SLOT == bytes32(uint256(keccak256("eip1967.uniVault.dataContract")) - 1)); + } + + // Stripped down most implementation to save contract size. + + // creates a new position with the designated range + function mintPosition(int24 _tickLower, int24 _tickUpper, uint256 _initAmount0, uint256 _initAmount1) public onlyGovernance { + token0().safeTransferFrom(msg.sender, address(this), _initAmount0); + token1().safeTransferFrom(msg.sender, address(this), _initAmount1); + token0().safeApprove(_NFT_POSITION_MANAGER, 0); + token0().safeApprove(_NFT_POSITION_MANAGER, _initAmount0); + token1().safeApprove(_NFT_POSITION_MANAGER, 0); + token1().safeApprove(_NFT_POSITION_MANAGER, _initAmount1); + + (uint256 _tokenId, uint128 _initialLiquidity,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).mint( + INonfungiblePositionManager.MintParams({ + token0: address(token0()), + token1: address(token1()), + fee: getStorage().fee(), + tickLower: _tickLower, + tickUpper: _tickUpper, + amount0Desired: _initAmount0, // amount0Desired + amount1Desired: _initAmount1, // amount1Desired + amount0Min: 0, // amount0Min, first small deposit, can stay 0 + amount1Min: 0, // amount1Min, first small deposit, can stay 0 + recipient: address(this), + deadline: block.timestamp // will be done on this block. + }) + ); + } + + // switch to the designated position + function changeRange(uint256 newPosId) public onlyGovernance { + // If there is no such position, this will also fail. + require(IERC721Upgradeable(_NFT_POSITION_MANAGER).ownerOf(newPosId) == address(this), "pos owner mismatch"); + + // making sure that we are pointing to the same thing. + (,,,,,,, uint128 _oldLiquidity,,,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(getStorage().posId()); + // fetching information from new position + (,, address _t0, address _t1, uint24 _fee, int24 _newTickLower, int24 _newTickUpper,,,,,) = + INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(newPosId); + + require(_t0 == address(token0()) && _t1 == address(token1()), "pool mismatch"); + getStorage().setFee(_fee); + + uint256 oldPosId = getStorage().posId(); + + // remove liquidity from old + (uint256 _receivedToken0, uint256 _receivedToken1) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).decreaseLiquidity( + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: oldPosId, + liquidity: _oldLiquidity, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + + // collect the amount fetched above + previous accumulated fees + INonfungiblePositionManager(_NFT_POSITION_MANAGER).collect( + INonfungiblePositionManager.CollectParams({ + tokenId: oldPosId, + recipient: address(this), + amount0Max: uint128(-1), // collect all token0 (since we are changing ranges) + amount1Max: uint128(-1) // collect all token1 (since we are changing ranges) + }) + ); + + // set new ranges + getStorage().setTickLower(_newTickLower); + getStorage().setTickUpper(_newTickUpper); + + // Zap will balance it to the new range + _zap(); + + // point the storge to the new Pos Id + getStorage().setPosId(newPosId); + + // we should now put them all into the new position. + + uint256 _amount0 = token0().balanceOf(address(this)); + uint256 _amount1 = token1().balanceOf(address(this)); + // approvals for the liquidity increase + token0().safeApprove(_NFT_POSITION_MANAGER, 0); + token0().safeApprove(_NFT_POSITION_MANAGER, _amount0); + token1().safeApprove(_NFT_POSITION_MANAGER, 0); + token1().safeApprove(_NFT_POSITION_MANAGER, _amount1); + // increase the liquidity + (uint128 _liquidity,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).increaseLiquidity( + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: getStorage().posId(), + amount0Desired: _amount0, + amount1Desired: _amount1, + amount0Min: 0, + amount1Min: 0, + deadline: block.timestamp + }) + ); + + // then lets sweep the rest. + _transferLeftOverTo(msg.sender); + } + + /** + * @dev Returns the total liquidity stored in the position + * Dev Note: need to turn on the solc optimizer otherwise the compiler + * will throw stack too deep on this function + */ + function underlyingBalanceWithInvestment() public view returns (uint256) { + // note that the liquidity is not a token, so there is no local balance added + (,,,,,,, uint128 liquidity,,,,) = INonfungiblePositionManager(_NFT_POSITION_MANAGER).positions(getStorage().posId()); + return liquidity; + } + + /** + * @dev A dummy method to get the controller working + */ + function setStrategy(address unused) public onlyControllerOrGovernance { + // intentionally a no-op + } + + /** + * @dev A dummy method to get the controller working + */ + function strategy() public view returns (address) { + return address(this); + } + + /** + * @dev Returns the price per full share, scaled to 1e18 + */ + function getPricePerFullShare() public view returns (uint256) { + return totalSupply() == 0 ? _UNDERLYING_UNIT : _UNDERLYING_UNIT.mul(underlyingBalanceWithInvestment()).div(totalSupply()); + } + + /** + * @dev Allows governance to get dust that stays behind, or accidental token transfers. + * No tokens should stay in this contract. + */ + function sweepDust() external onlyControllerOrGovernance { + _transferLeftOverTo(msg.sender); + } + + /** + * @dev Handles transferring the leftovers + */ + function _transferLeftOverTo(address _to) internal { + uint256 balance0 = token0().balanceOf(address(this)); + uint256 balance1 = token1().balanceOf(address(this)); + if (balance0 > 0) { + token0().safeTransfer(_to, balance0); + } + if (balance1 > 0) { + token1().safeTransfer(_to, balance1); + } + } + + /** + * @dev Zap takes in what users provided and calculates an approximately balanced amount for + * liquidity provision. It then swaps one token to another to make the tokens balanced. + * To reduce the size of the bytecode for this contract, zap is an independent contract + * that we make a delegatecall to. + */ + function _zap() internal { + uint256 _originalAmount0 = token0().balanceOf(address(this)); + uint256 _originalAmount1 = token1().balanceOf(address(this)); + (bool success, bytes memory data) = getStorage().zapContract().delegatecall( + abi.encodeWithSignature("zap(uint256,uint256)", _originalAmount0, _originalAmount1) + ); + if (!success) { + // there is no return value, we do not want to decode is there is no failure + require(success, abi.decode(data, (string))); + } + } + + /** + * @dev Swap takes in token amount and swap it to another. This code resides in the zap contract. + * To reduce the size of the bytecode for this contract, zap is an independent contract + * that we make a delegatecall to. + */ + function _swap(uint256 _amount0In, uint256 _amount1In) internal { + (bool success, bytes memory data) = + getStorage().zapContract().delegatecall(abi.encodeWithSignature("swap(uint256,uint256)", _amount0In, _amount1In)); + if (!success) { + // there is no return value, we do not want to decode is there is no failure + require(success, abi.decode(data, (string))); + } + } + + /** + * @dev Convenience getter for the data contract. + */ + function getStorage() public view returns (IUniVaultStorageV1) { + return IUniVaultStorageV1(getAddress(_DATA_CONTRACT_SLOT)); + } + + /** + * @dev Convenience getter for token0. + */ + function token0() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token0()); + } + + /** + * @dev Convenience getter for token1. + */ + function token1() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(getStorage().token1()); + } + + /** + * @dev Convenience getter for the current sqrtPriceX96 of the Uniswap pool. + */ + function getSqrtPriceX96() public view returns (uint160) { + address poolAddr = + IUniswapV3Factory(_UNI_POOL_FACTORY).getPool(getStorage().token0(), getStorage().token1(), getStorage().fee()); + (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(poolAddr).slot0(); + return sqrtPriceX96; + } + + /** + * @dev Sets an address to a slot. + */ + function setAddress(bytes32 slot, address _address) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, _address) + } + } + + /** + * @dev Reads an address from a slot. + */ + function getAddress(bytes32 slot) internal view returns (address str) { + // solhint-disable-next-line no-inline-assembly + assembly { + str := sload(slot) + } + } + + /** + * @dev Configure fees + * @param _feeRewardForwarder the address of the fee reward forwarder + * @param _feeRatio the profit sharing fee percentage (100% = 10000) + * @param _platformTarget the address for gas fees contributions + * @param _platformRatio the percentage for gas fee contributions (100% = 10000) + */ + function configureFees(address _feeRewardForwarder, uint256 _feeRatio, address _platformTarget, uint256 _platformRatio) + external + onlyGovernance + { + getStorage().configureFees(_feeRewardForwarder, _feeRatio, _platformTarget, _platformRatio); + } + + /** + * @dev Schedules an upgrade to the new implementation for the proxy. + */ + function scheduleUpgrade(address impl) public onlyGovernance { + getStorage().setNextImplementation(impl); + getStorage().setNextImplementationTimestamp(getStorage().nextImplementationDelay().add(block.timestamp)); + } + + /** + * @dev Tells the proxy if an upgrade is scheduled and the timelock elapsed. + */ + function shouldUpgrade() external view returns (bool, address) { + return ( + getStorage().nextImplementationTimestamp() != 0 && block.timestamp > getStorage().nextImplementationTimestamp() + && getStorage().nextImplementation() != address(0), + getStorage().nextImplementation() + ); + } + + /** + * @dev Completes the upgrade and resets the upgrade state to no upgrade. + */ + function finalizeUpgrade() external onlyGovernance { + getStorage().setNextImplementation(address(0)); + getStorage().setNextImplementationTimestamp(0); + } + + /** + * @dev Increases the upgrade timelock. Governance only. + */ + function increaseUpgradeDelay(uint256 delay) external onlyGovernance { + getStorage().setNextImplementationDelay(delay.add(getStorage().nextImplementationDelay())); + } +}