diff --git a/contracts/interfaces/IGauge.sol b/contracts/interfaces/IGauge.sol new file mode 100644 index 00000000..9c9e88fe --- /dev/null +++ b/contracts/interfaces/IGauge.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.0; + +/// @title IGauge interface +/// @notice This interface can be used for any contracts that has +/// gauge like functionalities. +interface IGauge { + function deposit(uint256) external; + + function withdraw(uint256) external; + + function lp_token() external view returns (address); + + function set_rewards_receiver(address) external; + + // Below are ChildGauge specific methods. Using them for + // other contracts will result in revert. + + function SDL() external view returns (address); + + function FACTORY() external view returns (address); +} diff --git a/contracts/interfaces/IMinterLike.sol b/contracts/interfaces/IMinterLike.sol new file mode 100644 index 00000000..88f14786 --- /dev/null +++ b/contracts/interfaces/IMinterLike.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.5.0; + +/// @title IMinterLike interface +/// @notice This interface can be used for any contracts that has +/// minter like functionalities. For example ChildGaugeFactory +/// on a side chain is a minter like contract. +interface IMinterLike { + function mint(address gauge) external; +} diff --git a/contracts/meta/MetaSwapDepositGaugeSupportV1.sol b/contracts/meta/MetaSwapDepositGaugeSupportV1.sol new file mode 100644 index 00000000..ba85bf6b --- /dev/null +++ b/contracts/meta/MetaSwapDepositGaugeSupportV1.sol @@ -0,0 +1,785 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.17; + +import "@openzeppelin/contracts-4.7.3/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable-4.7.3/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable-4.7.3/security/ReentrancyGuardUpgradeable.sol"; +import "../LPTokenV2.sol"; +import "../interfaces/ISwapV2.sol"; +import "./MetaSwapGaugeSupportV1.sol"; + +/** + * @title MetaSwapDeposit + * @notice This contract flattens the LP token in a MetaSwap pool for easier user access. MetaSwap must be + * deployed before this contract can be initialized successfully. + * + * For example, suppose there exists a base Swap pool consisting of [DAI, USDC, USDT]. + * Then a MetaSwap pool can be created with [sUSD, BaseSwapLPToken] to allow trades between either + * the LP token or the underlying tokens and sUSD. + * + * MetaSwapDeposit flattens the LP token and remaps them to a single array, allowing users + * to ignore the dependency on BaseSwapLPToken. Using the above example, MetaSwapDeposit can act + * as a Swap containing [sUSD, DAI, USDC, USDT] tokens. + */ +contract MetaSwapDepositGaugeSupportV1 is + Initializable, + ReentrancyGuardUpgradeable +{ + using SafeERC20 for IERC20; + + ISwapV2 public baseSwap; + MetaSwapGaugeSupportV1 public metaSwap; + address public baseLPToken; + IERC20[] public baseTokens; + IERC20[] public metaTokens; + IERC20[] public tokens; + IERC20 public metaLPToken; + + uint256 constant MAX_UINT256 = 2**256 - 1; + + struct RemoveLiquidityImbalanceInfo { + ISwapV2 baseSwap; + MetaSwapGaugeSupportV1 metaSwap; + IERC20 metaLPToken; + uint8 baseLPTokenIndex; + bool withdrawFromBase; + uint256 leftoverMetaLPTokenAmount; + } + + /** + * @notice Sets the address for the base Swap contract, MetaSwap contract, and the + * MetaSwap LP token contract. + * @param _baseSwap the address of the base Swap contract + * @param _metaSwap the address of the MetaSwap contract + * @param _metaLPToken the address of the MetaSwap LP token contract + */ + function initialize( + ISwapV2 _baseSwap, + MetaSwapGaugeSupportV1 _metaSwap, + IERC20 _metaLPToken + ) external initializer { + __ReentrancyGuard_init(); + // Check and approve base level tokens to be deposited to the base Swap contract + { + uint8 i; + for (; i < 32; i++) { + try _baseSwap.getToken(i) returns (IERC20 token) { + baseTokens.push(token); + token.safeApprove(address(_baseSwap), MAX_UINT256); + token.safeApprove(address(_metaSwap), MAX_UINT256); + } catch { + break; + } + } + require(i > 1, "baseSwap must have at least 2 tokens"); + } + + // Check and approve meta level tokens to be deposited to the MetaSwap contract + IERC20 gaugeToken; + { + uint8 i; + for (; i < 32; i++) { + try _metaSwap.getToken(i) returns (IERC20 token) { + gaugeToken = token; + metaTokens.push(token); + tokens.push(token); + token.safeApprove(address(_metaSwap), MAX_UINT256); + } catch { + break; + } + } + require(i > 1, "metaSwap must have at least 2 tokens"); + } + + // Flatten baseTokens and append it to tokens array + tokens[tokens.length - 1] = baseTokens[0]; + for (uint8 i = 1; i < baseTokens.length; i++) { + tokens.push(baseTokens[i]); + } + + baseLPToken = IGauge(address(gaugeToken)).lp_token(); + + // Approve base Swap LP token to be burned by the base Swap contract for withdrawing + IERC20(baseLPToken).safeApprove(address(_baseSwap), MAX_UINT256); + // Approve base Swap LP token to be burned by the gauge contract for depositing + IERC20(baseLPToken).safeApprove(address(gaugeToken), MAX_UINT256); + // Approve MetaSwap LP token to be burned by the MetaSwap contract for withdrawing + _metaLPToken.safeApprove(address(_metaSwap), MAX_UINT256); + + // Initialize storage variables + baseSwap = _baseSwap; + metaSwap = _metaSwap; + metaLPToken = _metaLPToken; + } + + // Mutative functions + + /** + * @notice Swap two underlying tokens using the meta pool and the base pool + * @param tokenIndexFrom the token the user wants to swap from + * @param tokenIndexTo the token the user wants to swap to + * @param dx the amount of tokens the user wants to swap from + * @param minDy the min amount the user would like to receive, or revert. + * @param deadline latest timestamp to accept this transaction + */ + function swap( + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx, + uint256 minDy, + uint256 deadline + ) external nonReentrant returns (uint256) { + tokens[tokenIndexFrom].safeTransferFrom(msg.sender, address(this), dx); + uint256 tokenToAmount = metaSwap.swapUnderlying( + tokenIndexFrom, + tokenIndexTo, + dx, + minDy, + deadline + ); + tokens[tokenIndexTo].safeTransfer(msg.sender, tokenToAmount); + return tokenToAmount; + } + + /** + * @notice Swap two tokens at meta level using base lp token instead of the gauge token + * @param tokenIndexFrom the token the user wants to swap from + * @param tokenIndexTo the token the user wants to swap to + * @param dx the amount of tokens the user wants to swap from + * @param minDy the min amount the user would like to receive, or revert. + * @param deadline latest timestamp to accept this transaction + */ + function swapWithBaseLPToken( + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx, + uint256 minDy, + uint256 deadline + ) external nonReentrant returns (uint256) { + address tokenFrom; + address tokenTo; + uint256 baseLPTokenIndex = metaTokens.length - 1; + + // Get token addresses but replace gauge token address with base lp token address + if (tokenIndexFrom == baseLPTokenIndex) { + tokenFrom = baseLPToken; + } else { + tokenFrom = address(metaTokens[tokenIndexFrom]); + } + + if (tokenIndexTo == baseLPTokenIndex) { + tokenTo = baseLPToken; + } else { + tokenTo = address(metaTokens[tokenIndexTo]); + } + + // Get the token from the user + IERC20(tokenFrom).safeTransferFrom(msg.sender, address(this), dx); + + // If user is swapping from gauge token to meta level token, + // get gauge tokens by depositing base lp token + if (tokenIndexFrom == baseLPTokenIndex) { + IGauge(address(metaTokens[baseLPTokenIndex])).deposit(dx); + } + + // Swap at meta level + uint256 tokenToAmount = metaSwap.swap( + tokenIndexFrom, + tokenIndexTo, + dx, + minDy, + deadline + ); + + // If user is swapping to gauge token, withdraw base lp token + if (tokenIndexTo == baseLPTokenIndex) { + IGauge(address(metaTokens[baseLPTokenIndex])).withdraw( + tokenToAmount + ); + } + + // Give either one of meta level tokens to user + IERC20(tokenTo).safeTransfer(msg.sender, tokenToAmount); + } + + /** + * @notice Add liquidity to the pool with the given amounts of tokens + * @param amounts the amounts of each token to add, in their native precision + * @param minToMint the minimum LP tokens adding this amount of liquidity + * should mint, otherwise revert. Handy for front-running mitigation + * @param deadline latest timestamp to accept this transaction + * @return amount of LP token user minted and received + */ + function addLiquidity( + uint256[] calldata amounts, + uint256 minToMint, + uint256 deadline + ) external nonReentrant returns (uint256) { + // Read to memory to save on gas + IERC20[] memory memBaseTokens = baseTokens; + IERC20[] memory memMetaTokens = metaTokens; + uint256 baseLPTokenIndex = memMetaTokens.length - 1; + + require(amounts.length == memBaseTokens.length + baseLPTokenIndex); + + uint256 baseLPTokenAmount; + { + // Transfer base tokens from the caller and deposit to the base Swap pool + uint256[] memory baseAmounts = new uint256[](memBaseTokens.length); + bool shouldDepositBaseTokens; + for (uint8 i = 0; i < memBaseTokens.length; i++) { + IERC20 token = memBaseTokens[i]; + uint256 depositAmount = amounts[baseLPTokenIndex + i]; + if (depositAmount > 0) { + token.safeTransferFrom( + msg.sender, + address(this), + depositAmount + ); + baseAmounts[i] = token.balanceOf(address(this)); // account for any fees on transfer + // if there are any base Swap level tokens, flag it for deposits + shouldDepositBaseTokens = true; + } + } + if (shouldDepositBaseTokens) { + // Deposit any base Swap level tokens and receive baseLPToken + baseLPTokenAmount = baseSwap.addLiquidity( + baseAmounts, + 0, + deadline + ); + + // Get the gauge token by depositing base lp token into the gauge + IGauge(address(memMetaTokens[baseLPTokenIndex])).deposit( + baseLPTokenAmount + ); + } + } + + uint256 metaLPTokenAmount; + { + // Transfer remaining meta level tokens from the caller + uint256[] memory metaAmounts = new uint256[](memMetaTokens.length); + for (uint8 i = 0; i < baseLPTokenIndex; i++) { + IERC20 token = memMetaTokens[i]; + uint256 depositAmount = amounts[i]; + if (depositAmount > 0) { + token.safeTransferFrom( + msg.sender, + address(this), + depositAmount + ); + metaAmounts[i] = token.balanceOf(address(this)); // account for any fees on transfer + } + } + // Update the baseLPToken amount that will be deposited + metaAmounts[baseLPTokenIndex] = baseLPTokenAmount; + + // Deposit the meta level tokens and the baseLPToken + metaLPTokenAmount = metaSwap.addLiquidity( + metaAmounts, + minToMint, + deadline + ); + } + + // Transfer the meta lp token to the caller + metaLPToken.safeTransfer(msg.sender, metaLPTokenAmount); + + return metaLPTokenAmount; + } + + /** + * @notice Add liquidity to the meta pool with the given amounts of tokens. + * This function is similar to addLiquidity, but it receives base lp token + * from the user. The base lp token is then deposited into the gauge to + * get the gauge token necessary for adding liquidity to the meta pool. + * @param amounts the amounts of each token to add, in their native precision + * @param minToMint the minimum LP tokens adding this amount of liquidity + * should mint, otherwise revert. Handy for front-running mitigation + * @param deadline latest timestamp to accept this transaction + * @return amount of LP token user minted and received + */ + function addLiquidityWrappedWithBaseLPToken( + uint256[] calldata amounts, + uint256 minToMint, + uint256 deadline + ) external nonReentrant returns (uint256) { + IERC20[] memory memMetaTokens = metaTokens; + uint256 baseLPTokenIndex = memMetaTokens.length - 1; + + require(amounts.length == baseLPTokenIndex, "Invalid amounts length"); + + // If the base lp token amount is more than 0, transfer base lp token + // from the caller and deposit to the gauge. + uint256 baseLPTokenAmount = amounts[baseLPTokenIndex]; + if (baseLPTokenAmount > 0) { + IERC20(baseLPToken).safeTransferFrom( + msg.sender, + address(this), + baseLPTokenAmount + ); + IGauge(address(baseLPToken)).deposit(baseLPTokenAmount); + } + + uint256 metaLpTokenAmount = metaSwap.addLiquidity( + amounts, + minToMint, + deadline + ); + + // Transfer the meta lp token to the caller + metaLPToken.safeTransfer(msg.sender, metaLpTokenAmount); + } + + /** + * @notice Burn LP tokens to remove liquidity from the pool. Withdraw fee that decays linearly + * over period of 4 weeks since last deposit will apply. + * @dev Liquidity can always be removed, even when the pool is paused. + * @param amount the amount of LP tokens to burn + * @param minAmounts the minimum amounts of each token in the pool + * acceptable for this burn. Useful as a front-running mitigation + * @param deadline latest timestamp to accept this transaction + * @return amounts of tokens user received + */ + function removeLiquidity( + uint256 amount, + uint256[] calldata minAmounts, + uint256 deadline + ) external nonReentrant returns (uint256[] memory) { + IERC20[] memory memBaseTokens = baseTokens; + IERC20[] memory memMetaTokens = metaTokens; + uint256[] memory totalRemovedAmounts; + + { + uint256 numOfAllTokens = memBaseTokens.length + + memMetaTokens.length - + 1; + require(minAmounts.length == numOfAllTokens, "out of range"); + totalRemovedAmounts = new uint256[](numOfAllTokens); + } + + // Transfer meta lp token from the caller to this + metaLPToken.safeTransferFrom(msg.sender, address(this), amount); + + uint256 baseLPTokenAmount; + { + // Remove liquidity from the MetaSwap pool + uint256[] memory removedAmounts; + uint256 baseLPTokenIndex = memMetaTokens.length - 1; + { + uint256[] memory metaMinAmounts = new uint256[]( + memMetaTokens.length + ); + for (uint8 i = 0; i < baseLPTokenIndex; i++) { + metaMinAmounts[i] = minAmounts[i]; + } + removedAmounts = metaSwap.removeLiquidity( + amount, + metaMinAmounts, + deadline + ); + } + + // Send the meta level tokens to the caller + for (uint8 i = 0; i < baseLPTokenIndex; i++) { + totalRemovedAmounts[i] = removedAmounts[i]; + memMetaTokens[i].safeTransfer(msg.sender, removedAmounts[i]); + } + baseLPTokenAmount = removedAmounts[baseLPTokenIndex]; + + // Remove liquidity from the base Swap pool + { + uint256[] memory baseMinAmounts = new uint256[]( + memBaseTokens.length + ); + for (uint8 i = 0; i < baseLPTokenIndex; i++) { + baseMinAmounts[i] = minAmounts[baseLPTokenIndex + i]; + } + + // Get the base LP token by withdrawing from the gauge + IGauge(address(memMetaTokens[baseLPTokenIndex])).withdraw( + baseLPTokenAmount + ); + + removedAmounts = baseSwap.removeLiquidity( + baseLPTokenAmount, + baseMinAmounts, + deadline + ); + } + + // Send the base level tokens to the caller + for (uint8 i = 0; i < memBaseTokens.length; i++) { + totalRemovedAmounts[baseLPTokenIndex + i] = removedAmounts[i]; + memBaseTokens[i].safeTransfer(msg.sender, removedAmounts[i]); + } + } + + return totalRemovedAmounts; + } + + /** + * @notice Burn meta LP tokens to remove liquidity from the pool. + * Base LP token is returned instead of the gauge token. + * @dev Liquidity can always be removed, even when the pool is paused. + * @param amount the amount of meta LP tokens to burn + * @param minAmounts the minimum amounts of each token in the pool + * acceptable for this burn. Useful as a front-running mitigation + * @param deadline latest timestamp to accept this transaction + * @return amounts of tokens user received + */ + function removeLiquidityWrappedWithBaseLPToken( + uint256 amount, + uint256[] calldata minAmounts, + uint256 deadline + ) external nonReentrant returns (uint256[] memory) { + IERC20[] memory memMetaTokens = metaTokens; + uint256[] memory totalRemovedAmounts; + + require(minAmounts.length == memMetaTokens.length, "out of range"); + + // Transfer meta lp token from the caller to this + metaLPToken.safeTransferFrom(msg.sender, address(this), amount); + totalRemovedAmounts = metaSwap.removeLiquidity( + amount, + minAmounts, + deadline + ); + + // Get the base LP token by withdrawing from the gauge + uint256 baseLPTokenIndex = memMetaTokens.length - 1; + IGauge(address(memMetaTokens[baseLPTokenIndex])).withdraw( + totalRemovedAmounts[baseLPTokenIndex] + ); + + // Send the meta level tokens to the caller + // The last token is replaced with base lp token + for (uint8 i = 0; i < baseLPTokenIndex; i++) { + memMetaTokens[i].safeTransfer(msg.sender, totalRemovedAmounts[i]); + } + IERC20(baseLPToken).safeTransfer( + msg.sender, + totalRemovedAmounts[baseLPTokenIndex] + ); + } + + /** + * @notice Remove liquidity from the pool all in one token. Withdraw fee that decays linearly + * over period of 4 weeks since last deposit will apply. + * @param tokenAmount the amount of the token you want to receive + * @param tokenIndex the index of the token you want to receive + * @param minAmount the minimum amount to withdraw, otherwise revert + * @param deadline latest timestamp to accept this transaction + * @return amount of chosen token user received + */ + function removeLiquidityOneToken( + uint256 tokenAmount, + uint8 tokenIndex, + uint256 minAmount, + uint256 deadline + ) external nonReentrant returns (uint256) { + uint8 baseLPTokenIndex = uint8(metaTokens.length - 1); + uint8 baseTokensLength = uint8(baseTokens.length); + + // Transfer metaLPToken from the caller + metaLPToken.safeTransferFrom(msg.sender, address(this), tokenAmount); + + IERC20 token; + if (tokenIndex < baseLPTokenIndex) { + // When the desired token is meta level token, we can just call `removeLiquidityOneToken` directly + metaSwap.removeLiquidityOneToken( + tokenAmount, + tokenIndex, + minAmount, + deadline + ); + token = metaTokens[tokenIndex]; + } else if (tokenIndex < baseLPTokenIndex + baseTokensLength) { + // When the desired token is a base level token, we need to first withdraw via baseLPToken, then withdraw + // the desired token from the base Swap contract. + uint256 removedBaseLPTokenAmount = metaSwap.removeLiquidityOneToken( + tokenAmount, + baseLPTokenIndex, + 0, + deadline + ); + + // Get the base LP token by withdrawing from the gauge + IGauge(address(metaTokens[baseLPTokenIndex])).withdraw( + removedBaseLPTokenAmount + ); + + // Get the underlying tokens + baseSwap.removeLiquidityOneToken( + removedBaseLPTokenAmount, + tokenIndex - baseLPTokenIndex, + minAmount, + deadline + ); + token = baseTokens[tokenIndex - baseLPTokenIndex]; + } else { + revert("out of range"); + } + + uint256 amountWithdrawn = token.balanceOf(address(this)); + token.safeTransfer(msg.sender, amountWithdrawn); + return amountWithdrawn; + } + + /** + * @notice Remove liquidity from the pool, weighted differently than the + * pool's current balances. Withdraw fee that decays linearly + * over period of 4 weeks since last deposit will apply. + * @param amounts how much of each token to withdraw + * @param maxBurnAmount the max LP token provider is willing to pay to + * remove liquidity. Useful as a front-running mitigation. + * @param deadline latest timestamp to accept this transaction + * @return amount of LP tokens burned + */ + function removeLiquidityImbalance( + uint256[] calldata amounts, + uint256 maxBurnAmount, + uint256 deadline + ) external nonReentrant returns (uint256) { + IERC20[] memory memBaseTokens = baseTokens; + IERC20[] memory memMetaTokens = metaTokens; + uint256[] memory metaAmounts = new uint256[](memMetaTokens.length); + uint256[] memory baseAmounts = new uint256[](memBaseTokens.length); + + require( + amounts.length == memBaseTokens.length + memMetaTokens.length - 1, + "out of range" + ); + + RemoveLiquidityImbalanceInfo memory v = RemoveLiquidityImbalanceInfo( + baseSwap, + metaSwap, + metaLPToken, + uint8(metaAmounts.length - 1), + false, + 0 + ); + + for (uint8 i = 0; i < v.baseLPTokenIndex; i++) { + metaAmounts[i] = amounts[i]; + } + + for (uint8 i = 0; i < baseAmounts.length; i++) { + baseAmounts[i] = amounts[v.baseLPTokenIndex + i]; + if (baseAmounts[i] > 0) { + v.withdrawFromBase = true; + } + } + + // Calculate how much base LP token we need to get the desired amount of underlying tokens + if (v.withdrawFromBase) { + metaAmounts[v.baseLPTokenIndex] = + (v.baseSwap.calculateTokenAmount(baseAmounts, false) * 10005) / + 10000; + } + + // Transfer MetaSwap LP token from the caller to this contract + v.metaLPToken.safeTransferFrom( + msg.sender, + address(this), + maxBurnAmount + ); + + // Withdraw the paired meta level tokens and the base LP token from the MetaSwap pool + uint256 burnedMetaLPTokenAmount = v.metaSwap.removeLiquidityImbalance( + metaAmounts, + maxBurnAmount, + deadline + ); + v.leftoverMetaLPTokenAmount = maxBurnAmount - burnedMetaLPTokenAmount; + + // If underlying tokens are desired, withdraw them from the base Swap pool + if (v.withdrawFromBase) { + // Get the base LP token by withdrawing from the gauge + IGauge(address(metaTokens[v.baseLPTokenIndex])).withdraw( + metaAmounts[v.baseLPTokenIndex] + ); + + // Burn the base LP token and get the underlying tokens + v.baseSwap.removeLiquidityImbalance( + baseAmounts, + metaAmounts[v.baseLPTokenIndex], + deadline + ); + + // Base Swap may require LESS base LP token than the amount we have + // In that case, deposit it to the MetaSwap pool. + uint256[] memory leftovers = new uint256[](metaAmounts.length); + IERC20 baseLPToken = memMetaTokens[v.baseLPTokenIndex]; + uint256 leftoverBaseLPTokenAmount = baseLPToken.balanceOf( + address(this) + ); + if (leftoverBaseLPTokenAmount > 0) { + leftovers[v.baseLPTokenIndex] = leftoverBaseLPTokenAmount; + v.leftoverMetaLPTokenAmount = + v.leftoverMetaLPTokenAmount + + v.metaSwap.addLiquidity(leftovers, 0, deadline); + } + } + + // Transfer all withdrawn tokens to the caller + for (uint8 i = 0; i < amounts.length; i++) { + IERC20 token; + if (i < v.baseLPTokenIndex) { + token = memMetaTokens[i]; + } else { + token = memBaseTokens[i - v.baseLPTokenIndex]; + } + if (amounts[i] > 0) { + token.safeTransfer(msg.sender, amounts[i]); + } + } + + // If there were any extra meta lp token, transfer them back to the caller as well + if (v.leftoverMetaLPTokenAmount > 0) { + v.metaLPToken.safeTransfer(msg.sender, v.leftoverMetaLPTokenAmount); + } + + return maxBurnAmount - v.leftoverMetaLPTokenAmount; + } + + // VIEW FUNCTIONS + + /** + * @notice A simple method to calculate prices from deposits or + * withdrawals, excluding fees but including slippage. This is + * helpful as an input into the various "min" parameters on calls + * to fight front-running. When withdrawing from the base pool in imbalanced + * fashion, the recommended slippage setting is 0.2% or higher. + * + * @dev This shouldn't be used outside frontends for user estimates. + * + * @param amounts an array of token amounts to deposit or withdrawal, + * corresponding to pooledTokens. The amount should be in each + * pooled token's native precision. If a token charges a fee on transfers, + * use the amount that gets transferred after the fee. + * @param deposit whether this is a deposit or a withdrawal + * @return token amount the user will receive + */ + function calculateTokenAmount(uint256[] calldata amounts, bool deposit) + external + view + returns (uint256) + { + uint256[] memory metaAmounts = new uint256[](metaTokens.length); + uint256[] memory baseAmounts = new uint256[](baseTokens.length); + uint256 baseLPTokenIndex = metaAmounts.length - 1; + + for (uint8 i = 0; i < baseLPTokenIndex; i++) { + metaAmounts[i] = amounts[i]; + } + + for (uint8 i = 0; i < baseAmounts.length; i++) { + baseAmounts[i] = amounts[baseLPTokenIndex + i]; + } + + uint256 baseLPTokenAmount = baseSwap.calculateTokenAmount( + baseAmounts, + deposit + ); + metaAmounts[baseLPTokenIndex] = baseLPTokenAmount; + + return metaSwap.calculateTokenAmount(metaAmounts, deposit); + } + + /** + * @notice A simple method to calculate amount of each underlying + * tokens that is returned upon burning given amount of LP tokens + * @param amount the amount of LP tokens that would be burned on withdrawal + * @return array of token balances that the user will receive + */ + function calculateRemoveLiquidity(uint256 amount) + external + view + returns (uint256[] memory) + { + uint256[] memory metaAmounts = metaSwap.calculateRemoveLiquidity( + amount + ); + uint8 baseLPTokenIndex = uint8(metaAmounts.length - 1); + uint256[] memory baseAmounts = baseSwap.calculateRemoveLiquidity( + metaAmounts[baseLPTokenIndex] + ); + + uint256[] memory totalAmounts = new uint256[]( + baseLPTokenIndex + baseAmounts.length + ); + for (uint8 i = 0; i < baseLPTokenIndex; i++) { + totalAmounts[i] = metaAmounts[i]; + } + for (uint8 i = 0; i < baseAmounts.length; i++) { + totalAmounts[baseLPTokenIndex + i] = baseAmounts[i]; + } + + return totalAmounts; + } + + /** + * @notice Calculate the amount of underlying token available to withdraw + * when withdrawing via only single token + * @param tokenAmount the amount of LP token to burn + * @param tokenIndex index of which token will be withdrawn + * @return availableTokenAmount calculated amount of underlying token + * available to withdraw + */ + function calculateRemoveLiquidityOneToken( + uint256 tokenAmount, + uint8 tokenIndex + ) external view returns (uint256) { + uint8 baseLPTokenIndex = uint8(metaTokens.length - 1); + + if (tokenIndex < baseLPTokenIndex) { + return + metaSwap.calculateRemoveLiquidityOneToken( + tokenAmount, + tokenIndex + ); + } else { + uint256 baseLPTokenAmount = metaSwap + .calculateRemoveLiquidityOneToken( + tokenAmount, + baseLPTokenIndex + ); + return + baseSwap.calculateRemoveLiquidityOneToken( + baseLPTokenAmount, + tokenIndex - baseLPTokenIndex + ); + } + } + + /** + * @notice Returns the address of the pooled token at given index. Reverts if tokenIndex is out of range. + * This is a flattened representation of the pooled tokens. + * @param index the index of the token + * @return address of the token at given index + */ + function getToken(uint8 index) external view returns (IERC20) { + require(index < tokens.length, "index out of range"); + return tokens[index]; + } + + /** + * @notice Calculate amount of tokens you receive on swap + * @param tokenIndexFrom the token the user wants to sell + * @param tokenIndexTo the token the user wants to buy + * @param dx the amount of tokens the user wants to sell. If the token charges + * a fee on transfers, use the amount that gets transferred after the fee. + * @return amount of tokens the user will receive + */ + function calculateSwap( + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx + ) external view returns (uint256) { + return + metaSwap.calculateSwapUnderlying(tokenIndexFrom, tokenIndexTo, dx); + } +} diff --git a/contracts/meta/MetaSwapGaugeSupportV1.sol b/contracts/meta/MetaSwapGaugeSupportV1.sol new file mode 100644 index 00000000..45e83e5e --- /dev/null +++ b/contracts/meta/MetaSwapGaugeSupportV1.sol @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.17; + +import "../SwapV2.sol"; +import "@openzeppelin/contracts-4.7.3/token/ERC20/utils/SafeERC20.sol"; +import "./MetaSwapUtilsGaugeSupportV1.sol"; +import "../xchainGauges/GaugeTokenHolder.sol"; + +/** + * @title MetaSwap - A StableSwap implementation in solidity. + * @notice This contract is responsible for custody of closely pegged assets (eg. group of stablecoins) + * and automatic market making system. Users become an LP (Liquidity Provider) by depositing their tokens + * in desired ratios for an exchange of the pool token that represents their share of the pool. + * Users can burn pool tokens and withdraw their share of token(s). + * + * Each time a swap between the pooled tokens happens, a set fee incurs which effectively gets + * distributed to the LPs. + * + * In case of emergencies, admin can pause additional deposits, swaps, or single-asset withdraws - which + * stops the ratio of the tokens in the pool from changing. + * Users can always withdraw their tokens via multi-asset withdraws. + * + * MetaSwap is a modified version of Swap that allows Swap's LP token to be utilized in pooling with other tokens. + * As an example, if there is a Swap pool consisting of [DAI, USDC, USDT], then a MetaSwap pool can be created + * with [sUSD, BaseSwapLPToken] to allow trades between either the LP token or the underlying tokens and sUSD. + * Note that when interacting with MetaSwap, users cannot deposit or withdraw via underlying tokens. In that case, + * `MetaSwapDeposit.sol` can be additionally deployed to allow interacting with unwrapped representations of the tokens. + * + * @dev Most of the logic is stored as a library `MetaSwapUtils` for the sake of reducing contract's + * deployment size. + */ +contract MetaSwapGaugeSupportV1 is SwapV2, GaugeTokenHolder { + using MetaSwapUtilsGaugeSupportV1 for SwapUtilsV2.Swap; + using SafeERC20 for IERC20; + + MetaSwapUtilsGaugeSupportV1.MetaSwap public metaSwapStorage; + + uint256 constant MAX_UINT256 = 2**256 - 1; + + /*** EVENTS ***/ + + // events replicated from SwapUtils to make the ABI easier for dumb + // clients + event TokenSwapUnderlying( + address indexed buyer, + uint256 tokensSold, + uint256 tokensBought, + uint128 soldId, + uint128 boughtId + ); + + /** + * @notice Get the virtual price, to help calculate profit + * @return the virtual price, scaled to the POOL_PRECISION_DECIMALS + */ + function getVirtualPrice() + external + view + virtual + override + returns (uint256) + { + return + MetaSwapUtilsGaugeSupportV1.getVirtualPrice( + swapStorage, + metaSwapStorage + ); + } + + /** + * @notice Calculate amount of tokens you receive on swap + * @param tokenIndexFrom the token the user wants to sell + * @param tokenIndexTo the token the user wants to buy + * @param dx the amount of tokens the user wants to sell. If the token charges + * a fee on transfers, use the amount that gets transferred after the fee. + * @return amount of tokens the user will receive + */ + function calculateSwap( + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx + ) external view virtual override returns (uint256) { + return + MetaSwapUtilsGaugeSupportV1.calculateSwap( + swapStorage, + metaSwapStorage, + tokenIndexFrom, + tokenIndexTo, + dx + ); + } + + /** + * @notice Calculate amount of tokens you receive on swap. For this function, + * the token indices are flattened out so that underlying tokens are represented. + * @param tokenIndexFrom the token the user wants to sell + * @param tokenIndexTo the token the user wants to buy + * @param dx the amount of tokens the user wants to sell. If the token charges + * a fee on transfers, use the amount that gets transferred after the fee. + * @return amount of tokens the user will receive + */ + function calculateSwapUnderlying( + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx + ) external view virtual returns (uint256) { + return + MetaSwapUtilsGaugeSupportV1.calculateSwapUnderlying( + swapStorage, + metaSwapStorage, + tokenIndexFrom, + tokenIndexTo, + dx + ); + } + + /** + * @notice A simple method to calculate prices from deposits or + * withdrawals, excluding fees but including slippage. This is + * helpful as an input into the various "min" parameters on calls + * to fight front-running + * + * @dev This shouldn't be used outside frontends for user estimates. + * + * @param amounts an array of token amounts to deposit or withdrawal, + * corresponding to pooledTokens. The amount should be in each + * pooled token's native precision. If a token charges a fee on transfers, + * use the amount that gets transferred after the fee. + * @param deposit whether this is a deposit or a withdrawal + * @return token amount the user will receive + */ + function calculateTokenAmount(uint256[] calldata amounts, bool deposit) + external + view + virtual + override + returns (uint256) + { + return + MetaSwapUtilsGaugeSupportV1.calculateTokenAmount( + swapStorage, + metaSwapStorage, + amounts, + deposit + ); + } + + /** + * @notice Calculate the amount of underlying token available to withdraw + * when withdrawing via only single token + * @param tokenAmount the amount of LP token to burn + * @param tokenIndex index of which token will be withdrawn + * @return availableTokenAmount calculated amount of underlying token + * available to withdraw + */ + function calculateRemoveLiquidityOneToken( + uint256 tokenAmount, + uint8 tokenIndex + ) external view virtual override returns (uint256) { + return + MetaSwapUtilsGaugeSupportV1.calculateWithdrawOneToken( + swapStorage, + metaSwapStorage, + tokenAmount, + tokenIndex + ); + } + + /*** STATE MODIFYING FUNCTIONS ***/ + + /** + * @notice This function is overriden to prevent the use of the original + * Swap's initialize function. Use initializeMetaSwap() instead. + */ + function initialize( + IERC20[] memory, + uint8[] memory, + string memory, + string memory, + uint256, + uint256, + uint256, + address + ) public payable virtual override initializer { + revert("use initializeMetaSwap() instead"); + } + + /** + * @notice Initializes this MetaSwap contract with the given parameters. + * MetaSwap uses an existing Swap pool to expand the available liquidity. + * _pooledTokens array should contain the base Swap pool's LP token as + * the last element. For example, if there is a Swap pool consisting of + * [DAI, USDC, USDT]. Then a MetaSwap pool can be created with [sUSD, BaseSwapLPToken] + * as _pooledTokens. + * + * This will also deploy the LPToken that represents users' + * LP position. The owner of LPToken will be this contract - which means + * only this contract is allowed to mint new tokens. + * + * @param _pooledTokens an array of ERC20s this pool will accept. The last + * element must be an existing Swap pool's LP token's address. + * @param decimals the decimals to use for each pooled token, + * eg 8 for WBTC. Cannot be larger than POOL_PRECISION_DECIMALS + * @param lpTokenName the long-form name of the token to be deployed + * @param lpTokenSymbol the short symbol for the token to be deployed + * @param _a the amplification coefficient * n * (n - 1). See the + * StableSwap paper for details + * @param _fee default swap fee to be initialized with + * @param _adminFee default adminFee to be initialized with + */ + function initializeMetaSwap( + IERC20[] memory _pooledTokens, + uint8[] memory decimals, + string memory lpTokenName, + string memory lpTokenSymbol, + uint256 _a, + uint256 _fee, + uint256 _adminFee, + address lpTokenTargetAddress, + ISwapV2 baseSwap + ) public payable virtual initializer { + __SwapV2_init( + _pooledTokens, + decimals, + lpTokenName, + lpTokenSymbol, + _a, + _fee, + _adminFee, + lpTokenTargetAddress + ); + + // MetaSwap initializer + metaSwapStorage.baseSwap = baseSwap; + metaSwapStorage.baseVirtualPrice = baseSwap.getVirtualPrice(); + metaSwapStorage.baseCacheLastUpdated = block.timestamp; + + // Read all tokens that belong to baseSwap + { + uint8 i; + for (; i < 32; i++) { + try baseSwap.getToken(i) returns (IERC20 token) { + metaSwapStorage.baseTokens.push(token); + token.safeApprove(address(baseSwap), MAX_UINT256); + } catch { + break; + } + } + require(i > 1, "baseSwap must pool at least 2 tokens"); + } + + // Check the last element of _pooledTokens is a gauge token + // This is a naiive check + IERC20 guageToken = _pooledTokens[_pooledTokens.length - 1]; + LPTokenV2 baseLPToken = LPTokenV2( + address(IGauge(address(guageToken)).lp_token()) + ); + require( + baseLPToken.owner() == address(baseSwap), + "baseLPToken is not owned by baseSwap" + ); + + // Pre-approve the baseLPToken to be used by baseSwap + IERC20(address(baseLPToken)).safeApprove( + address(baseSwap), + MAX_UINT256 + ); + // Pre-approve the baseLPToken to be used by the gauge contract + IERC20(address(baseLPToken)).safeApprove( + address(guageToken), + MAX_UINT256 + ); + + __GaugeTokenHolder_init(address(guageToken)); + } + + /** + * @notice Swap two tokens using this pool + * @param tokenIndexFrom the token the user wants to swap from + * @param tokenIndexTo the token the user wants to swap to + * @param dx the amount of tokens the user wants to swap from + * @param minDy the min amount the user would like to receive, or revert. + * @param deadline latest timestamp to accept this transaction + */ + function swap( + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx, + uint256 minDy, + uint256 deadline + ) + external + payable + virtual + override + nonReentrant + whenNotPaused + deadlineCheck(deadline) + returns (uint256) + { + return + MetaSwapUtilsGaugeSupportV1.swap( + swapStorage, + metaSwapStorage, + tokenIndexFrom, + tokenIndexTo, + dx, + minDy + ); + } + + /** + * @notice Swap two tokens using this pool and the base pool. + * @param tokenIndexFrom the token the user wants to swap from + * @param tokenIndexTo the token the user wants to swap to + * @param dx the amount of tokens the user wants to swap from + * @param minDy the min amount the user would like to receive, or revert. + * @param deadline latest timestamp to accept this transaction + */ + function swapUnderlying( + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx, + uint256 minDy, + uint256 deadline + ) + external + virtual + nonReentrant + whenNotPaused + deadlineCheck(deadline) + returns (uint256) + { + return + MetaSwapUtilsGaugeSupportV1.swapUnderlying( + swapStorage, + metaSwapStorage, + tokenIndexFrom, + tokenIndexTo, + dx, + minDy + ); + } + + /** + * @notice Add liquidity to the pool with the given amounts of tokens + * @param amounts the amounts of each token to add, in their native precision + * @param minToMint the minimum LP tokens adding this amount of liquidity + * should mint, otherwise revert. Handy for front-running mitigation + * @param deadline latest timestamp to accept this transaction + * @return amount of LP token user minted and received + */ + function addLiquidity( + uint256[] calldata amounts, + uint256 minToMint, + uint256 deadline + ) + external + payable + virtual + override + nonReentrant + whenNotPaused + deadlineCheck(deadline) + returns (uint256) + { + return + MetaSwapUtilsGaugeSupportV1.addLiquidity( + swapStorage, + metaSwapStorage, + amounts, + minToMint + ); + } + + /** + * @notice Remove liquidity from the pool all in one token. Withdraw fee that decays linearly + * over period of 4 weeks since last deposit will apply. + * @param tokenAmount the amount of the token you want to receive + * @param tokenIndex the index of the token you want to receive + * @param minAmount the minimum amount to withdraw, otherwise revert + * @param deadline latest timestamp to accept this transaction + * @return amount of chosen token user received + */ + function removeLiquidityOneToken( + uint256 tokenAmount, + uint8 tokenIndex, + uint256 minAmount, + uint256 deadline + ) + external + payable + virtual + override + nonReentrant + whenNotPaused + deadlineCheck(deadline) + returns (uint256) + { + return + MetaSwapUtilsGaugeSupportV1.removeLiquidityOneToken( + swapStorage, + metaSwapStorage, + tokenAmount, + tokenIndex, + minAmount + ); + } + + /** + * @notice Remove liquidity from the pool, weighted differently than the + * pool's current balances. Withdraw fee that decays linearly + * over period of 4 weeks since last deposit will apply. + * @param amounts how much of each token to withdraw + * @param maxBurnAmount the max LP token provider is willing to pay to + * remove liquidity. Useful as a front-running mitigation. + * @param deadline latest timestamp to accept this transaction + * @return amount of LP tokens burned + */ + function removeLiquidityImbalance( + uint256[] calldata amounts, + uint256 maxBurnAmount, + uint256 deadline + ) + external + payable + virtual + override + nonReentrant + whenNotPaused + deadlineCheck(deadline) + returns (uint256) + { + return + MetaSwapUtilsGaugeSupportV1.removeLiquidityImbalance( + swapStorage, + metaSwapStorage, + amounts, + maxBurnAmount + ); + } + + /** + * @notice Sets the reward receiver for the gauge rewards. + * @param _rewardReceiver the address to receive the gauge rewards + * @dev Both the SDL gauge reward and the third party rewards will be + * forwarded to this address. + */ + function setRewardReceiver(address _rewardReceiver) external onlyOwner { + GaugeTokenHolder._setRewardReceiver(_rewardReceiver); + } +} diff --git a/contracts/meta/MetaSwapUtilsGaugeSupportV1.sol b/contracts/meta/MetaSwapUtilsGaugeSupportV1.sol new file mode 100644 index 00000000..e60ec260 --- /dev/null +++ b/contracts/meta/MetaSwapUtilsGaugeSupportV1.sol @@ -0,0 +1,1227 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.17; + +import "@openzeppelin/contracts-4.7.3/token/ERC20/utils/SafeERC20.sol"; +import "../LPTokenV2.sol"; +import "../interfaces/ISwapV2.sol"; +import "../interfaces/IGauge.sol"; +import "../MathUtilsV1.sol"; +import "../SwapUtilsV2.sol"; + +/** + * @title MetaSwapUtils library with gauge support + * @notice A library to be used within MetaSwap.sol. Contains functions responsible for custody and AMM functionalities. + * + * MetaSwap is a modified version of Swap that allows Swap's LP token to be utilized in pooling with other tokens. + * As an example, if there is a Swap pool consisting of [DAI, USDC, USDT]. Then a MetaSwap pool can be created + * with [sUSD, BaseSwapLPToken] to allow trades between either the LP token or the underlying tokens and sUSD. + * + * @dev Contracts relying on this library must initialize SwapUtils.Swap struct then use this library + * for SwapUtils.Swap struct. Note that this library contains both functions called by users and admins. + * Admin functions should be protected within contracts using this library. + */ +library MetaSwapUtilsGaugeSupportV1 { + using SafeERC20 for IERC20; + using MathUtilsV1 for uint256; + using AmplificationUtilsV2 for SwapUtilsV2.Swap; + + /*** EVENTS ***/ + + event TokenSwap( + address indexed buyer, + uint256 tokensSold, + uint256 tokensBought, + uint128 soldId, + uint128 boughtId + ); + event TokenSwapUnderlying( + address indexed buyer, + uint256 tokensSold, + uint256 tokensBought, + uint128 soldId, + uint128 boughtId + ); + event AddLiquidity( + address indexed provider, + uint256[] tokenAmounts, + uint256[] fees, + uint256 invariant, + uint256 lpTokenSupply + ); + event RemoveLiquidityOne( + address indexed provider, + uint256 lpTokenAmount, + uint256 lpTokenSupply, + uint256 boughtId, + uint256 tokensBought + ); + event RemoveLiquidityImbalance( + address indexed provider, + uint256[] tokenAmounts, + uint256[] fees, + uint256 invariant, + uint256 lpTokenSupply + ); + event NewAdminFee(uint256 newAdminFee); + event NewSwapFee(uint256 newSwapFee); + event NewWithdrawFee(uint256 newWithdrawFee); + + struct MetaSwap { + // Meta-Swap related parameters + ISwapV2 baseSwap; + uint256 baseVirtualPrice; + uint256 baseCacheLastUpdated; + IERC20[] baseTokens; + } + + // Struct storing variables used in calculations in the + // calculateWithdrawOneTokenDY function to avoid stack too deep errors + struct CalculateWithdrawOneTokenDYInfo { + uint256 d0; + uint256 d1; + uint256 newY; + uint256 feePerToken; + uint256 preciseA; + uint256 xpi; + } + + // Struct storing variables used in calculation in removeLiquidityImbalance function + // to avoid stack too deep error + struct ManageLiquidityInfo { + uint256 d0; + uint256 d1; + uint256 d2; + LPTokenV2 lpToken; + uint256 totalSupply; + uint256 preciseA; + uint256 baseVirtualPrice; + uint256[] tokenPrecisionMultipliers; + uint256[] newBalances; + } + + struct SwapUnderlyingInfo { + uint256 x; + uint256 dx; + uint256 dy; + uint256[] tokenPrecisionMultipliers; + uint256[] oldBalances; + IERC20[] baseTokens; + IERC20 tokenFrom; + uint8 metaIndexFrom; + IERC20 tokenTo; + uint8 metaIndexTo; + uint256 baseVirtualPrice; + } + + struct CalculateSwapUnderlyingInfo { + uint256 baseVirtualPrice; + ISwapV2 baseSwap; + uint8 baseLPTokenIndex; + uint8 baseTokensLength; + uint8 metaIndexTo; + uint256 x; + uint256 dy; + } + + // the denominator used to calculate admin and LP fees. For example, an + // LP fee might be something like tradeAmount.mul(fee).div(FEE_DENOMINATOR) + uint256 private constant FEE_DENOMINATOR = 10**10; + + // Cache expire time for the stored value of base Swap's virtual price + uint256 public constant BASE_CACHE_EXPIRE_TIME = 10 minutes; + uint256 public constant BASE_VIRTUAL_PRICE_PRECISION = 10**18; + + /*** VIEW & PURE FUNCTIONS ***/ + + /** + * @notice Return the stored value of base Swap's virtual price. If + * value was updated past BASE_CACHE_EXPIRE_TIME, then read it directly + * from the base Swap contract. + * @param metaSwapStorage MetaSwap struct to read from + * @return base Swap's virtual price + */ + function _getBaseVirtualPrice(MetaSwap storage metaSwapStorage) + internal + view + returns (uint256) + { + if ( + block.timestamp > + metaSwapStorage.baseCacheLastUpdated + BASE_CACHE_EXPIRE_TIME + ) { + return metaSwapStorage.baseSwap.getVirtualPrice(); + } + return metaSwapStorage.baseVirtualPrice; + } + + function _getBaseSwapFee(ISwapV2 baseSwap) + internal + view + returns (uint256 swapFee) + { + (, , , , swapFee, , ) = baseSwap.swapStorage(); + } + + /** + * @notice Calculate how much the user would receive when withdrawing via single token + * @param self Swap struct to read from + * @param metaSwapStorage MetaSwap struct to read from + * @param tokenAmount the amount to withdraw in the pool's precision + * @param tokenIndex which token will be withdrawn + * @return dy the amount of token user will receive + */ + function calculateWithdrawOneToken( + SwapUtilsV2.Swap storage self, + MetaSwap storage metaSwapStorage, + uint256 tokenAmount, + uint8 tokenIndex + ) external view returns (uint256 dy) { + (dy, ) = _calculateWithdrawOneToken( + self, + tokenAmount, + tokenIndex, + _getBaseVirtualPrice(metaSwapStorage), + self.lpToken.totalSupply() + ); + } + + function _calculateWithdrawOneToken( + SwapUtilsV2.Swap storage self, + uint256 tokenAmount, + uint8 tokenIndex, + uint256 baseVirtualPrice, + uint256 totalSupply + ) internal view returns (uint256, uint256) { + uint256 dy; + uint256 dySwapFee; + + { + uint256 currentY; + uint256 newY; + + // Calculate how much to withdraw + (dy, newY, currentY) = _calculateWithdrawOneTokenDY( + self, + tokenIndex, + tokenAmount, + baseVirtualPrice, + totalSupply + ); + + // Calculate the associated swap fee + dySwapFee = + ((currentY - newY) / + self.tokenPrecisionMultipliers[tokenIndex]) - + dy; + } + + return (dy, dySwapFee); + } + + /** + * @notice Calculate the dy of withdrawing in one token + * @param self Swap struct to read from + * @param tokenIndex which token will be withdrawn + * @param tokenAmount the amount to withdraw in the pools precision + * @param baseVirtualPrice the virtual price of the base swap's LP token + * @return the dy excluding swap fee, the new y after withdrawing one token, and current y + */ + function _calculateWithdrawOneTokenDY( + SwapUtilsV2.Swap storage self, + uint8 tokenIndex, + uint256 tokenAmount, + uint256 baseVirtualPrice, + uint256 totalSupply + ) + internal + view + returns ( + uint256, + uint256, + uint256 + ) + { + // Get the current D, then solve the stableswap invariant + // y_i for D - tokenAmount + uint256[] memory xp = _xp(self, baseVirtualPrice); + require(tokenIndex < xp.length, "Token index out of range"); + + CalculateWithdrawOneTokenDYInfo + memory v = CalculateWithdrawOneTokenDYInfo( + 0, + 0, + 0, + 0, + self._getAPrecise(), + 0 + ); + v.d0 = SwapUtilsV2.getD(xp, v.preciseA); + v.d1 = v.d0 - ((tokenAmount * v.d0) / totalSupply); + + require(tokenAmount <= xp[tokenIndex], "Withdraw exceeds available"); + + v.newY = SwapUtilsV2.getYD(v.preciseA, tokenIndex, xp, v.d1); + + uint256[] memory xpReduced = new uint256[](xp.length); + + v.feePerToken = SwapUtilsV2._feePerToken(self.swapFee, xp.length); + for (uint256 i = 0; i < xp.length; i++) { + v.xpi = xp[i]; + // if i == tokenIndex, dxExpected = xp[i] * d1 / d0 - newY + // else dxExpected = xp[i] - (xp[i] * d1 / d0) + // xpReduced[i] -= dxExpected * fee / FEE_DENOMINATOR + xpReduced[i] = + v.xpi - + ((( + (i == tokenIndex) + ? (v.xpi * v.d1) / v.d0 - v.newY + : v.xpi - ((v.xpi * v.d1) / v.d0) + ) * v.feePerToken) / FEE_DENOMINATOR); + } + + uint256 dy = xpReduced[tokenIndex] - + SwapUtilsV2.getYD(v.preciseA, tokenIndex, xpReduced, v.d1); + + if (tokenIndex == xp.length - 1) { + dy = (dy * BASE_VIRTUAL_PRICE_PRECISION) / baseVirtualPrice; + v.newY = (v.newY * BASE_VIRTUAL_PRICE_PRECISION) / baseVirtualPrice; + xp[tokenIndex] = + (xp[tokenIndex] * BASE_VIRTUAL_PRICE_PRECISION) / + baseVirtualPrice; + } + dy = (dy - 1) / self.tokenPrecisionMultipliers[tokenIndex]; + + return (dy, v.newY, xp[tokenIndex]); + } + + /** + * @notice Given a set of balances and precision multipliers, return the + * precision-adjusted balances. The last element will also get scaled up by + * the given baseVirtualPrice. + * + * @param balances an array of token balances, in their native precisions. + * These should generally correspond with pooled tokens. + * + * @param precisionMultipliers an array of multipliers, corresponding to + * the amounts in the balances array. When multiplied together they + * should yield amounts at the pool's precision. + * + * @param baseVirtualPrice the base virtual price to scale the balance of the + * base Swap's LP token. + * + * @return an array of amounts "scaled" to the pool's precision + */ + function _xp( + uint256[] memory balances, + uint256[] memory precisionMultipliers, + uint256 baseVirtualPrice + ) internal pure returns (uint256[] memory) { + uint256[] memory xp = SwapUtilsV2._xp(balances, precisionMultipliers); + uint256 baseLPTokenIndex = balances.length - 1; + xp[baseLPTokenIndex] = + (xp[baseLPTokenIndex] * baseVirtualPrice) / + BASE_VIRTUAL_PRICE_PRECISION; + return xp; + } + + /** + * @notice Return the precision-adjusted balances of all tokens in the pool + * @param self Swap struct to read from + * @return the pool balances "scaled" to the pool's precision, allowing + * them to be more easily compared. + */ + function _xp(SwapUtilsV2.Swap storage self, uint256 baseVirtualPrice) + internal + view + returns (uint256[] memory) + { + return + _xp( + self.balances, + self.tokenPrecisionMultipliers, + baseVirtualPrice + ); + } + + /** + * @notice Get the virtual price, to help calculate profit + * @param self Swap struct to read from + * @param metaSwapStorage MetaSwap struct to read from + * @return the virtual price, scaled to precision of BASE_VIRTUAL_PRICE_PRECISION + */ + function getVirtualPrice( + SwapUtilsV2.Swap storage self, + MetaSwap storage metaSwapStorage + ) external view returns (uint256) { + uint256 d = SwapUtilsV2.getD( + _xp( + self.balances, + self.tokenPrecisionMultipliers, + _getBaseVirtualPrice(metaSwapStorage) + ), + self._getAPrecise() + ); + uint256 supply = self.lpToken.totalSupply(); + if (supply != 0) { + return (d * BASE_VIRTUAL_PRICE_PRECISION) / supply; + } + return 0; + } + + /** + * @notice Externally calculates a swap between two tokens. The SwapUtils.Swap storage and + * MetaSwap storage should be from the same MetaSwap contract. + * @param self Swap struct to read from + * @param metaSwapStorage MetaSwap struct from the same contract + * @param tokenIndexFrom the token to sell + * @param tokenIndexTo the token to buy + * @param dx the number of tokens to sell. If the token charges a fee on transfers, + * use the amount that gets transferred after the fee. + * @return dy the number of tokens the user will get + */ + function calculateSwap( + SwapUtilsV2.Swap storage self, + MetaSwap storage metaSwapStorage, + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx + ) external view returns (uint256 dy) { + (dy, ) = _calculateSwap( + self, + tokenIndexFrom, + tokenIndexTo, + dx, + _getBaseVirtualPrice(metaSwapStorage) + ); + } + + /** + * @notice Internally calculates a swap between two tokens. + * + * @dev The caller is expected to transfer the actual amounts (dx and dy) + * using the token contracts. + * + * @param self Swap struct to read from + * @param tokenIndexFrom the token to sell + * @param tokenIndexTo the token to buy + * @param dx the number of tokens to sell. If the token charges a fee on transfers, + * use the amount that gets transferred after the fee. + * @param baseVirtualPrice the virtual price of the base LP token + * @return dy the number of tokens the user will get and dyFee the associated fee + */ + function _calculateSwap( + SwapUtilsV2.Swap storage self, + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx, + uint256 baseVirtualPrice + ) internal view returns (uint256 dy, uint256 dyFee) { + uint256[] memory xp = _xp(self, baseVirtualPrice); + require( + tokenIndexFrom < xp.length && tokenIndexTo < xp.length, + "Token index out of range" + ); + uint256 baseLPTokenIndex = xp.length - 1; + + uint256 x = dx * self.tokenPrecisionMultipliers[tokenIndexFrom]; + if (tokenIndexFrom == baseLPTokenIndex) { + // When swapping from a base Swap token, scale up dx by its virtual price + x = (x * baseVirtualPrice) / BASE_VIRTUAL_PRICE_PRECISION; + } + x = x + xp[tokenIndexFrom]; + + uint256 y = SwapUtilsV2.getY( + self._getAPrecise(), + tokenIndexFrom, + tokenIndexTo, + x, + xp + ); + dy = xp[tokenIndexTo] - y - 1; + + if (tokenIndexTo == baseLPTokenIndex) { + // When swapping to a base Swap token, scale down dy by its virtual price + dy = (dy * BASE_VIRTUAL_PRICE_PRECISION) / baseVirtualPrice; + } + + dyFee = (dy * self.swapFee) / FEE_DENOMINATOR; + dy = dy - dyFee; + + dy = dy / self.tokenPrecisionMultipliers[tokenIndexTo]; + } + + /** + * @notice Calculates the expected return amount from swapping between + * the pooled tokens and the underlying tokens of the base Swap pool. + * + * @param self Swap struct to read from + * @param metaSwapStorage MetaSwap struct from the same contract + * @param tokenIndexFrom the token to sell + * @param tokenIndexTo the token to buy + * @param dx the number of tokens to sell. If the token charges a fee on transfers, + * use the amount that gets transferred after the fee. + * @return dy the number of tokens the user will get + */ + function calculateSwapUnderlying( + SwapUtilsV2.Swap storage self, + MetaSwap storage metaSwapStorage, + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx + ) external view returns (uint256) { + CalculateSwapUnderlyingInfo memory v = CalculateSwapUnderlyingInfo( + _getBaseVirtualPrice(metaSwapStorage), + metaSwapStorage.baseSwap, + 0, + uint8(metaSwapStorage.baseTokens.length), + 0, + 0, + 0 + ); + + uint256[] memory xp = _xp(self, v.baseVirtualPrice); + v.baseLPTokenIndex = uint8(xp.length - 1); + { + uint8 maxRange = v.baseLPTokenIndex + v.baseTokensLength; + require( + tokenIndexFrom < maxRange && tokenIndexTo < maxRange, + "Token index out of range" + ); + } + + if (tokenIndexFrom < v.baseLPTokenIndex) { + // tokenFrom is from this pool + v.x = + xp[tokenIndexFrom] + + (dx * self.tokenPrecisionMultipliers[tokenIndexFrom]); + } else { + // tokenFrom is from the base pool + tokenIndexFrom = tokenIndexFrom - v.baseLPTokenIndex; + if (tokenIndexTo < v.baseLPTokenIndex) { + uint256[] memory baseInputs = new uint256[](v.baseTokensLength); + baseInputs[tokenIndexFrom] = dx; + v.x = + (v.baseSwap.calculateTokenAmount(baseInputs, true) * + v.baseVirtualPrice) / + BASE_VIRTUAL_PRICE_PRECISION; + // when adding to the base pool,you pay approx 50% of the swap fee + v.x = + v.x - + ((v.x * _getBaseSwapFee(metaSwapStorage.baseSwap)) / + (FEE_DENOMINATOR * 2)) + + xp[v.baseLPTokenIndex]; + } else { + // both from and to are from the base pool + return + v.baseSwap.calculateSwap( + tokenIndexFrom, + tokenIndexTo - v.baseLPTokenIndex, + dx + ); + } + tokenIndexFrom = v.baseLPTokenIndex; + } + + v.metaIndexTo = v.baseLPTokenIndex; + if (tokenIndexTo < v.baseLPTokenIndex) { + v.metaIndexTo = tokenIndexTo; + } + + { + uint256 y = SwapUtilsV2.getY( + self._getAPrecise(), + tokenIndexFrom, + v.metaIndexTo, + v.x, + xp + ); + v.dy = xp[v.metaIndexTo] - y - 1; + uint256 dyFee = (v.dy * self.swapFee) / FEE_DENOMINATOR; + v.dy = v.dy - dyFee; + } + + if (tokenIndexTo < v.baseLPTokenIndex) { + // tokenTo is from this pool + v.dy = v.dy / self.tokenPrecisionMultipliers[v.metaIndexTo]; + } else { + // tokenTo is from the base pool + v.dy = v.baseSwap.calculateRemoveLiquidityOneToken( + (v.dy * BASE_VIRTUAL_PRICE_PRECISION) / v.baseVirtualPrice, + tokenIndexTo - v.baseLPTokenIndex + ); + } + + return v.dy; + } + + /** + * @notice A simple method to calculate prices from deposits or + * withdrawals, excluding fees but including slippage. This is + * helpful as an input into the various "min" parameters on calls + * to fight front-running + * + * @dev This shouldn't be used outside frontends for user estimates. + * + * @param self Swap struct to read from + * @param metaSwapStorage MetaSwap struct to read from + * @param amounts an array of token amounts to deposit or withdrawal, + * corresponding to pooledTokens. The amount should be in each + * pooled token's native precision. If a token charges a fee on transfers, + * use the amount that gets transferred after the fee. + * @param deposit whether this is a deposit or a withdrawal + * @return if deposit was true, total amount of lp token that will be minted and if + * deposit was false, total amount of lp token that will be burned + */ + function calculateTokenAmount( + SwapUtilsV2.Swap storage self, + MetaSwap storage metaSwapStorage, + uint256[] calldata amounts, + bool deposit + ) external view returns (uint256) { + uint256 a = self._getAPrecise(); + uint256 d0; + uint256 d1; + { + uint256 baseVirtualPrice = _getBaseVirtualPrice(metaSwapStorage); + uint256[] memory balances1 = self.balances; + uint256[] memory tokenPrecisionMultipliers = self + .tokenPrecisionMultipliers; + uint256 numTokens = balances1.length; + d0 = SwapUtilsV2.getD( + _xp(balances1, tokenPrecisionMultipliers, baseVirtualPrice), + a + ); + for (uint256 i = 0; i < numTokens; i++) { + if (deposit) { + balances1[i] = balances1[i] + amounts[i]; + } else { + if (amounts[i] > balances1[i]) { + revert("Cannot withdraw more than available"); + } else { + unchecked { + balances1[i] = balances1[i] - amounts[i]; + } + } + } + } + d1 = SwapUtilsV2.getD( + _xp(balances1, tokenPrecisionMultipliers, baseVirtualPrice), + a + ); + } + uint256 totalSupply = self.lpToken.totalSupply(); + + if (deposit) { + return ((d1 - d0) * totalSupply) / d0; + } else { + return ((d0 - d1) * totalSupply) / d0; + } + } + + /*** STATE MODIFYING FUNCTIONS ***/ + + /** + * @notice swap two tokens in the pool + * @param self Swap struct to read from and write to + * @param metaSwapStorage MetaSwap struct to read from and write to + * @param tokenIndexFrom the token the user wants to sell + * @param tokenIndexTo the token the user wants to buy + * @param dx the amount of tokens the user wants to sell + * @param minDy the min amount the user would like to receive, or revert. + * @return amount of token user received on swap + */ + function swap( + SwapUtilsV2.Swap storage self, + MetaSwap storage metaSwapStorage, + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx, + uint256 minDy + ) external returns (uint256) { + { + uint256 pooledTokensLength = self.pooledTokens.length; + require( + tokenIndexFrom < pooledTokensLength && + tokenIndexTo < pooledTokensLength, + "Token index is out of range" + ); + } + + uint256 transferredDx; + { + IERC20 tokenFrom = self.pooledTokens[tokenIndexFrom]; + require( + dx <= tokenFrom.balanceOf(msg.sender), + "Cannot swap more than you own" + ); + + { + // Transfer tokens first to see if a fee was charged on transfer + uint256 beforeBalance = tokenFrom.balanceOf(address(this)); + tokenFrom.safeTransferFrom(msg.sender, address(this), dx); + + // Use the actual transferred amount for AMM math + transferredDx = + tokenFrom.balanceOf(address(this)) - + beforeBalance; + } + } + + (uint256 dy, uint256 dyFee) = _calculateSwap( + self, + tokenIndexFrom, + tokenIndexTo, + transferredDx, + _updateBaseVirtualPrice(metaSwapStorage) + ); + require(dy >= minDy, "Swap didn't result in min tokens"); + + uint256 dyAdminFee = (dyFee * self.adminFee) / + FEE_DENOMINATOR / + self.tokenPrecisionMultipliers[tokenIndexTo]; + + self.balances[tokenIndexFrom] = + self.balances[tokenIndexFrom] + + transferredDx; + self.balances[tokenIndexTo] = + self.balances[tokenIndexTo] - + dy - + dyAdminFee; + + self.pooledTokens[tokenIndexTo].safeTransfer(msg.sender, dy); + + emit TokenSwap( + msg.sender, + transferredDx, + dy, + tokenIndexFrom, + tokenIndexTo + ); + + return dy; + } + + /** + * @notice Swaps with the underlying tokens of the base Swap pool. For this function, + * the token indices are flattened out so that underlying tokens are represented + * in the indices. + * @dev Since this calls multiple external functions during the execution, + * it is recommended to protect any function that depends on this with reentrancy guards. + * @param self Swap struct to read from and write to + * @param metaSwapStorage MetaSwap struct to read from and write to + * @param tokenIndexFrom the token the user wants to sell + * @param tokenIndexTo the token the user wants to buy + * @param dx the amount of tokens the user wants to sell + * @param minDy the min amount the user would like to receive, or revert. + * @return amount of token user received on swap + */ + function swapUnderlying( + SwapUtilsV2.Swap storage self, + MetaSwap storage metaSwapStorage, + uint8 tokenIndexFrom, + uint8 tokenIndexTo, + uint256 dx, + uint256 minDy + ) external returns (uint256) { + SwapUnderlyingInfo memory v = SwapUnderlyingInfo( + 0, + 0, + 0, + self.tokenPrecisionMultipliers, + self.balances, + metaSwapStorage.baseTokens, + IERC20(address(0)), + 0, + IERC20(address(0)), + 0, + _updateBaseVirtualPrice(metaSwapStorage) + ); + + uint8 baseLPTokenIndex = uint8(v.oldBalances.length - 1); + + { + uint8 maxRange = uint8(baseLPTokenIndex + v.baseTokens.length); + require( + tokenIndexFrom < maxRange && tokenIndexTo < maxRange, + "Token index out of range" + ); + } + + ISwapV2 baseSwap = metaSwapStorage.baseSwap; + + // Find the address of the token swapping from and the index in MetaSwap's token list + if (tokenIndexFrom < baseLPTokenIndex) { + v.tokenFrom = self.pooledTokens[tokenIndexFrom]; + v.metaIndexFrom = tokenIndexFrom; + } else { + v.tokenFrom = v.baseTokens[tokenIndexFrom - baseLPTokenIndex]; + v.metaIndexFrom = baseLPTokenIndex; + } + + // Find the address of the token swapping to and the index in MetaSwap's token list + if (tokenIndexTo < baseLPTokenIndex) { + v.tokenTo = self.pooledTokens[tokenIndexTo]; + v.metaIndexTo = tokenIndexTo; + } else { + v.tokenTo = v.baseTokens[tokenIndexTo - baseLPTokenIndex]; + v.metaIndexTo = baseLPTokenIndex; + } + + // Check for possible fee on transfer + v.dx = v.tokenFrom.balanceOf(address(this)); + v.tokenFrom.safeTransferFrom(msg.sender, address(this), dx); + v.dx = v.tokenFrom.balanceOf(address(this)) - v.dx; // update dx in case of fee on transfer + + if ( + tokenIndexFrom < baseLPTokenIndex || tokenIndexTo < baseLPTokenIndex + ) { + // Either one of the tokens belongs to the MetaSwap tokens list + uint256[] memory xp = _xp( + v.oldBalances, + v.tokenPrecisionMultipliers, + v.baseVirtualPrice + ); + + if (tokenIndexFrom < baseLPTokenIndex) { + // Swapping from a MetaSwap token + v.x = + xp[tokenIndexFrom] + + (dx * v.tokenPrecisionMultipliers[tokenIndexFrom]); + } else { + // Swapping from one of the tokens hosted in the base Swap + // This case requires adding the underlying token to the base Swap, then + // using the base LP token to swap to the desired token + uint256[] memory baseAmounts = new uint256[]( + v.baseTokens.length + ); + baseAmounts[tokenIndexFrom - baseLPTokenIndex] = v.dx; + + // Add liquidity to the base Swap contract and receive base LP token + v.dx = baseSwap.addLiquidity(baseAmounts, 0, block.timestamp); + + // Deposit the base LP token to the gauge + IGauge(address(self.pooledTokens[baseLPTokenIndex])).deposit( + v.dx + ); + + // Calculate the value of total amount of baseLPToken we end up with + v.x = + ((v.dx * v.baseVirtualPrice) / + BASE_VIRTUAL_PRICE_PRECISION) + + xp[baseLPTokenIndex]; + } + + // Calculate how much to withdraw in MetaSwap level and the the associated swap fee + uint256 dyFee; + { + uint256 y = SwapUtilsV2.getY( + self._getAPrecise(), + v.metaIndexFrom, + v.metaIndexTo, + v.x, + xp + ); + v.dy = xp[v.metaIndexTo] - y - 1; + if (tokenIndexTo >= baseLPTokenIndex) { + // When swapping to a base Swap token, scale down dy by its virtual price + v.dy = + (v.dy * BASE_VIRTUAL_PRICE_PRECISION) / + v.baseVirtualPrice; + } + dyFee = (v.dy * self.swapFee) / FEE_DENOMINATOR; + v.dy = + (v.dy - dyFee) / + v.tokenPrecisionMultipliers[v.metaIndexTo]; + } + + // Update the balances array according to the calculated input and output amount + { + uint256 dyAdminFee = (dyFee * self.adminFee) / FEE_DENOMINATOR; + dyAdminFee = + dyAdminFee / + v.tokenPrecisionMultipliers[v.metaIndexTo]; + self.balances[v.metaIndexFrom] = + v.oldBalances[v.metaIndexFrom] + + v.dx; + self.balances[v.metaIndexTo] = + v.oldBalances[v.metaIndexTo] - + v.dy - + dyAdminFee; + } + + if (tokenIndexTo >= baseLPTokenIndex) { + // When swapping to a token that belongs to the base Swap, burn the LP token + // and withdraw the desired token from the base pool + uint256 oldBalance = v.tokenTo.balanceOf(address(this)); + + // Withdraw from the gauge to get the base LP token. + IGauge(address(self.pooledTokens[baseLPTokenIndex])).withdraw( + v.dx + ); + + // Burn base LP token receive the desired underlying token + baseSwap.removeLiquidityOneToken( + v.dy, + tokenIndexTo - baseLPTokenIndex, + 0, + block.timestamp + ); + v.dy = v.tokenTo.balanceOf(address(this)) - oldBalance; + } + + // Check the amount of token to send meets minDy + require(v.dy >= minDy, "Swap didn't result in min tokens"); + } else { + // Both tokens are from the base Swap pool + // Do a swap through the base Swap + v.dy = v.tokenTo.balanceOf(address(this)); + baseSwap.swap( + tokenIndexFrom - baseLPTokenIndex, + tokenIndexTo - baseLPTokenIndex, + v.dx, + minDy, + block.timestamp + ); + v.dy = v.tokenTo.balanceOf(address(this)) - v.dy; + } + + // Send the desired token to the caller + v.tokenTo.safeTransfer(msg.sender, v.dy); + + emit TokenSwapUnderlying( + msg.sender, + dx, + v.dy, + tokenIndexFrom, + tokenIndexTo + ); + + return v.dy; + } + + /** + * @notice Add liquidity to the pool + * @param self Swap struct to read from and write to + * @param metaSwapStorage MetaSwap struct to read from and write to + * @param amounts the amounts of each token to add, in their native precision + * @param minToMint the minimum LP tokens adding this amount of liquidity + * should mint, otherwise revert. Handy for front-running mitigation + * allowed addresses. If the pool is not in the guarded launch phase, this parameter will be ignored. + * @return amount of LP token user received + */ + function addLiquidity( + SwapUtilsV2.Swap storage self, + MetaSwap storage metaSwapStorage, + uint256[] memory amounts, + uint256 minToMint + ) external returns (uint256) { + IERC20[] memory pooledTokens = self.pooledTokens; + require( + amounts.length == pooledTokens.length, + "Amounts must match pooled tokens" + ); + + uint256[] memory fees = new uint256[](pooledTokens.length); + + // current state + ManageLiquidityInfo memory v = ManageLiquidityInfo( + 0, + 0, + 0, + self.lpToken, + 0, + self._getAPrecise(), + _updateBaseVirtualPrice(metaSwapStorage), + self.tokenPrecisionMultipliers, + self.balances + ); + v.totalSupply = v.lpToken.totalSupply(); + + if (v.totalSupply != 0) { + v.d0 = SwapUtilsV2.getD( + _xp( + v.newBalances, + v.tokenPrecisionMultipliers, + v.baseVirtualPrice + ), + v.preciseA + ); + } + + for (uint256 i = 0; i < pooledTokens.length; i++) { + require( + v.totalSupply != 0 || amounts[i] > 0, + "Must supply all tokens in pool" + ); + + // Transfer tokens first to see if a fee was charged on transfer + if (amounts[i] != 0) { + uint256 beforeBalance = pooledTokens[i].balanceOf( + address(this) + ); + pooledTokens[i].safeTransferFrom( + msg.sender, + address(this), + amounts[i] + ); + + // Update the amounts[] with actual transfer amount + amounts[i] = + pooledTokens[i].balanceOf(address(this)) - + beforeBalance; + } + + v.newBalances[i] = v.newBalances[i] + amounts[i]; + } + + // invariant after change + v.d1 = SwapUtilsV2.getD( + _xp(v.newBalances, v.tokenPrecisionMultipliers, v.baseVirtualPrice), + v.preciseA + ); + require(v.d1 > v.d0, "D should increase"); + + // updated to reflect fees and calculate the user's LP tokens + v.d2 = v.d1; + uint256 toMint; + + if (v.totalSupply != 0) { + uint256 feePerToken = SwapUtilsV2._feePerToken( + self.swapFee, + pooledTokens.length + ); + for (uint256 i = 0; i < pooledTokens.length; i++) { + uint256 idealBalance = (v.d1 * self.balances[i]) / v.d0; + fees[i] = + (feePerToken * + (idealBalance.difference(v.newBalances[i]))) / + FEE_DENOMINATOR; + self.balances[i] = + v.newBalances[i] - + ((fees[i] * self.adminFee) / FEE_DENOMINATOR); + v.newBalances[i] = v.newBalances[i] - fees[i]; + } + v.d2 = SwapUtilsV2.getD( + _xp( + v.newBalances, + v.tokenPrecisionMultipliers, + v.baseVirtualPrice + ), + v.preciseA + ); + toMint = ((v.d2 - v.d0) * v.totalSupply) / v.d0; + } else { + // the initial depositor doesn't pay fees + self.balances = v.newBalances; + toMint = v.d1; + } + + require(toMint >= minToMint, "Couldn't mint min requested"); + + // mint the user's LP tokens + self.lpToken.mint(msg.sender, toMint); + + emit AddLiquidity( + msg.sender, + amounts, + fees, + v.d1, + v.totalSupply + toMint + ); + + return toMint; + } + + /** + * @notice Remove liquidity from the pool all in one token. + * @param self Swap struct to read from and write to + * @param metaSwapStorage MetaSwap struct to read from and write to + * @param tokenAmount the amount of the lp tokens to burn + * @param tokenIndex the index of the token you want to receive + * @param minAmount the minimum amount to withdraw, otherwise revert + * @return amount chosen token that user received + */ + function removeLiquidityOneToken( + SwapUtilsV2.Swap storage self, + MetaSwap storage metaSwapStorage, + uint256 tokenAmount, + uint8 tokenIndex, + uint256 minAmount + ) external returns (uint256) { + LPTokenV2 lpToken = self.lpToken; + uint256 totalSupply = lpToken.totalSupply(); + uint256 numTokens = self.pooledTokens.length; + require(tokenAmount <= lpToken.balanceOf(msg.sender), ">LP.balanceOf"); + require(tokenIndex < numTokens, "Token not found"); + + uint256 dyFee; + uint256 dy; + + (dy, dyFee) = _calculateWithdrawOneToken( + self, + tokenAmount, + tokenIndex, + _updateBaseVirtualPrice(metaSwapStorage), + totalSupply + ); + + require(dy >= minAmount, "dy < minAmount"); + + // Update balances array + self.balances[tokenIndex] = + self.balances[tokenIndex] - + (dy + ((dyFee * self.adminFee) / FEE_DENOMINATOR)); + + // Burn the associated LP token from the caller and send the desired token + lpToken.burnFrom(msg.sender, tokenAmount); + self.pooledTokens[tokenIndex].safeTransfer(msg.sender, dy); + + emit RemoveLiquidityOne( + msg.sender, + tokenAmount, + totalSupply, + tokenIndex, + dy + ); + + return dy; + } + + /** + * @notice Remove liquidity from the pool, weighted differently than the + * pool's current balances. + * + * @param self Swap struct to read from and write to + * @param metaSwapStorage MetaSwap struct to read from and write to + * @param amounts how much of each token to withdraw + * @param maxBurnAmount the max LP token provider is willing to pay to + * remove liquidity. Useful as a front-running mitigation. + * @return actual amount of LP tokens burned in the withdrawal + */ + function removeLiquidityImbalance( + SwapUtilsV2.Swap storage self, + MetaSwap storage metaSwapStorage, + uint256[] memory amounts, + uint256 maxBurnAmount + ) public returns (uint256) { + // Using this struct to avoid stack too deep error + ManageLiquidityInfo memory v = ManageLiquidityInfo( + 0, + 0, + 0, + self.lpToken, + 0, + self._getAPrecise(), + _updateBaseVirtualPrice(metaSwapStorage), + self.tokenPrecisionMultipliers, + self.balances + ); + v.totalSupply = v.lpToken.totalSupply(); + + require( + amounts.length == v.newBalances.length, + "Amounts should match pool tokens" + ); + require(maxBurnAmount != 0, "Must burn more than 0"); + + uint256 feePerToken = SwapUtilsV2._feePerToken( + self.swapFee, + v.newBalances.length + ); + + // Calculate how much LPToken should be burned + uint256[] memory fees = new uint256[](v.newBalances.length); + { + uint256[] memory balances1 = new uint256[](v.newBalances.length); + + v.d0 = SwapUtilsV2.getD( + _xp( + v.newBalances, + v.tokenPrecisionMultipliers, + v.baseVirtualPrice + ), + v.preciseA + ); + for (uint256 i = 0; i < v.newBalances.length; i++) { + if (amounts[i] > v.newBalances[i]) { + revert("Cannot withdraw more than available"); + } else { + unchecked { + balances1[i] = v.newBalances[i] - amounts[i]; + } + } + } + v.d1 = SwapUtilsV2.getD( + _xp(balances1, v.tokenPrecisionMultipliers, v.baseVirtualPrice), + v.preciseA + ); + + for (uint256 i = 0; i < v.newBalances.length; i++) { + uint256 idealBalance = (v.d1 * v.newBalances[i]) / v.d0; + uint256 difference = idealBalance.difference(balances1[i]); + fees[i] = (feePerToken * difference) / FEE_DENOMINATOR; + self.balances[i] = + balances1[i] - + ((fees[i] * self.adminFee) / FEE_DENOMINATOR); + balances1[i] = balances1[i] - fees[i]; + } + + v.d2 = SwapUtilsV2.getD( + _xp(balances1, v.tokenPrecisionMultipliers, v.baseVirtualPrice), + v.preciseA + ); + } + + uint256 tokenAmount = ((v.d0 - v.d2) * v.totalSupply) / v.d0; + require(tokenAmount != 0, "Burnt amount cannot be zero"); + + // Scale up by withdraw fee + tokenAmount = tokenAmount + 1; + + // Check for max burn amount + require(tokenAmount <= maxBurnAmount, "tokenAmount > maxBurnAmount"); + + // Burn the calculated amount of LPToken from the caller and send the desired tokens + v.lpToken.burnFrom(msg.sender, tokenAmount); + for (uint256 i = 0; i < v.newBalances.length; i++) { + self.pooledTokens[i].safeTransfer(msg.sender, amounts[i]); + } + + emit RemoveLiquidityImbalance( + msg.sender, + amounts, + fees, + v.d1, + v.totalSupply - tokenAmount + ); + + return tokenAmount; + } + + /** + * @notice Determines if the stored value of base Swap's virtual price is expired. + * If the last update was past the BASE_CACHE_EXPIRE_TIME, then update the stored value. + * + * @param metaSwapStorage MetaSwap struct to read from and write to + * @return base Swap's virtual price + */ + function _updateBaseVirtualPrice(MetaSwap storage metaSwapStorage) + internal + returns (uint256) + { + if ( + block.timestamp > + metaSwapStorage.baseCacheLastUpdated + BASE_CACHE_EXPIRE_TIME + ) { + // When the cache is expired, update it + uint256 baseVirtualPrice = ISwapV2(metaSwapStorage.baseSwap) + .getVirtualPrice(); + metaSwapStorage.baseVirtualPrice = baseVirtualPrice; + metaSwapStorage.baseCacheLastUpdated = block.timestamp; + return baseVirtualPrice; + } else { + return metaSwapStorage.baseVirtualPrice; + } + } +} diff --git a/contracts/xchainGauges/GaugeTokenHolder.sol b/contracts/xchainGauges/GaugeTokenHolder.sol new file mode 100644 index 00000000..19fe4ac3 --- /dev/null +++ b/contracts/xchainGauges/GaugeTokenHolder.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.17; + +import "@openzeppelin/contracts-4.7.3/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable-4.7.3/proxy/utils/Initializable.sol"; + +import "../interfaces/IGauge.sol"; +import "../interfaces/IMinterLike.sol"; + +/// @title GaugeTokenHolder contract +/// @notice GaugeTokenHolder contract allows the itself to claims tokens +/// from the associated Minter contract. Optionally allows forwarding the +/// reward token to another contract. +abstract contract GaugeTokenHolder is Initializable { + using SafeERC20 for IERC20; + address public rewardReceiver; + address public gaugeToken; + address public rewardToken; + address public minter; + + /// @notice Emitted when the reward token is claimed + /// @param amount Amount of reward token claimed + event Claimed(uint256 amount); + + /// @notice Emitted when the reward token is forwarded + /// @param dest Destination address + /// @param amount Amount of reward token forwarded + event Forwarded(address dest, uint256 amount); + + /// @notice Emitted when the reward receiver is changed + /// @param oldRewardReceiver Old reward receiver address + /// @param newRewardReceiver New reward receiver address + event RewardReceiverUpdated( + address oldRewardReceiver, + address newRewardReceiver + ); + + /// @notice Initialize the GaugeTokenHolder contract. Assumes the gauge + // has SDL() and FACTORY() methods. + /// @param gauge Gauge token address + function __GaugeTokenHolder_init(address gauge) internal onlyInitializing { + require(gauge != address(0), "gauge address cannot be 0"); + require(gaugeToken == address(0), "already initialized"); + gaugeToken = gauge; + rewardToken = IGauge(gauge).SDL(); + minter = IGauge(gauge).FACTORY(); + } + + /// @notice Claim the reward token from the associated Minter contract. + /// If a rewardReceiver is set, the reward token is forwarded to the + /// rewardReceiver. Otherwise, the reward token is kept. + function _claim() internal { + address _gaugeToken = gaugeToken; + require(_gaugeToken != address(0), "gaugeToken address not set"); + + address _rewardToken = rewardToken; + address _minter = minter; + + // Claim SDL. Amount is based on balance of the gauge token held by this + IMinterLike(_minter).mint(gaugeToken); + uint256 amount = IERC20(_rewardToken).balanceOf(address(this)); + emit Claimed(amount); + + // If a rewardReceiver is set, forward the reward token to the rewardReceiver + address _rewardReceiver = rewardReceiver; + if (_rewardReceiver != address(0)) { + IERC20(_rewardToken).safeTransfer(_rewardReceiver, amount); + emit Forwarded(_rewardReceiver, amount); + } + } + + /// @notice Claim the reward token from the associated Minter contract. + /// If a rewardReceiver is set, the reward token is forwarded to the + /// rewardReceiver. Otherwise, the reward token is kept. + function claimGaugeRewards() public virtual { + _claim(); + } + + /// @notice Set the reward receiver address. Also set the reward receiver + /// for the gauge's third party rewards. + /// @param _rewardReceiver Address of the reward receiver + function _setRewardReceiver(address _rewardReceiver) internal { + address oldRewardReceiver = rewardReceiver; + rewardReceiver = _rewardReceiver; + emit RewardReceiverUpdated(oldRewardReceiver, _rewardReceiver); + + // Set reward receiver for the gauge's third party rewards as well + IGauge(gaugeToken).set_rewards_receiver(_rewardReceiver); + } +}