diff --git a/contracts/interfaces/IMigration.sol b/contracts/interfaces/IMigration.sol index 123aad1b..b703f942 100644 --- a/contracts/interfaces/IMigration.sol +++ b/contracts/interfaces/IMigration.sol @@ -4,6 +4,10 @@ pragma solidity 0.8.9; /// @title Generic migration vault interface /// @author Carter Carlson (@cartercarlson) interface IMigration { + /// @notice Method returns true is the migration has started + /// @param meToken Address of meToken + function isStarted(address meToken) external view returns (bool); + /// @notice Method to trigger actions from the migration vault if needed /// @param meToken Address of meToken function poke(address meToken) external; diff --git a/contracts/libs/LibFoundry.sol b/contracts/libs/LibFoundry.sol index f5d0af5b..f9ab70a3 100644 --- a/contracts/libs/LibFoundry.sol +++ b/contracts/libs/LibFoundry.sol @@ -53,20 +53,6 @@ library LibFoundry { address sender = LibMeta.msgSender(); MeTokenInfo memory meTokenInfo = s.meTokens[meToken]; HubInfo memory hubInfo = s.hubs[meTokenInfo.hubId]; - - // Handling changes - if (hubInfo.updating && block.timestamp > hubInfo.endTime) { - LibHub.finishUpdate(meTokenInfo.hubId); - } else if (meTokenInfo.targetHubId != 0) { - if (block.timestamp > meTokenInfo.endTime) { - hubInfo = s.hubs[meTokenInfo.targetHubId]; - meTokenInfo = LibMeToken.finishResubscribe(meToken); - } else if (block.timestamp > meTokenInfo.startTime) { - // Handle migration actions if needed - IMigration(meTokenInfo.migration).poke(meToken); - meTokenInfo = s.meTokens[meToken]; - } - } uint256[3] memory amounts; amounts[1] = (assetsDeposited * s.mintFee) / s.PRECISION; // fee amounts[2] = assetsDeposited - amounts[1]; //assetsDepositedAfterFees @@ -74,17 +60,35 @@ library LibFoundry { amounts[0] = _calculateMeTokensMinted(meToken, amounts[2]); // meTokensMinted IVault vault = IVault(hubInfo.vault); address asset = hubInfo.asset; + // Check if meToken is using a migration vault and in the active stage of resubscribing. // Sometimes a meToken may be resubscribing to a hub w/ the same asset, // in which case a migration vault isn't needed if ( meTokenInfo.migration != address(0) && - block.timestamp > meTokenInfo.startTime + block.timestamp > meTokenInfo.startTime && + IMigration(meTokenInfo.migration).isStarted(meToken) ) { // Use meToken address to get the asset address from the migration vault vault = IVault(meTokenInfo.migration); asset = s.hubs[meTokenInfo.targetHubId].asset; } + vault.handleDeposit(sender, asset, assetsDeposited, amounts[1]); + LibMeToken.updateBalancePooled(true, meToken, amounts[2]); + + // Handling changes + if (hubInfo.updating && block.timestamp > hubInfo.endTime) { + LibHub.finishUpdate(meTokenInfo.hubId); + } else if (meTokenInfo.targetHubId != 0) { + if (block.timestamp > meTokenInfo.endTime) { + //hubInfo = s.hubs[meTokenInfo.targetHubId]; + LibMeToken.finishResubscribe(meToken); + } else if (block.timestamp > meTokenInfo.startTime) { + // Handle migration actions if needed + IMigration(meTokenInfo.migration).poke(meToken); + } + } + return (vault, asset, sender, amounts); } @@ -94,14 +98,12 @@ library LibFoundry { address recipient ) internal { ( - IVault vault, + , address asset, address sender, uint256[3] memory amounts // 0-meTokensMinted 1-fee 2-assetsDepositedAfterFees ) = handleMint(meToken, assetsDeposited); - vault.handleDeposit(sender, asset, assetsDeposited, amounts[1]); - LibMeToken.updateBalancePooled(true, meToken, amounts[2]); // Mint meToken to user IMeToken(meToken).mint(recipient, amounts[0]); emit Mint( @@ -119,28 +121,24 @@ library LibFoundry { uint256 assetsDeposited, address recipient, uint256 deadline, - uint8 v, - bytes32 r, - bytes32 s + uint8 vSig, + bytes32 rSig, + bytes32 sSig ) internal { ( - IVault vault, address asset, address sender, - uint256[3] memory amounts // 0-meTokensMinted 1-fee 2-assetsDepositedAfterFees - ) = handleMint(meToken, assetsDeposited); - vault.handleDepositWithPermit( - sender, - asset, - assetsDeposited, - amounts[1], - deadline, - v, - r, - s - ); + uint256[2] memory amounts // 0-meTokensMinted 1-assetsDepositedAfterFees + ) = _handleMintWithPermit( + meToken, + assetsDeposited, + deadline, + vSig, + rSig, + sSig + ); - LibMeToken.updateBalancePooled(true, meToken, amounts[2]); + LibMeToken.updateBalancePooled(true, meToken, amounts[1]); // Mint meToken to user IMeToken(meToken).mint(recipient, amounts[0]); emit Mint( @@ -166,15 +164,12 @@ library LibFoundry { // Handling changes if (hubInfo.updating && block.timestamp > hubInfo.endTime) { LibHub.finishUpdate(meTokenInfo.hubId); - } else if (meTokenInfo.targetHubId != 0) { - if (block.timestamp > meTokenInfo.endTime) { - hubInfo = s.hubs[meTokenInfo.targetHubId]; - meTokenInfo = LibMeToken.finishResubscribe(meToken); - } else if (block.timestamp > meTokenInfo.startTime) { - // Handle migration actions if needed - IMigration(meTokenInfo.migration).poke(meToken); - meTokenInfo = s.meTokens[meToken]; - } + } else if ( + meTokenInfo.targetHubId != 0 && + block.timestamp > meTokenInfo.endTime + ) { + hubInfo = s.hubs[meTokenInfo.targetHubId]; + meTokenInfo = LibMeToken.finishResubscribe(meToken); } // Calculate how many tokens are returned uint256 rawAssetsReturned = _calculateRawAssetsReturned( @@ -218,6 +213,7 @@ library LibFoundry { rawAssetsReturned - assetsReturned ); } + _vaultWithdrawal( sender, recipient, @@ -228,29 +224,97 @@ library LibFoundry { assetsReturned, feeRate ); - /* uint256 fee = (assetsReturned * feeRate) / s.PRECISION; - assetsReturned = assetsReturned - fee; + } + + function _handleMintWithPermit( + address meToken, + uint256 assetsDeposited, + uint256 deadline, + uint8 vSig, + bytes32 rSig, + bytes32 sSig + ) + private + returns ( + address asset, + address sender, + uint256[2] memory amounts + ) + { + AppStorage storage s = LibAppStorage.diamondStorage(); + // 0-meTokensMinted 1-fee 2-assetsDepositedAfterFees + + MeTokenInfo memory meTokenInfo = s.meTokens[meToken]; + HubInfo memory hubInfo = s.hubs[meTokenInfo.hubId]; + + // uint256[2] memory amounts; + // amounts[1] = (assetsDeposited * s.mintFee) / s.PRECISION; // fee + amounts[1] = + assetsDeposited - + ((assetsDeposited * s.mintFee) / s.PRECISION); //assetsDepositedAfterFees + + amounts[0] = _calculateMeTokensMinted(meToken, amounts[1]); // meTokensMinted + + asset = _handlingChangesWithPermit( + amounts[1], + meToken, + meTokenInfo, + hubInfo, + assetsDeposited, + deadline, + vSig, + rSig, + sSig + ); + return (asset, sender, amounts); + } + + function _handlingChangesWithPermit( + uint256 assetsDepositedAfterFees, + address meToken, + MeTokenInfo memory meTokenInfo, + HubInfo memory hubInfo, + uint256 assetsDeposited, + uint256 deadline, + uint8 vSig, + bytes32 rSig, + bytes32 sSig + ) private returns (address asset) { + AppStorage storage s = LibAppStorage.diamondStorage(); + address sender = LibMeta.msgSender(); IVault vault = IVault(hubInfo.vault); - address asset = hubInfo.asset; + asset = hubInfo.asset; if ( meTokenInfo.migration != address(0) && - block.timestamp > meTokenInfo.startTime + block.timestamp > meTokenInfo.startTime && + IMigration(meTokenInfo.migration).isStarted(meToken) ) { + // Use meToken address to get the asset address from the migration vault vault = IVault(meTokenInfo.migration); asset = s.hubs[meTokenInfo.targetHubId].asset; } - - vault.handleWithdrawal(recipient, asset, assetsReturned, fee); - - emit Burn( - meToken, - asset, + vault.handleDepositWithPermit( sender, - recipient, - meTokensBurned, - assetsReturned - ); */ + asset, + assetsDeposited, + (assetsDeposited * s.mintFee) / s.PRECISION, + deadline, + vSig, + rSig, + sSig + ); + LibMeToken.updateBalancePooled(true, meToken, assetsDepositedAfterFees); + if (hubInfo.updating && block.timestamp > hubInfo.endTime) { + LibHub.finishUpdate(meTokenInfo.hubId); + } else if (meTokenInfo.targetHubId != 0) { + if (block.timestamp > meTokenInfo.endTime) { + LibMeToken.finishResubscribe(meToken); + } else if (block.timestamp > meTokenInfo.startTime) { + // Handle migration actions if needed + IMigration(meTokenInfo.migration).poke(meToken); + } + } } function _vaultWithdrawal( diff --git a/contracts/migrations/SameAssetTransferMigration.sol b/contracts/migrations/SameAssetTransferMigration.sol index 01c2c82f..c29b498a 100644 --- a/contracts/migrations/SameAssetTransferMigration.sol +++ b/contracts/migrations/SameAssetTransferMigration.sol @@ -102,6 +102,11 @@ contract SameAssetTransferMigration is ReentrancyGuard, Vault, IMigration { delete _sameAssetMigration[meToken]; } + /// @inheritdoc IMigration + function isStarted(address meToken) external view override returns (bool) { + return _sameAssetMigration[meToken].started; + } + function getDetails(address meToken) external view diff --git a/contracts/migrations/UniswapSingleTransferMigration.sol b/contracts/migrations/UniswapSingleTransferMigration.sol index cf9bf365..50ac92e6 100644 --- a/contracts/migrations/UniswapSingleTransferMigration.sol +++ b/contracts/migrations/UniswapSingleTransferMigration.sol @@ -8,7 +8,7 @@ import {IHubFacet} from "../interfaces/IHubFacet.sol"; import {IMeTokenRegistryFacet} from "../interfaces/IMeTokenRegistryFacet.sol"; import {IMigration} from "../interfaces/IMigration.sol"; import {ISingleAssetVault} from "../interfaces/ISingleAssetVault.sol"; -import {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import {IV3SwapRouter} from "@uniswap/swap-router-contracts/contracts/interfaces/IV3SwapRouter.sol"; import {HubInfo} from "../libs/LibHub.sol"; import {MeTokenInfo} from "../libs/LibMeToken.sol"; import {Vault} from "../vaults/Vault.sol"; @@ -33,8 +33,8 @@ contract UniswapSingleTransferMigration is ReentrancyGuard, Vault, IMigration { // NOTE: this can be found at // github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/interfaces/ISwapRouter.sol - ISwapRouter private immutable _router = - ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + IV3SwapRouter private immutable _router = + IV3SwapRouter(0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45); // args for uniswap router uint24 public constant MINFEE = 500; // 0.05% @@ -56,7 +56,6 @@ contract UniswapSingleTransferMigration is ReentrancyGuard, Vault, IMigration { { MeTokenInfo memory meTokenInfo = IMeTokenRegistryFacet(diamond) .getMeTokenInfo(meToken); - require( IHubFacet(diamond).getHubInfo(meTokenInfo.hubId).asset != IHubFacet(diamond).getHubInfo(meTokenInfo.targetHubId).asset, @@ -100,7 +99,6 @@ contract UniswapSingleTransferMigration is ReentrancyGuard, Vault, IMigration { HubInfo memory targetHubInfo = IHubFacet(diamond).getHubInfo( meTokenInfo.targetHubId ); - uint256 amountOut; if (!_uniswapSingleTransfers[meToken].started) { ISingleAssetVault( @@ -122,6 +120,11 @@ contract UniswapSingleTransferMigration is ReentrancyGuard, Vault, IMigration { delete _uniswapSingleTransfers[meToken]; } + /// @inheritdoc IMigration + function isStarted(address meToken) external view override returns (bool) { + return _uniswapSingleTransfers[meToken].started; + } + function getDetails(address meToken) external view @@ -191,13 +194,12 @@ contract UniswapSingleTransferMigration is ReentrancyGuard, Vault, IMigration { IERC20(hubInfo.asset).safeApprove(address(_router), amountIn); // https://docs.uniswap.org/protocol/guides/swaps/single-swaps - ISwapRouter.ExactInputSingleParams memory params = ISwapRouter + IV3SwapRouter.ExactInputSingleParams memory params = IV3SwapRouter .ExactInputSingleParams({ tokenIn: hubInfo.asset, tokenOut: targetHubInfo.asset, fee: usts.fee, recipient: address(this), - deadline: block.timestamp, amountIn: amountIn, amountOutMinimum: 0, sqrtPriceLimitX96: 0 diff --git a/hardhat.config.ts b/hardhat.config.ts index d510084c..ca145d3a 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -82,9 +82,12 @@ const config: HardhatUserConfig = { }, mocha: { delay: true, - timeout: 60000, // Here is 2min but can be whatever timeout is suitable for you. + timeout: 120000, // Here is 2min but can be whatever timeout is suitable for you. }, namedAccounts: { + SwapRouter: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", + Quoter: "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", + UNIV3Factory: "0x1F98431c8aD98523631AE4a59f267346ea31F984", DAI: { default: "0x6B175474E89094C44Da98b954EedeAC495271d0F", // here this will by default take the first account as deployer 1: "0x6B175474E89094C44Da98b954EedeAC495271d0F", // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another @@ -108,7 +111,7 @@ const config: HardhatUserConfig = { forking: { url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_API_KEY}`, accounts, - blockNumber: 13310410, + blockNumber: 14448329, }, gas: "auto", timeout: 1800000, diff --git a/package.json b/package.json index 70d4d94c..96cb1f12 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@openzeppelin/contracts": "^4.5.0", "@uniswap/sdk-core": "^3.0.1", "@uniswap/v3-core": "^1.0.0", + "@uniswap/smart-order-router": "^2.5.22", "@uniswap/v3-periphery": "^1.4.1", "@uniswap/v3-sdk": "^3.8.1", "yarn": "^1.22.18" diff --git a/test/contracts/curves/helper/curvesTestsHelper.ts b/test/contracts/curves/helper/curvesTestsHelper.ts index b8b80903..92b85430 100644 --- a/test/contracts/curves/helper/curvesTestsHelper.ts +++ b/test/contracts/curves/helper/curvesTestsHelper.ts @@ -166,6 +166,48 @@ export const curvesTestsHelper = async ({ ); expect(toETHNumber(estimate)).to.be.approximately(calculatedRes, precision); }); + it("viewMeTokensMinted should be linked to balance pooled", async () => { + const amountNum = 2; + let amount = one.mul(amountNum); + + // we need to have the right balancedPooled for supply + let balancedPooledNum = 1000; + let balancedPooled = one.mul(balancedPooledNum); + let supplyOne = await curve.viewMeTokensMinted(balancedPooled, hubId, 0, 0); + + // we need to have the right balancedPooled for supply + balancedPooledNum = 10000000; + balancedPooled = one.mul(balancedPooledNum); + let supplyTwo = await curve.viewMeTokensMinted(balancedPooled, hubId, 0, 0); + expect(supplyOne).to.not.equal(supplyTwo); + }); + it("viewAssetsReturned() should be linked to balance pooled", async () => { + // we need to have the right balancedPooled for supply + let amount = 2; + let balancedPooledNum = 600000; + let balancedPooled = one.mul(balancedPooledNum); + let supply = await curve.viewMeTokensMinted(balancedPooled, hubId, 0, 0); + + let estimateOne = await curve.viewAssetsReturned( + amount, + hubId, + supply, + balancedPooled + ); + // amount = 9999999999; + balancedPooledNum = 6; + balancedPooled = one.mul(balancedPooledNum); + // supply = await curve.viewMeTokensMinted(balancedPooled, hubId, 0, 0); + + let estimateTwo = await curve.viewAssetsReturned( + amount, + hubId, + supply, + balancedPooled + ); + expect(estimateOne).to.not.equal(estimateTwo); + }); + it("viewAssetsReturned() from non-zero supply should work", async () => { // we need to have the right balancedPooled for supply let balancedPooledNum = 600000; diff --git a/test/contracts/migrations/UniswapSingleTransfer.ts b/test/contracts/migrations/UniswapSingleTransfer.ts index 4be98448..2813b3b5 100644 --- a/test/contracts/migrations/UniswapSingleTransfer.ts +++ b/test/contracts/migrations/UniswapSingleTransfer.ts @@ -2,7 +2,7 @@ import { expect } from "chai"; import { ethers, getNamedAccounts } from "hardhat"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { Signer, BigNumber } from "ethers"; -import { deploy, getContractAt } from "../../utils/helpers"; +import { deploy, getContractAt, toETHNumber } from "../../utils/helpers"; import { impersonate, mineBlock } from "../../utils/hardhatNode"; import { hubSetup } from "../../utils/hubSetup"; import { @@ -16,6 +16,7 @@ import { SingleAssetVault, UniswapSingleTransferMigration, } from "../../../artifacts/types"; +import { getQuote } from "../../utils/uniswap"; const setup = async () => { describe("UniswapSingleTransferMigration.sol", () => { @@ -25,6 +26,7 @@ const setup = async () => { let WETHWhale: string; let daiHolder: Signer; let wethHolder: Signer; + let UNIV3Factory: string; let dai: ERC20; let weth: ERC20; let account0: SignerWithAddress; @@ -67,7 +69,8 @@ const setup = async () => { }; before(async () => { - ({ DAI, DAIWhale, WETH, WETHWhale } = await getNamedAccounts()); + ({ DAI, DAIWhale, WETH, WETHWhale, UNIV3Factory } = + await getNamedAccounts()); encodedVaultDAIArgs = ethers.utils.defaultAbiCoder.encode( ["address"], @@ -513,14 +516,23 @@ const setup = async () => { ); const migrationDAIBefore = await dai.balanceOf(migration.address); const migrationWETHBefore = await weth.balanceOf(migration.address); - + expect(migrationWETHBefore).to.be.equal(0); + const DAIBalanceBefore = await dai.balanceOf(account2.address); + const price = await getQuote( + UNIV3Factory, + dai, + weth, + migrationDetails.fee, + amount.add(meTokenInfo.balanceLocked).add(meTokenInfo.balancePooled) + ); const tx = await foundry .connect(account2) .mint(meToken.address, amount, account2.address); await tx.wait(); await expect(tx).to.be.emit(dai, "Transfer"); - + const DAIBalanceAfter = await dai.balanceOf(account2.address); + expect(DAIBalanceBefore.sub(DAIBalanceAfter)).to.equal(amount); const initialVaultDAIAfter = await dai.balanceOf( initialVault.address ); @@ -529,16 +541,20 @@ const setup = async () => { ); const migrationDAIAfter = await dai.balanceOf(migration.address); const migrationWETHAfter = await weth.balanceOf(migration.address); - + // initial vault weth balance has no change expect(initialVaultWETHBefore.sub(initialVaultWETHAfter)).to.be.equal( 0 - ); // initial vault weth balance has no change + ); + // amount deposited before start time expect(initialVaultDAIBefore.sub(initialVaultDAIAfter)).to.equal( amount - ); // amount deposited before start time + ); expect(migrationDAIAfter.sub(migrationDAIBefore)).to.be.equal(0); // no change - // TODO fix with swap balance - expect(migrationWETHAfter.sub(migrationWETHBefore)).to.be.gt(amount); // gt due to swap amount + // dai to eth swap amount + expect(toETHNumber(migrationWETHAfter)).to.be.approximately( + Number(price.token0Price), + 0.01 + ); }); it("After endTime: assets transferred to/from target vault", async () => { const meTokenInfo = await meTokenRegistry.getMeTokenInfo( diff --git a/test/contracts/vaults/VaultYearn.ts b/test/contracts/vaults/VaultYearn.ts index fe651f05..503c3a5d 100644 --- a/test/contracts/vaults/VaultYearn.ts +++ b/test/contracts/vaults/VaultYearn.ts @@ -54,9 +54,6 @@ describe("Vault with Yearn Token", () => { const tokenDeposited = ethers.utils.parseEther( tokenDepositedInETH.toString() ); - /* after(async () => { - await resetFork(13310410); - }); */ before(async () => { ({ DAI, DAIWhale, YDAI, YDAIWhale } = await getNamedAccounts()); diff --git a/test/integration/MeTokenRegistry/ResubscribeCurve.ts b/test/integration/MeTokenRegistry/ResubscribeCurve.ts index cb37d10e..13ed4d50 100644 --- a/test/integration/MeTokenRegistry/ResubscribeCurve.ts +++ b/test/integration/MeTokenRegistry/ResubscribeCurve.ts @@ -23,20 +23,22 @@ import { ERC20, MigrationRegistry, SingleAssetVault, - UniswapSingleTransferMigration, + SameAssetTransferMigration, } from "../../../artifacts/types"; +import { getQuote } from "../../utils/uniswap"; const setup = async () => { describe("MeToken Resubscribe - new curve", () => { let meTokenRegistry: MeTokenRegistryFacet; let migrationRegistry: MigrationRegistry; - let migration: UniswapSingleTransferMigration; + let migration: SameAssetTransferMigration; let singleAssetVault: SingleAssetVault; let foundry: FoundryFacet; let hub: HubFacet; let whale: Signer; let dai: ERC20; let weth: ERC20; + let UNIV3Factory: string; let daiWhale: Signer; let meToken: MeToken; let account0: SignerWithAddress; @@ -67,7 +69,7 @@ const setup = async () => { before(async () => { let token: ERC20; let DAI, WETH; - ({ DAI, WETH } = await getNamedAccounts()); + ({ DAI, WETH, UNIV3Factory } = await getNamedAccounts()); const encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( ["address"], @@ -99,7 +101,7 @@ const setup = async () => { await hub.register( account0.address, - WETH, + DAI, singleAssetVault.address, refundRatio, newBaseY, @@ -116,8 +118,8 @@ const setup = async () => { await fees.setBurnBuyerFee(burnBuyerFee); // Deploy uniswap migration and approve it to the registry - migration = await deploy( - "UniswapSingleTransferMigration", + migration = await deploy( + "SameAssetTransferMigration", undefined, account0.address, // DAO hub.address // diamond @@ -162,11 +164,7 @@ const setup = async () => { await dai.connect(account0).approve(singleAssetVault.address, max); await dai.connect(account1).approve(singleAssetVault.address, max); await dai.connect(account0).approve(migration.address, max); - await dai.connect(account0).approve(migration.address, max); - await weth.connect(account0).approve(migration.address, max); - await weth.connect(account1).approve(migration.address, max); - await weth.connect(account0).approve(singleAssetVault.address, max); - await weth.connect(account1).approve(singleAssetVault.address, max); + await dai.connect(account1).approve(migration.address, max); }); describe("Warmup", () => { @@ -318,7 +316,7 @@ const setup = async () => { }); it("mint() [buyer]: meTokens received based on weighted average of Curves", async () => { const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); - const migrationWETHBefore = await weth.balanceOf(migration.address); + const migrationDaiBefore = await dai.balanceOf(migration.address); const meTokenTotalSupplyBefore = await meToken.totalSupply(); expect(meTokenTotalSupplyBefore).to.be.equal(0); const meTokenInfo = await meTokenRegistry.getMeTokenInfo( @@ -356,7 +354,7 @@ const setup = async () => { const ownerMeTokenAfter = await meToken.balanceOf(account0.address); const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); - const migrationWETHAfter = await weth.balanceOf(migration.address); + const migrationDaiAfter = await dai.balanceOf(migration.address); const meTokenTotalSupplyAfter = await meToken.totalSupply(); expect(toETHNumber(ownerMeTokenAfter)).to.be.approximately( @@ -365,9 +363,9 @@ const setup = async () => { ); expect(meTokenTotalSupplyAfter).to.be.equal(ownerMeTokenAfter); expect(vaultDAIAfter.sub(vaultDAIBefore)).to.equal(0); // new asset goes to migration - expect(migrationWETHAfter.sub(migrationWETHBefore)).to.equal( + expect(migrationDaiAfter.sub(migrationDaiBefore)).to.equal( tokenDeposited - ); // new asset is WETH + ); }); it("burn() [buyer]: assets received based on weighted average of Curves", async () => { const ownerMeToken = await meToken.balanceOf(account0.address); @@ -396,7 +394,7 @@ const setup = async () => { toETHNumber(meTokenInfo.balancePooled), newReserveWeight / MAX_WEIGHT ); - + const buyerDAIBefore = await dai.balanceOf(account1.address); await foundry .connect(account1) .burn(meToken.address, buyerMeToken, account1.address); @@ -416,6 +414,7 @@ const setup = async () => { const buyerMeTokenAfter = await meToken.balanceOf(account1.address); const buyerWETHAfter = await weth.balanceOf(account1.address); + const buyerDAIAfter = await dai.balanceOf(account1.address); const migrationWETHAfter = await weth.balanceOf(migration.address); const meTokenTotalSupplyAfter = await meToken.totalSupply(); @@ -426,31 +425,28 @@ const setup = async () => { ); expect( - toETHNumber(buyerWETHAfter.sub(buyerWETHBefore)) + toETHNumber(buyerDAIAfter.sub(buyerDAIBefore)) ).to.be.approximately( new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), 1e-15 ); + expect(buyerWETHAfter.sub(buyerWETHBefore)).to.equal(0); expect(buyerMeTokenAfter).to.equal(0); expect(toETHNumber(meTokenTotalSupplyAfter)).to.be.approximately( toETHNumber(meTokenTotalSupply.div(2)), 1e-18 ); - expect( - toETHNumber(migrationWETHBefore.sub(migrationWETHAfter)) - ).to.approximately( - new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), - 1e-15 - ); + expect(migrationWETHBefore).to.equal(0); + expect(migrationWETHAfter).to.equal(0); }); it("burn() [owner]: assets received based on weighted average of Curves", async () => { const ownerMeToken = await meToken.balanceOf(account0.address); - const migrationWETHBefore = await weth.balanceOf(migration.address); + const migrationBefore = await dai.balanceOf(migration.address); const meTokenTotalSupply = await meToken.totalSupply(); const meTokenInfo = await meTokenRegistry.getMeTokenInfo( meToken.address ); - const ownerWETHBefore = await weth.balanceOf(account0.address); + const ownerBefore = await dai.balanceOf(account0.address); await setAutomine(false); const block = await ethers.provider.getBlock("latest"); @@ -487,8 +483,8 @@ const setup = async () => { await mineBlock(block.timestamp + 1); await setAutomine(true); const ownerMeTokenAfter = await meToken.balanceOf(account0.address); - const ownerWETHAfter = await weth.balanceOf(account0.address); - const migrationWETHAfter = await weth.balanceOf(migration.address); + const ownerAfter = await dai.balanceOf(account0.address); + const migrationAfter = await dai.balanceOf(migration.address); const meTokenTotalSupplyAfter = await meToken.totalSupply(); const burnFee = toETHNumber( @@ -497,12 +493,10 @@ const setup = async () => { .div(PRECISION) ); - expect(migrationWETHBefore.sub(migrationWETHAfter)).to.equal( - ownerWETHAfter.sub(ownerWETHBefore) + expect(migrationBefore.sub(migrationAfter)).to.equal( + ownerAfter.sub(ownerBefore) ); - expect( - toETHNumber(ownerWETHAfter.sub(ownerWETHBefore)) - ).to.be.approximately( + expect(toETHNumber(ownerAfter.sub(ownerBefore))).to.be.approximately( new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), 1e-15 ); @@ -522,8 +516,8 @@ const setup = async () => { expect(meTokenInfo.endTime).to.be.lt(block.timestamp); }); it("mint() [buyer]: assets received based on target Curve", async () => { - const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); - const migrationWETHBefore = await weth.balanceOf(migration.address); + const vaultBefore = await dai.balanceOf(singleAssetVault.address); + const migrationBefore = await dai.balanceOf(migration.address); const meTokenTotalSupplyBefore = await meToken.totalSupply(); expect(meTokenTotalSupplyBefore).to.be.equal(0); @@ -542,8 +536,8 @@ const setup = async () => { .mint(meToken.address, tokenDeposited, account0.address); const ownerMeTokenAfter = await meToken.balanceOf(account0.address); - const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); - const migrationWETHAfter = await weth.balanceOf(migration.address); + const vaultAfter = await dai.balanceOf(singleAssetVault.address); + const migrationAfter = await dai.balanceOf(migration.address); const meTokenTotalSupplyAfter = await meToken.totalSupply(); expect(toETHNumber(ownerMeTokenAfter)).to.be.approximately( @@ -551,8 +545,8 @@ const setup = async () => { 1e-15 ); expect(meTokenTotalSupplyAfter).to.be.equal(ownerMeTokenAfter); - expect(vaultWETHAfter.sub(vaultWETHBefore)).to.equal(tokenDeposited); - expect(migrationWETHAfter.sub(migrationWETHBefore)).to.equal(0); + expect(vaultAfter.sub(vaultBefore)).to.equal(tokenDeposited); + expect(migrationAfter.sub(migrationBefore)).to.equal(0); }); it("burn() [buyer]: assets received based on target Curve", async () => { const ownerMeToken = await meToken.balanceOf(account0.address); @@ -560,9 +554,9 @@ const setup = async () => { const buyerMeToken = await meToken.balanceOf(account1.address); expect(buyerMeToken).to.be.equal(ownerMeToken.div(2)); - const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); + const vaultBefore = await dai.balanceOf(singleAssetVault.address); const meTokenTotalSupply = await meToken.totalSupply(); - const buyerWETHBefore = await weth.balanceOf(account1.address); + const buyerBefore = await dai.balanceOf(account1.address); const meTokenInfo = await meTokenRegistry.getMeTokenInfo( meToken.address ); @@ -582,8 +576,8 @@ const setup = async () => { (targetAssetsReturned * refundRatio) / MAX_WEIGHT; const buyerMeTokenAfter = await meToken.balanceOf(account1.address); - const buyerWETHAfter = await weth.balanceOf(account1.address); - const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); + const buyerAfter = await dai.balanceOf(account1.address); + const vaultAfter = await dai.balanceOf(singleAssetVault.address); const meTokenTotalSupplyAfter = await meToken.totalSupply(); const burnFee = toETHNumber( @@ -592,9 +586,7 @@ const setup = async () => { .div(PRECISION) ); - expect( - toETHNumber(buyerWETHAfter.sub(buyerWETHBefore)) - ).to.be.approximately( + expect(toETHNumber(buyerAfter.sub(buyerBefore))).to.be.approximately( new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), 1e-15 ); @@ -603,21 +595,19 @@ const setup = async () => { toETHNumber(meTokenTotalSupply.div(2)), 1e-18 ); - expect( - toETHNumber(vaultWETHBefore.sub(vaultWETHAfter)) - ).to.approximately( + expect(toETHNumber(vaultBefore.sub(vaultAfter))).to.approximately( new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), 1e-15 ); }); it("burn() [owner]: assets received based on target Curve", async () => { const ownerMeToken = await meToken.balanceOf(account0.address); - const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); + const vaultBefore = await dai.balanceOf(singleAssetVault.address); const meTokenTotalSupply = await meToken.totalSupply(); const meTokenInfo = await meTokenRegistry.getMeTokenInfo( meToken.address ); - const ownerWETHBefore = await weth.balanceOf(account0.address); + const ownerBefore = await dai.balanceOf(account0.address); const targetAssetsReturned = calculateCollateralReturned( toETHNumber(ownerMeToken), @@ -636,8 +626,8 @@ const setup = async () => { toETHNumber(meTokenInfo.balanceLocked); const ownerMeTokenAfter = await meToken.balanceOf(account0.address); - const ownerWETHAfter = await weth.balanceOf(account0.address); - const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); + const ownerAfter = await dai.balanceOf(account0.address); + const vaultAfter = await dai.balanceOf(singleAssetVault.address); const meTokenTotalSupplyAfter = await meToken.totalSupply(); const burnFee = toETHNumber( @@ -646,12 +636,10 @@ const setup = async () => { .div(PRECISION) ); - expect(vaultWETHBefore.sub(vaultWETHAfter)).to.equal( - ownerWETHAfter.sub(ownerWETHBefore) + expect(vaultBefore.sub(vaultAfter)).to.equal( + ownerAfter.sub(ownerBefore) ); - expect( - toETHNumber(ownerWETHAfter.sub(ownerWETHBefore)) - ).to.be.approximately( + expect(toETHNumber(ownerAfter.sub(ownerBefore))).to.be.approximately( new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), 1e-14 ); diff --git a/test/integration/MeTokenRegistry/ResubscribeCurveInfo.ts b/test/integration/MeTokenRegistry/ResubscribeCurveInfo.ts index 0a94fc26..4da589b3 100644 --- a/test/integration/MeTokenRegistry/ResubscribeCurveInfo.ts +++ b/test/integration/MeTokenRegistry/ResubscribeCurveInfo.ts @@ -25,6 +25,7 @@ import { SingleAssetVault, UniswapSingleTransferMigration, } from "../../../artifacts/types"; +import { getQuote } from "../../utils/uniswap"; const setup = async () => { describe("MeToken Resubscribe - Same curve, new Curve Details", () => { @@ -37,6 +38,7 @@ const setup = async () => { let whale: Signer; let dai: ERC20; let weth: ERC20; + let UNIV3Factory: string; let daiWhale: Signer; let meToken: MeToken; let account0: SignerWithAddress; @@ -67,7 +69,7 @@ const setup = async () => { before(async () => { let token: ERC20; let DAI, WETH; - ({ DAI, WETH } = await getNamedAccounts()); + ({ DAI, WETH, UNIV3Factory } = await getNamedAccounts()); const encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( ["address"], @@ -370,9 +372,22 @@ const setup = async () => { ); expect(meTokenTotalSupplyAfter).to.be.equal(ownerMeTokenAfter); expect(vaultDAIAfter.sub(vaultDAIBefore)).to.equal(0); // new asset goes to migration - expect(migrationWETHAfter.sub(migrationWETHBefore)).to.equal( + // new asset is WETH + const migrationDetails = await migration.getDetails(meToken.address); + const price = await getQuote( + UNIV3Factory, + dai, + weth, + migrationDetails.fee, tokenDeposited - ); // new asset is WETH + .add(meTokenInfo.balanceLocked) + .add(meTokenInfo.balancePooled) + ); + // dai to eth swap amount + expect(toETHNumber(migrationWETHAfter)).to.be.approximately( + Number(price.token0Price), + 0.01 + ); }); it("burn() [buyer]: assets received based on weighted average Curve info", async () => { const ownerMeToken = await meToken.balanceOf(account0.address); diff --git a/test/integration/MeTokenRegistry/ResubscribeMultipleChanges.ts b/test/integration/MeTokenRegistry/ResubscribeMultipleChanges.ts index a1ed1693..1808c758 100644 --- a/test/integration/MeTokenRegistry/ResubscribeMultipleChanges.ts +++ b/test/integration/MeTokenRegistry/ResubscribeMultipleChanges.ts @@ -25,6 +25,7 @@ import { SingleAssetVault, UniswapSingleTransferMigration, } from "../../../artifacts/types"; +import { getQuote } from "../../utils/uniswap"; // Differences: // 1. Curve details: baseY, reserveWeight - encodedStepwiseDetails @@ -47,6 +48,7 @@ const setup = async () => { let meToken: MeToken; let account0: SignerWithAddress; let account1: SignerWithAddress; + let UNIV3Factory: string; const hubId1 = 1; const hubId2 = 2; const hubWarmup = 7 * 60 * 24 * 24; // 1 week @@ -72,7 +74,7 @@ const setup = async () => { before(async () => { let token: ERC20; let DAI, WETH; - ({ DAI, WETH } = await getNamedAccounts()); + ({ DAI, WETH, UNIV3Factory } = await getNamedAccounts()); const encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( ["address"], @@ -378,9 +380,21 @@ const setup = async () => { ); expect(meTokenTotalSupplyAfter).to.be.equal(ownerMeTokenAfter); expect(vaultDAIAfter.sub(vaultDAIBefore)).to.equal(0); // new asset goes to migration - expect(migrationWETHAfter.sub(migrationWETHBefore)).to.equal( + const migrationDetails = await migration.getDetails(meToken.address); + const price = await getQuote( + UNIV3Factory, + dai, + weth, + migrationDetails.fee, tokenDeposited - ); // new asset is WETH + .add(meTokenInfo.balanceLocked) + .add(meTokenInfo.balancePooled) + ); + // dai to eth swap amount + expect(toETHNumber(migrationWETHAfter)).to.be.approximately( + Number(price.token0Price), + 0.01 + ); }); it("burn() [buyer]: assets received based on weighted average of Curves and and assets received apply weighted average refundRatio", async () => { const ownerMeToken = await meToken.balanceOf(account0.address); diff --git a/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts b/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts index f8110d4a..c172f474 100644 --- a/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts +++ b/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts @@ -6,6 +6,7 @@ import { deploy, getContractAt, weightedAverageSimulation, + toETHNumber, } from "../../utils/helpers"; import { hubSetup } from "../../utils/hubSetup"; import { mineBlock } from "../../utils/hardhatNode"; @@ -19,7 +20,7 @@ import { SingleAssetVault, UniswapSingleTransferMigration, } from "../../../artifacts/types"; - +import { getQuote } from "../../utils/uniswap"; const setup = async () => { describe("MeToken Resubscribe - new RefundRatio", () => { let meTokenRegistry: MeTokenRegistryFacet; @@ -41,11 +42,11 @@ const setup = async () => { const MAX_WEIGHT = 1000000; let encodedVaultArgs: string; const firstHubId = 1; - const initialRefundRatio = ethers.utils.parseUnits("5000", 0); // 0.005% - const targetRefundRatio = ethers.utils.parseUnits("500000", 0); // 50% + const initialRefundRatio = ethers.utils.parseUnits("4000", 0); // 0.004% + const targetRefundRatio = ethers.utils.parseUnits("400000", 0); // 40% const fees = 3000; - let tokenDepositedInETH; + let UNIV3Factory: string; let tokenDeposited: BigNumber; before(async () => { @@ -53,7 +54,7 @@ const setup = async () => { const reserveWeight = MAX_WEIGHT / 2; let DAI; let WETH; - ({ DAI, WETH } = await getNamedAccounts()); + ({ DAI, WETH, UNIV3Factory } = await getNamedAccounts()); encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( ["address"], @@ -93,11 +94,11 @@ const setup = async () => { // Pre-load owner and buyer w/ DAI await dai .connect(whale) - .transfer(account2.address, ethers.utils.parseEther("50")); + .transfer(account2.address, ethers.utils.parseEther("50000")); await weth .connect(whale) - .transfer(account2.address, ethers.utils.parseEther("50")); + .transfer(account2.address, ethers.utils.parseEther("500")); // Create meToken and subscribe to Hub1 await meTokenRegistry @@ -132,8 +133,9 @@ const setup = async () => { migration.address, encodedMigrationArgs ); - tokenDepositedInETH = 5; + const tokenDepositedInETH = 5; tokenDeposited = ethers.utils.parseEther(tokenDepositedInETH.toString()); + //tokenDeposited = ethers.utils.parseUnits("30.49711783897089858", "ether"); await dai .connect(account2) .approve(singleAssetVault.address, ethers.constants.MaxUint256); @@ -185,9 +187,10 @@ const setup = async () => { expect(meTokenInfo.balanceLocked).to.equal(0); }); it("burn() [buyer]: assets received based on initial refundRatio", async () => { + const collateralDeposited = tokenDeposited.mul(3); await foundry .connect(account2) - .mint(meToken.address, tokenDeposited, account1.address); + .mint(meToken.address, collateralDeposited, account1.address); const buyerMeTokenBefore = await meToken.balanceOf(account1.address); const buyerDAIBefore = await dai.balanceOf(account1.address); @@ -205,8 +208,9 @@ const setup = async () => { meToken.address ); - const refundAmount = tokenDeposited.mul(initialRefundRatio).div(1e6); - + const refundAmount = collateralDeposited + .mul(initialRefundRatio) + .div(1e6); expect(totalSupply).to.equal(0); expect(buyerMeTokenAfter).to.equal(0); expect(buyerDAIAfter.sub(buyerDAIBefore)).to.equal(refundAmount); @@ -230,6 +234,9 @@ const setup = async () => { const vaultDAIBeforeMint = await dai.balanceOf( singleAssetVault.address ); + const meTokenInfoBefore = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); const tx = await foundry .connect(account2) .mint(meToken.address, tokenDeposited, account0.address); @@ -248,9 +255,22 @@ const setup = async () => { expect(vaultDAIBeforeMint).to.be.gt(0); expect(vaultDAIBefore).to.be.equal(0); // as all is swapped for weth and goes to migration - // TODO check extra balance due to swap - expect(migrationWETHBefore).to.gt(tokenDeposited); // migration vault receives minted funds plus dai swap - + // migration vault receives minted funds plus dai swap + const migrationDetails = await migration.getDetails(meToken.address); + const price = await getQuote( + UNIV3Factory, + dai, + weth, + migrationDetails.fee, + tokenDeposited + .add(meTokenInfoBefore.balanceLocked) + .add(meTokenInfoBefore.balancePooled) + ); + // dai to eth swap amount + expect(toETHNumber(migrationWETHBefore)).to.be.approximately( + Number(price.token0Price), + 0.01 + ); await foundry .connect(account0) .burn(meToken.address, ownerMeTokenBefore, account0.address); @@ -273,10 +293,10 @@ const setup = async () => { expect(vaultDAIBefore).to.equal(vaultDAIAfter); // as vault do not receive any funds expect(vaultWETHBefore).to.equal(vaultWETHAfter); // as vault do not receive any funds expect(migrationDAIBefore).to.equal(migrationDAIAfter); // as migration receives new fund in weth - expect(migrationWETHAfter).to.equal(0); // as all funds are transferred to owner - expect(ownerWETHAfter.sub(ownerWETHBefore)).to.equal( - migrationWETHBefore - ); // as all token deposited goes to owner plus swap tokens + expect(migrationWETHAfter).to.be.lte(1); // as all funds are transferred to owner + expect( + ownerWETHAfter.sub(ownerWETHBefore).sub(migrationWETHBefore) + ).to.be.lte(1); // as all token deposited goes to owner plus swap tokens expect(meTokenInfo.balancePooled).to.equal(0); expect(meTokenInfo.balanceLocked).to.equal(0); }); @@ -295,7 +315,7 @@ const setup = async () => { const migrationDAIBefore = await dai.balanceOf(migration.address); const migrationWETHBefore = await weth.balanceOf(migration.address); - expect(migrationWETHBefore).to.equal(tokenDeposited); + expect(migrationWETHBefore.sub(1)).to.equal(tokenDeposited); await foundry .connect(account1) // non owner @@ -379,10 +399,10 @@ const setup = async () => { const migrationWETHBefore = await weth.balanceOf(migration.address); expect(migrationWETHBeforeMint).to.be.gt(0); // due to refund ration from last burn - expect(vaultWETHBefore).to.equal( + expect(vaultWETHBefore.add(1)).to.equal( tokenDeposited.add(migrationWETHBeforeMint) ); - expect(migrationWETHBefore).to.equal(0); // as all funds are transferred to vault + expect(migrationWETHBefore).to.be.lte(1); // as all funds are transferred to vault await foundry .connect(account0) diff --git a/test/integration/MeTokenRegistry/ResubscribeVault.ts b/test/integration/MeTokenRegistry/ResubscribeVault.ts new file mode 100644 index 00000000..c17bd9e2 --- /dev/null +++ b/test/integration/MeTokenRegistry/ResubscribeVault.ts @@ -0,0 +1,768 @@ +import { expect } from "chai"; +import Decimal from "decimal.js"; +import { BigNumber, Contract, Signer, providers } from "ethers"; +import { ethers, getNamedAccounts } from "hardhat"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { hubSetup } from "../../utils/hubSetup"; +import { + calculateCollateralReturned, + deploy, + getContractAt, + toETHNumber, + weightedAverageSimulation, + calculateTokenReturnedFromZero, + calculateStepwiseTokenReturned, + calculateStepwiseCollateralReturned, + fromETHNumber, +} from "../../utils/helpers"; +import { impersonate, mineBlock, setAutomine } from "../../utils/hardhatNode"; +import { + FoundryFacet, + HubFacet, + MeTokenRegistryFacet, + FeesFacet, + MeToken, + ERC20, + MigrationRegistry, + SingleAssetVault, + UniswapSingleTransferMigration, + VaultRegistry, + Diamond, +} from "../../../artifacts/types"; +import { getQuote } from "../../utils/uniswap"; + +const setup = async () => { + describe("MeToken Resubscribe - new vault", () => { + let meTokenRegistry: MeTokenRegistryFacet; + let migrationRegistry: MigrationRegistry; + let migration: UniswapSingleTransferMigration; + let initialVault: SingleAssetVault; // DAI + let targetVault: SingleAssetVault; // WETH + let foundry: FoundryFacet; + let hub: HubFacet; + let whale: Signer; + let dai: ERC20; + let weth: ERC20; + let daiWhale: Signer; + let meToken: MeToken; + let account0: SignerWithAddress; + let account1: SignerWithAddress; // meToken's owner + let account2: SignerWithAddress; + let encodedVaultDAIArgs: string; + let encodedVaultWETHArgs: string; + let fees: FeesFacet; + //let curve: ICurve; + let vaultRegistry: VaultRegistry; + let diamond: Diamond; + let UNIV3Factory: string; + const hubId1 = 1; + const hubId2 = 2; + const hubWarmup = 7 * 60 * 24 * 24; // 1 week + const warmup = 2 * 60 * 24 * 24; // 2 days + const duration = 4 * 60 * 24 * 24; // 4 days + const coolDown = 5 * 60 * 24 * 24; // 5 days + const MAX_WEIGHT = 1000000; + const PRECISION = BigNumber.from(10).pow(18); + const baseY = PRECISION.div(1000); + const reserveWeight = MAX_WEIGHT / 2; + const refundRatio = 5000; + const fee = 3000; + let tokenDepositedInETH = 302; + let tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + const burnOwnerFee = 1e8; + const burnBuyerFee = 1e9; + + before(async () => { + let token: ERC20; + let DAI, WETH, DAIWhale; + ({ DAI, WETH, DAIWhale, UNIV3Factory } = await getNamedAccounts()); + dai = await getContractAt("ERC20", DAI); + weth = await getContractAt("ERC20", WETH); + daiWhale = await impersonate(DAIWhale); + + encodedVaultDAIArgs = ethers.utils.defaultAbiCoder.encode( + ["address"], + [DAI] + ); + encodedVaultWETHArgs = ethers.utils.defaultAbiCoder.encode( + ["address"], + [WETH] + ); + + const encodedMigrationArgs = ethers.utils.defaultAbiCoder.encode( + ["uint24"], + [fee] + ); + + // Register first and second hub + ({ + token, + whale, + migrationRegistry, + singleAssetVault: initialVault, + account0, + account1, + account2, + meTokenRegistry, + vaultRegistry, + fee: fees, + foundry, + hub, + diamond, + } = await hubSetup( + baseY, + reserveWeight, + encodedVaultDAIArgs, + refundRatio + )); + + targetVault = await deploy( + "SingleAssetVault", + undefined, //no libs + account0.address, // DAO + hub.address // diamond + ); + await vaultRegistry.approve(targetVault.address); + + // Register 2nd hub to which we'll migrate to + await hub.register( + account0.address, + WETH, + targetVault.address, + refundRatio, + baseY, + reserveWeight, + encodedVaultWETHArgs + ); + // Deploy uniswap migration and approve it to the registry + migration = await deploy( + "UniswapSingleTransferMigration", + undefined, + account0.address, + hub.address // diamond + ); + await migrationRegistry.approve( + initialVault.address, + targetVault.address, + migration.address + ); + + // set update/resubscribe times + await hub.setHubWarmup(hubWarmup); + await meTokenRegistry.setMeTokenWarmup(warmup); + await meTokenRegistry.setMeTokenDuration(duration); + await meTokenRegistry.setMeTokenCooldown(coolDown); + await fees.setBurnOwnerFee(burnOwnerFee); + await fees.setBurnBuyerFee(burnBuyerFee); + + // Deploy uniswap migration and approve it to the registry + migration = await deploy( + "UniswapSingleTransferMigration", + undefined, + account0.address, // DAO + diamond.address // diamond + ); + await migrationRegistry.approve( + initialVault.address, + targetVault.address, + migration.address + ); + + // Pre-load owner account1 and buyer account2 w/ DAI & WETH + await dai + .connect(daiWhale) + .transfer(account1.address, ethers.utils.parseEther("4000")); + + await weth + .connect(whale) + .transfer(account1.address, ethers.utils.parseEther("20")); + dai + .connect(daiWhale) + .transfer(account0.address, ethers.utils.parseEther("4000")); + dai + .connect(daiWhale) + .transfer(account2.address, ethers.utils.parseEther("4000")); + weth + .connect(whale) + .transfer(account0.address, ethers.utils.parseEther("20")); + weth + .connect(whale) + .transfer(account2.address, ethers.utils.parseEther("1000")); + // Create meToken and subscribe to Hub1 + const name = "Carl meToken"; + const symbol = "CARL"; + // const amount = ethers.utils.parseEther("100"); + + const max = ethers.constants.MaxUint256; + await dai.connect(account1).approve(meTokenRegistry.address, max); + await dai.connect(account1).approve(initialVault.address, max); + await weth.connect(account1).approve(migration.address, max); + await dai.connect(account2).approve(initialVault.address, max); + + // await weth.connect(account2).approve(migration.address, max); + await weth.connect(account1).approve(targetVault.address, max); + // Create meToken + await meTokenRegistry + .connect(account1) + .subscribe(name, symbol, hubId1, 0); + const meTokenAddr = await meTokenRegistry.getOwnerMeToken( + account1.address + ); + meToken = await getContractAt("MeToken", meTokenAddr); + + await meTokenRegistry + .connect(account1) + .initResubscribe( + meToken.address, + hubId2, + migration.address, + encodedMigrationArgs + ); + const migrationDetails = await migration.getDetails(meToken.address); + expect(migrationDetails.fee).to.equal(fee); + expect(migrationDetails.started).to.equal(false); + }); + + describe("Warmup", () => { + before(async () => { + const meTokenInfo = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + const block = await ethers.provider.getBlock("latest"); + expect(meTokenInfo.startTime).to.be.gt(block.timestamp); + }); + it("mint() [buyer]: meTokens received based on initial vault", async () => { + const vaultDAIBefore = await dai.balanceOf(initialVault.address); + const meTokenTotalSupplyBefore = await meToken.totalSupply(); + expect(meTokenTotalSupplyBefore).to.be.equal(0); + + const calculatedReturn = calculateTokenReturnedFromZero( + tokenDepositedInETH, + toETHNumber(baseY), + reserveWeight / MAX_WEIGHT + ); + // buyer mint some meToken + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account2.address); + + const ownerMeTokenAfter = await meToken.balanceOf(account1.address); + const buyerMeTokenAfter = await meToken.balanceOf(account2.address); + + const vaultDAIAfter = await dai.balanceOf(initialVault.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + expect(ownerMeTokenAfter).to.equal(0); + expect(toETHNumber(buyerMeTokenAfter)).to.be.approximately( + calculatedReturn, + 1e-15 + ); + expect(meTokenTotalSupplyAfter).to.be.equal(buyerMeTokenAfter); + expect(vaultDAIAfter.sub(vaultDAIBefore)).to.equal(tokenDeposited); + }); + it("burn() [buyer]: assets received based on initial vault", async () => { + const buyerMeToken = await meToken.balanceOf(account2.address); + // transfer some MeTokens to owner + await meToken + .connect(account2) + .transfer(account1.address, buyerMeToken.div(2)); + const ownerMeToken = await meToken.balanceOf(account2.address); + expect(ownerMeToken).to.be.equal(buyerMeToken.div(2)); + + const vaultDAIBefore = await dai.balanceOf(initialVault.address); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenInfo = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + const buyerDAIBefore = await dai.balanceOf(account2.address); + + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(buyerMeToken.div(2)), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenInfo.balancePooled), + reserveWeight / MAX_WEIGHT + ); + const assetsReturned = (rawAssetsReturned * refundRatio) / MAX_WEIGHT; + + // buyer burns meTokens and keep the collateral returned + await foundry + .connect(account2) + .burn(meToken.address, buyerMeToken.div(2), account2.address); + + const buyerMeTokenAfter = await meToken.balanceOf(account2.address); + const buyerDAIAfter = await dai.balanceOf(account2.address); + const vaultDAIAfter = await dai.balanceOf(initialVault.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + const burnFee = toETHNumber( + (await fees.burnBuyerFee()) + .mul(fromETHNumber(assetsReturned)) + .div(PRECISION) + ); + + expect( + toETHNumber(buyerDAIAfter.sub(buyerDAIBefore)) + ).to.be.approximately( + new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), + 1e-15 + ); + expect(buyerMeTokenAfter).to.equal(0); + expect(toETHNumber(meTokenTotalSupplyAfter)).to.be.approximately( + toETHNumber(meTokenTotalSupply.div(2)), + 1e-18 + ); + expect( + toETHNumber(vaultDAIBefore.sub(vaultDAIAfter)) + ).to.be.approximately( + new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), + 1e-15 + ); + }); + it("burn() [owner]: assets received based on initial vault", async () => { + const ownerMeToken = (await meToken.balanceOf(account1.address)).div(2); + const vaultDAIBefore = await dai.balanceOf(initialVault.address); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenInfo = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + const ownerDAIBefore = await dai.balanceOf(account1.address); + + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(ownerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenInfo.balancePooled), + reserveWeight / MAX_WEIGHT + ); + const assetsReturned = + rawAssetsReturned + + (toETHNumber(ownerMeToken) / toETHNumber(meTokenTotalSupply)) * + toETHNumber(meTokenInfo.balanceLocked); + await foundry + .connect(account1) + .burn(meToken.address, ownerMeToken, account1.address); + const ownerMeTokenAfter = await meToken.balanceOf(account1.address); + const ownerDAIAfter = await dai.balanceOf(account1.address); + const vaultDAIAfter = await dai.balanceOf(initialVault.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + const burnFee = toETHNumber( + (await fees.burnOwnerFee()) + .mul(fromETHNumber(assetsReturned)) + .div(PRECISION) + ); + + expect(vaultDAIBefore.sub(vaultDAIAfter)).to.equal( + ownerDAIAfter.sub(ownerDAIBefore) + ); + expect( + toETHNumber(ownerDAIAfter.sub(ownerDAIBefore)) + ).to.be.approximately( + new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), + 1e-15 + ); + // only half of the metoken has been burnt (add 1 wei for precision) + expect(ownerMeTokenAfter).to.equal(ownerMeToken.add(1)); + // only half of the metoken has been burnt + expect(toETHNumber(meTokenTotalSupplyAfter)).to.equal( + toETHNumber(meTokenTotalSupply.div(2)) + ); + }); + }); + + describe("Duration", () => { + before(async () => { + const meTokenInfo = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + await mineBlock(meTokenInfo.startTime.toNumber() + 2); + + const block = await ethers.provider.getBlock("latest"); + expect(meTokenInfo.startTime).to.be.lt(block.timestamp); + }); + it("mint() [owner]: meTokens bought with DAI and migration vault swap it all to WETH", async () => { + // this first mint during migration period will trigger + // the deposit of dai to the migration contract + // and will swap dai to weth + // It will then update the balance pooled / locked to reflect the new amount in weth + const vaultInitDAIBefore = await dai.balanceOf(initialVault.address); + const vaultTargetWETHBefore = await weth.balanceOf(targetVault.address); + const vaultTargetDaiBefore = await dai.balanceOf(targetVault.address); + expect(vaultTargetDaiBefore).to.equal(0); + expect(vaultTargetWETHBefore).to.equal(0); + const migrationWETHBefore = await weth.balanceOf(migration.address); + const meTokenInfo = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + const balanceLockedBefore = meTokenInfo.balanceLocked; + const balancePooledBefore = meTokenInfo.balancePooled; + await setAutomine(false); + const block = await ethers.provider.getBlock("latest"); + + const migrationDetails = await migration.getDetails(meToken.address); + const price = await getQuote( + UNIV3Factory, + dai, + weth, + migrationDetails.fee, + tokenDeposited.add(vaultInitDAIBefore) + ); + const account1DaiBefore = await dai.balanceOf(account1.address); + await foundry + .connect(account1) + .mint(meToken.address, tokenDeposited, account1.address); + + await mineBlock(block.timestamp + 1); + await setAutomine(true); + + const vaultDAIAfter = await dai.balanceOf(initialVault.address); + const meTokenInfoAfter = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + const balanceLockedAfter = meTokenInfoAfter.balanceLocked; + const balancePooledAfter = meTokenInfoAfter.balancePooled; + + // all dai should have been swapped to ETH + const migrationWETHAfter = await weth.balanceOf(migration.address); + expect(migrationWETHBefore).to.equal(0); + expect(toETHNumber(migrationWETHAfter)).to.be.approximately( + Number(price.token0Price), + 0.01 + ); + // balance should have been updated accordingly + // balance locked has not been changed during mint + expect(toETHNumber(balanceLockedAfter)).to.be.approximately( + toETHNumber(balanceLockedBefore) / + Number(price.oneToken1inToken0Price), + 0.001 + ); + // balance pooled increased by the deposited amount in dai + expect(toETHNumber(balancePooledAfter)).to.be.approximately( + (toETHNumber(balancePooledBefore) + toETHNumber(tokenDeposited)) / + Number(price.oneToken1inToken0Price), + 0.001 + ); + const ownerMeTokenAfter = await meToken.balanceOf(account1.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + const vaultTargetDaiAfter = await dai.balanceOf(targetVault.address); + expect(vaultTargetDaiAfter).to.equal(0); + expect(meTokenTotalSupplyAfter).to.be.equal(ownerMeTokenAfter); + + // there is some dai left on the vault that belongs to the DAO that has not been swapped + // we should not have anymore dai on vault neither on migration + const daoDaiBalance = await dai.balanceOf(account0.address); + await initialVault.claim(dai.address, true, 0); + const daoDaiAfter = await dai.balanceOf(account0.address); + expect(daoDaiAfter.sub(daoDaiBalance)).equal(vaultDAIAfter); + const vaultDAIAfterClaim = await dai.balanceOf(initialVault.address); + expect(vaultDAIAfterClaim).to.equal(0); // old asset goes to migration and dao claimed fees + }); + it("burn() [buyer]: assets returned is WETH", async () => { + const ownerMeToken = await meToken.balanceOf(account1.address); + await meToken + .connect(account1) + .transfer(account2.address, ownerMeToken.div(2)); + const buyerMeToken = await meToken.balanceOf(account2.address); + expect(buyerMeToken).to.be.equal(ownerMeToken.div(2)); + + const migrationWETHBefore = await weth.balanceOf(migration.address); + const meTokenTotalSupply = await meToken.totalSupply(); + const buyerWETHBefore = await weth.balanceOf(account2.address); + const meTokenInfo = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + + await setAutomine(false); + const block = await ethers.provider.getBlock("latest"); + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(buyerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenInfo.balancePooled), + reserveWeight / MAX_WEIGHT + ); + // burn meToken returns WETH from migration contract + await foundry + .connect(account2) + .burn(meToken.address, buyerMeToken, account2.address); + const det = await migration.getDetails(meToken.address); + expect(det.started).to.be.true; + // expect(det.swapped).to.be.true; + + const assetsReturned = (rawAssetsReturned * refundRatio) / MAX_WEIGHT; + + await mineBlock(block.timestamp + 1); + await setAutomine(true); + + const buyerMeTokenAfter = await meToken.balanceOf(account2.address); + const buyerWETHAfter = await weth.balanceOf(account2.address); + const migrationWETHAfter = await weth.balanceOf(migration.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + const burnFee = toETHNumber( + (await fees.burnBuyerFee()) + .mul(fromETHNumber(assetsReturned)) + .div(PRECISION) + ); + // buyer gets WETH + expect( + toETHNumber(buyerWETHAfter.sub(buyerWETHBefore)) + ).to.be.approximately( + new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), + 1e-15 + ); + expect(buyerMeTokenAfter).to.equal(0); + expect(toETHNumber(meTokenTotalSupplyAfter)).to.be.approximately( + toETHNumber(meTokenTotalSupply.div(2)), + 1e-18 + ); + // less WETH in migration due to the burn + expect( + toETHNumber(migrationWETHBefore.sub(migrationWETHAfter)) + ).to.approximately( + new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), + 1e-15 + ); + const meTokenInfoAfter = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + const balanceLockedBefore = meTokenInfo.balanceLocked; + const balancePooledBefore = meTokenInfo.balancePooled; + const balanceLockedAfter = meTokenInfoAfter.balanceLocked; + const balancePooledAfter = meTokenInfoAfter.balancePooled; + + // the extracted balance pooled will be transferred to balance locked and to the burner + expect( + toETHNumber( + migrationWETHBefore + .sub(migrationWETHAfter) + .add(balanceLockedAfter.sub(balanceLockedBefore)) + ) + ).to.approximately( + toETHNumber(balancePooledBefore.sub(balancePooledAfter)), + 1e-11 + ); + }); + it("burn() [owner]: assets received based on weighted average of vault", async () => { + const ownerMeToken = await meToken.balanceOf(account1.address); + const migrationWETHBefore = await weth.balanceOf(migration.address); + const meTokenTotalSupply = await meToken.totalSupply(); + let meTokenInfo = await meTokenRegistry.getMeTokenInfo(meToken.address); + const ownerWETHBefore = await weth.balanceOf(account1.address); + + await setAutomine(false); + const block = await ethers.provider.getBlock("latest"); + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(ownerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenInfo.balancePooled), + reserveWeight / MAX_WEIGHT + ); + const assetsReturned = + rawAssetsReturned + + (toETHNumber(ownerMeToken) / toETHNumber(meTokenTotalSupply)) * + toETHNumber(meTokenInfo.balanceLocked); + + await foundry + .connect(account1) + .burn(meToken.address, ownerMeToken, account1.address); + + await mineBlock(block.timestamp + 1); + await setAutomine(true); + const ownerMeTokenAfter = await meToken.balanceOf(account1.address); + const ownerWETHAfter = await weth.balanceOf(account1.address); + const migrationWETHAfter = await weth.balanceOf(migration.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + const burnFee = toETHNumber( + (await fees.burnOwnerFee()) + .mul(fromETHNumber(assetsReturned)) + .div(PRECISION) + ); + const det = await migration.getDetails(meToken.address); + expect(migrationWETHBefore.sub(migrationWETHAfter)).to.equal( + ownerWETHAfter.sub(ownerWETHBefore) + ); + expect( + toETHNumber(ownerWETHAfter.sub(ownerWETHBefore)) + ).to.be.approximately( + new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), + 1e-15 + ); + expect(ownerMeTokenAfter).to.equal(0); + expect(meTokenTotalSupplyAfter).to.equal(0); + meTokenInfo = await meTokenRegistry.getMeTokenInfo(meToken.address); + const meTokenInfoAfter = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + const balanceLockedAfter = meTokenInfoAfter.balanceLocked; + const balancePooledAfter = meTokenInfoAfter.balancePooled; + expect(balanceLockedAfter).to.equal(0); + expect(balancePooledAfter).to.equal(0); + }); + }); + + describe("Cooldown", () => { + before(async () => { + const meTokenInfo = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + await mineBlock(meTokenInfo.endTime.toNumber() + 2); + const block = await ethers.provider.getBlock("latest"); + expect(meTokenInfo.endTime).to.be.lt(block.timestamp); + tokenDepositedInETH = 4.2; + tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + }); + it("mint() [owner]: assets received based on target vault", async () => { + const vaultWETHBefore = await weth.balanceOf(targetVault.address); + const migrationWETHBefore = await weth.balanceOf(migration.address); + const meTokenTotalSupplyBefore = await meToken.totalSupply(); + expect(meTokenTotalSupplyBefore).to.be.equal(0); + let meTokenInfo = await meTokenRegistry.getMeTokenInfo(meToken.address); + const rawAssetsReturned = calculateTokenReturnedFromZero( + tokenDepositedInETH, + toETHNumber(baseY), + reserveWeight / MAX_WEIGHT + ); + // target vault is empty before end of migration + const targetVaultWETHBefore = await weth.balanceOf(targetVault.address); + expect(targetVaultWETHBefore).to.be.equal(0); + await foundry + .connect(account1) + .mint(meToken.address, tokenDeposited, account1.address); + meTokenInfo = await meTokenRegistry.getMeTokenInfo(meToken.address); + const ownerMeTokenAfter = await meToken.balanceOf(account1.address); + const vaultWETHAfter = await weth.balanceOf(initialVault.address); + const targetVaultWETHAfter = await weth.balanceOf(targetVault.address); + const migrationWETHAfter = await weth.balanceOf(migration.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + expect(toETHNumber(ownerMeTokenAfter)).to.be.approximately( + rawAssetsReturned, + 1e-15 + ); + expect(meTokenTotalSupplyAfter).to.be.equal(ownerMeTokenAfter); + expect(vaultWETHAfter.sub(vaultWETHBefore)).to.equal(0); + expect(targetVaultWETHAfter.sub(targetVaultWETHBefore)).to.equal( + tokenDeposited + ); + expect(migrationWETHAfter.sub(migrationWETHBefore)).to.equal(0); + + const meTokenInfoAfter = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + const balanceLockedAfter = meTokenInfoAfter.balanceLocked; + const balancePooledAfter = meTokenInfoAfter.balancePooled; + expect(balanceLockedAfter).to.equal(0); + expect(balancePooledAfter).to.equal(tokenDeposited); + }); + it("burn() [buyer]: assets received based on target vault", async () => { + const ownerMeToken = await meToken.balanceOf(account1.address); + await meToken + .connect(account1) + .transfer(account2.address, ownerMeToken.div(2)); + const buyerMeToken = await meToken.balanceOf(account2.address); + const vaultWETHBefore = await weth.balanceOf(targetVault.address); + const meTokenTotalSupply = await meToken.totalSupply(); + const buyerWETHBefore = await weth.balanceOf(account2.address); + const meTokenInfo = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + const targetAssetsReturned = calculateCollateralReturned( + toETHNumber(buyerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenInfo.balancePooled), + reserveWeight / MAX_WEIGHT + ); + + await foundry + .connect(account2) + .burn(meToken.address, buyerMeToken, account2.address); + + const assetsReturned = + (targetAssetsReturned * refundRatio) / MAX_WEIGHT; + + const buyerMeTokenAfter = await meToken.balanceOf(account2.address); + const buyerWETHAfter = await weth.balanceOf(account2.address); + const vaultWETHAfter = await weth.balanceOf(targetVault.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + const burnFee = toETHNumber( + (await fees.burnBuyerFee()) + .mul(fromETHNumber(assetsReturned)) + .div(PRECISION) + ); + const meTokenInfoAfter = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + expect( + toETHNumber(buyerWETHAfter.sub(buyerWETHBefore)) + ).to.be.approximately( + new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), + 1e-15 + ); + expect(buyerMeTokenAfter).to.equal(0); + expect(toETHNumber(meTokenTotalSupplyAfter)).to.be.approximately( + toETHNumber(meTokenTotalSupply.div(2)), + 1e-18 + ); + expect( + toETHNumber(vaultWETHBefore.sub(vaultWETHAfter)) + ).to.approximately( + new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), + 1e-15 + ); + }); + it("burn() [owner]: assets received based on target vault", async () => { + const ownerMeToken = await meToken.balanceOf(account1.address); + const vaultWETHBefore = await weth.balanceOf(targetVault.address); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenInfo = await meTokenRegistry.getMeTokenInfo( + meToken.address + ); + const ownerWETHBefore = await weth.balanceOf(account1.address); + + const targetAssetsReturned = calculateCollateralReturned( + toETHNumber(ownerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenInfo.balancePooled), + reserveWeight / MAX_WEIGHT + ); + + await foundry + .connect(account1) + .burn(meToken.address, ownerMeToken, account1.address); + + const assetsReturned = + targetAssetsReturned + + (toETHNumber(ownerMeToken) / toETHNumber(meTokenTotalSupply)) * + toETHNumber(meTokenInfo.balanceLocked); + + const ownerMeTokenAfter = await meToken.balanceOf(account1.address); + const ownerWETHAfter = await weth.balanceOf(account1.address); + const vaultWETHAfter = await weth.balanceOf(targetVault.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + const burnFee = toETHNumber( + (await fees.burnOwnerFee()) + .mul(fromETHNumber(assetsReturned)) + .div(PRECISION) + ); + + expect(vaultWETHBefore.sub(vaultWETHAfter)).to.equal( + ownerWETHAfter.sub(ownerWETHBefore) + ); + expect( + toETHNumber(ownerWETHAfter.sub(ownerWETHBefore)) + ).to.be.approximately( + new Decimal(assetsReturned).sub(new Decimal(burnFee)).toNumber(), + 1e-14 + ); + expect(ownerMeTokenAfter).to.equal(0); + expect(toETHNumber(meTokenTotalSupplyAfter)).to.equal(0); + }); + }); + }); +}; + +setup().then(() => { + run(); +}); diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index 791128b2..824b5f38 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -326,7 +326,17 @@ export const toETHNumber = (num: BigNumber | string): number => { }; export const fromETHNumber = (num: number): BigNumber => { - return ethers.utils.parseEther(num.toString()); + let res: BigNumber = BigNumber.from(0); + let curNum = num.toString(); + while (curNum.length > 1) { + try { + res = ethers.utils.parseEther(curNum); + break; + } catch (e) { + curNum = curNum.slice(0, curNum.length - 1); + } + } + return res; }; /** Bancor curve calculation */ diff --git a/test/utils/uniswap.ts b/test/utils/uniswap.ts new file mode 100644 index 00000000..aeeeb9ed --- /dev/null +++ b/test/utils/uniswap.ts @@ -0,0 +1,142 @@ +import { Percent, Token, CurrencyAmount, TradeType } from "@uniswap/sdk-core"; +import { Pool } from "@uniswap/v3-sdk"; +import { abi as IUniswapV3PoolABI } from "@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json"; +import { abi as IUniswapV3FactoryABI } from "@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Factory.sol/IUniswapV3Factory.json"; + +import { BigNumber } from "ethers"; +import { ethers } from "hardhat"; +import { ERC20 } from "../../artifacts/types"; + +interface Immutables { + factory: string; + token0: string; + token1: string; + fee: number; + tickSpacing: number; + maxLiquidityPerTick: BigNumber; +} + +interface State { + liquidity: BigNumber; + sqrtPriceX96: BigNumber; + tick: number; + observationIndex: number; + observationCardinality: number; + observationCardinalityNext: number; + feeProtocol: number; + unlocked: boolean; +} +async function getPoolImmutables(poolContract: any) { + const [factory, token0, token1, fee, tickSpacing, maxLiquidityPerTick] = + await Promise.all([ + poolContract.factory(), + poolContract.token0(), + poolContract.token1(), + poolContract.fee(), + poolContract.tickSpacing(), + poolContract.maxLiquidityPerTick(), + ]); + + const immutables: Immutables = { + factory, + token0, + token1, + fee, + tickSpacing, + maxLiquidityPerTick, + }; + return immutables; +} +async function getPoolState(poolContract: any) { + const [liquidity, slot] = await Promise.all([ + poolContract.liquidity(), + poolContract.slot0(), + ]); + + const PoolState: State = { + liquidity, + sqrtPriceX96: slot[0], + tick: slot[1], + observationIndex: slot[2], + observationCardinality: slot[3], + observationCardinalityNext: slot[4], + feeProtocol: slot[5], + unlocked: slot[6], + }; + + return PoolState; +} +export const getQuote = async ( + UNIV3Factory: string, + token0: ERC20, + token1: ERC20, + fee: number, + amount: BigNumber +) => { + const factoryContract = new ethers.Contract( + UNIV3Factory, + IUniswapV3FactoryABI, + ethers.provider + ); + + const poolAddress = await factoryContract.getPool( + token0.address, + token1.address, + fee + ); + const poolContract = new ethers.Contract( + poolAddress, + IUniswapV3PoolABI, + ethers.provider + ); + + const chainId = (await ethers.provider.getNetwork()).chainId; + const [state] = await Promise.all([getPoolState(poolContract)]); + + const input = new Token( + chainId, + token0.address, + await token0.decimals(), + await token0.symbol(), + await token0.name() + ); + + const output = new Token( + chainId, + token1.address, + await token1.decimals(), + await token1.symbol(), + await token1.name() + ); + const poolExample = new Pool( + input, + output, + fee, + state.sqrtPriceX96.toString(), + state.liquidity.toString(), + state.tick + ); + const token0Amount = CurrencyAmount.fromRawAmount(input, amount.toString()); + const token1Amount = CurrencyAmount.fromRawAmount(output, amount.toString()); + + return { + oneToken0InToken1: poolExample.token0Price + .quote( + CurrencyAmount.fromRawAmount( + input, + ethers.utils.parseEther("1").toString() + ) + ) + .toFixed(), + oneToken1inToken0Price: poolExample.token1Price + .quote( + CurrencyAmount.fromRawAmount( + output, + ethers.utils.parseEther("1").toString() + ) + ) + .toFixed(), + token0Price: poolExample.token0Price.quote(token0Amount).toFixed(), + token1Price: poolExample.token1Price.quote(token1Amount).toFixed(), + }; +}; diff --git a/yarn.lock b/yarn.lock index 61fb2094..40853c91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -442,7 +442,7 @@ "@ethersproject/logger" "^5.5.0" "@ethersproject/rlp" "^5.5.0" -"@ethersproject/address@5.6.0", "@ethersproject/address@^5.6.0": +"@ethersproject/address@5.6.0", "@ethersproject/address@^5.0.0", "@ethersproject/address@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.0.tgz#13c49836d73e7885fc148ad633afad729da25012" integrity sha512-6nvhYXjbXsHPS+30sHZ+U4VMagFC/9zAk6Gd/h3S21YW4+yfb0WfRtaAIZ4kfM4rrVwqiy284LP0GtL5HXGLxQ== @@ -869,7 +869,7 @@ "@ethersproject/sha2" "^5.5.0" "@ethersproject/strings" "^5.5.0" -"@ethersproject/solidity@5.6.0": +"@ethersproject/solidity@5.6.0", "@ethersproject/solidity@^5.0.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.6.0.tgz#64657362a596bf7f5630bdc921c07dd78df06dc3" integrity sha512-YwF52vTNd50kjDzqKaoNNbC/r9kMDPq3YzDWmsjFTRBcIF1y4JCQJ8gB30wsTfHbaxgxelI5BfxQSxD/PbJOww== @@ -1301,6 +1301,18 @@ resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz#f055979a99f7654e84d6b8e6267419e9c4cfff87" integrity sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ== +"@types/async-retry@^1.4.2": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.4.4.tgz#3304ce1e64f8757723f166518fc4c4b68df4fe66" + integrity sha512-IGT+yESLPYje0MV8MfOpT5V5oH9lAKLwlosQRyq75tYJmntkkWcfEThHLxsgYjGmYXJEY7ZZkYPb4xuW+NA6GA== + dependencies: + "@types/retry" "*" + +"@types/await-timeout@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@types/await-timeout/-/await-timeout-0.3.1.tgz#3a0baafc3a96c7a14447a4dcfdcc76b21ce97c3b" + integrity sha512-H5PzROT4KuP7XQDua13Iw8did//OCKAZ/3TL15DjvMzDonrk4HvhH1+tLko96f2guU6XaD3AoqRa49ZOwbwNig== + "@types/big.js@^6.1.1": version "6.1.3" resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.3.tgz#c008dec4dae24c7a338ebb4521c46e9609020807" @@ -1320,6 +1332,20 @@ dependencies: "@types/node" "*" +"@types/bunyan-blackhole@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@types/bunyan-blackhole/-/bunyan-blackhole-0.2.2.tgz#469e58c5d129027a9e08bcf9a36232a69b6ad011" + integrity sha512-nbuxFn2FVw1AAT1h6shgluwz1cgpLKaMBYbEZcMU69Jb1UvSsXcwRiIg+FP4+/JjEUp/uPYLC+twWpfCAaVN1g== + dependencies: + "@types/bunyan" "*" + +"@types/bunyan@*", "@types/bunyan@^1.8.6": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.8.tgz#8d6d33f090f37c07e2a80af30ae728450a101008" + integrity sha512-Cblq+Yydg3u+sGiz2mjHjC5MPmdjY+No4qvHrF+BUhblsmSfMvsHLbOG62tPbonsqBj6sbWv1LHcsoe5Jw+/Ow== + dependencies: + "@types/node" "*" + "@types/chai@*": version "4.3.0" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc" @@ -1368,6 +1394,11 @@ "@types/level-errors" "*" "@types/node" "*" +"@types/lodash@^4.14.168": + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== + "@types/lru-cache@^5.1.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef" @@ -1462,6 +1493,11 @@ dependencies: "@types/node" "*" +"@types/retry@*": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" + integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== + "@types/secp256k1@^4.0.1": version "4.0.3" resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.3.tgz#1b8e55d8e00f08ee7220b4d59a6abe89c37a901c" @@ -1489,6 +1525,11 @@ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== +"@types/stats-lite@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/stats-lite/-/stats-lite-2.2.0.tgz#bc8190bf9dfa1e16b89eaa2b433c99dff0804de9" + integrity sha512-YV6SS4QC+pbzqjMIV8qVSTDOOazgKBLTVaN+7PfuxELjz/eyzc20KwDVGPrbHt2OcYMA7K2ezLB45Cp6DpNOSQ== + "@types/underscore@*": version "1.11.4" resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.11.4.tgz#62e393f8bc4bd8a06154d110c7d042a93751def3" @@ -1507,12 +1548,28 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== +"@uniswap/default-token-list@^2.0.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-2.3.0.tgz#e5e522e775791999643aac9b0faf1ccfb4c49bd8" + integrity sha512-yfd4snv9K20tEbNwy9Vjym41RU3Yb2lN0seKxsgkr+m3f6oub2lWyXfTiNwgGFbOQPDvX4dxjMhA+M+S7mxqKg== + "@uniswap/lib@^4.0.1-alpha": version "4.0.1-alpha" resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-4.0.1-alpha.tgz#2881008e55f075344675b3bca93f020b028fbd02" integrity sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA== -"@uniswap/sdk-core@^3.0.1": +"@uniswap/router-sdk@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@uniswap/router-sdk/-/router-sdk-1.0.5.tgz#59b429327b0d202744db6de958e6b61d5adeec12" + integrity sha512-PA/0Ye9u3U5cm/LHrJuOgVSff6W4oFY3h+MUEzdp80SYIQdZZJ+BV06RL0h5KOSH+tuIX72n/qNusuRcf2Tc8Q== + dependencies: + "@ethersproject/abi" "^5.5.0" + "@uniswap/sdk-core" "^3.0.1" + "@uniswap/swap-router-contracts" "1.1.0" + "@uniswap/v2-sdk" "^3.0.1" + "@uniswap/v3-sdk" "^3.7.1" + +"@uniswap/sdk-core@^3.0.0-alpha.3", "@uniswap/sdk-core@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-3.0.1.tgz#d08dd68257983af64b9a5f4d6b9cf26124b4138f" integrity sha512-WbeDkhZ9myVR0VnHOdTrb8nHKKkqTFa5uE9RvUbG3eyDt2NWWDwhhqGHwAWJEHG405l30Fa1u3PogHDFsIOQlA== @@ -1524,11 +1581,81 @@ tiny-invariant "^1.1.0" toformat "^2.0.0" +"@uniswap/smart-order-router@^2.5.22": + version "2.5.26" + resolved "https://registry.yarnpkg.com/@uniswap/smart-order-router/-/smart-order-router-2.5.26.tgz#8954cd1bca476bf821aaffe81071bcf89bc869b8" + integrity sha512-KCjxZoPawBEkvt5pJutsPC1Db61f6qrnSpWUPeC8fRJEKXc72msbtzopqB4q4ex5S0Ax3ck4gbA1qRO7zBeixQ== + dependencies: + "@types/async-retry" "^1.4.2" + "@types/await-timeout" "^0.3.1" + "@types/bunyan" "^1.8.6" + "@types/bunyan-blackhole" "^0.2.2" + "@types/lodash" "^4.14.168" + "@types/stats-lite" "^2.2.0" + "@uniswap/default-token-list" "^2.0.0" + "@uniswap/router-sdk" "^1.0.5" + "@uniswap/swap-router-contracts" "1.2.0" + "@uniswap/token-lists" "^1.0.0-beta.25" + "@uniswap/v2-sdk" "^3.0.1" + "@uniswap/v3-sdk" "^3.7.0" + async-retry "^1.3.1" + await-timeout "^1.1.1" + axios "^0.21.1" + bunyan "^1.8.15" + bunyan-blackhole "^1.1.1" + bunyan-debug-stream "^2.0.0" + ethers "^5.1.4" + graphql "^15.5.0" + graphql-request "^3.4.0" + lodash "^4.17.21" + mnemonist "^0.38.3" + node-cache "^5.1.2" + stats-lite "^2.2.0" + +"@uniswap/swap-router-contracts@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@uniswap/swap-router-contracts/-/swap-router-contracts-1.1.0.tgz#e027b14d4c172f231c53c48e1fd708a78d7d94d8" + integrity sha512-GPmpx1lvjXWloB95+YUabr3UHJYr3scnSS8EzaNXnNrIz9nYZ+XQcMaJxOKe85Yi7IfcUQpj0HzD2TW99dtolA== + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + "@uniswap/v3-periphery" "1.3.0" + hardhat-watcher "^2.1.1" + +"@uniswap/swap-router-contracts@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@uniswap/swap-router-contracts/-/swap-router-contracts-1.2.0.tgz#9fd3be07c40c697cf71804ab450ca7d6c028401d" + integrity sha512-wau6xVIrXY/7EQc3uB+A1HlRfxnewS4MAYOgcJBHGHG4OVy+X0WAl8mILC52Xx+y6vx6uQHOwH+sT3SaWi5EGw== + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + "@uniswap/v3-periphery" "1.3.0" + dotenv "^14.2.0" + hardhat-watcher "^2.1.1" + +"@uniswap/token-lists@^1.0.0-beta.25": + version "1.0.0-beta.28" + resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.28.tgz#76bb6ec25e76ae6bece9efc11b9b68079b2b9402" + integrity sha512-MmVeoOd/HlZYOn5NT2mlDoOUYjnZb+3V7aCD5/YwSgsqeDRXZmKfKHafEEhiZoMvXuZdqSBg4L/ythvzAf9cwA== + "@uniswap/v2-core@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425" integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== +"@uniswap/v2-sdk@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v2-sdk/-/v2-sdk-3.0.1.tgz#690c484104c1debd1db56a236e5497def53d698b" + integrity sha512-eSpm2gjo2CZh9FACH5fq42str/oSNyWcDxB27o5k44bEew4sxb+pld4gGIf/byJndLBvArR9PtH8c0n/goNOTw== + dependencies: + "@ethersproject/address" "^5.0.0" + "@ethersproject/solidity" "^5.0.0" + "@uniswap/sdk-core" "^3.0.0-alpha.3" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + "@uniswap/v3-core@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0.tgz#6c24adacc4c25dceee0ba3ca142b35adbd7e359d" @@ -1539,6 +1666,18 @@ resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.1.tgz#b6d2bdc6ba3c3fbd610bdc502395d86cd35264a0" integrity sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ== +"@uniswap/v3-periphery@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.3.0.tgz#37f0a1ef6025221722e50e9f3f2009c2d5d6e4ec" + integrity sha512-HjHdI5RkjBl8zz3bqHShrbULFoZSrjbbrRHoO2vbzn+WRzTa6xY4PWphZv2Tlcb38YEKfKHp6NPl5hVedac8uw== + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/lib" "^4.0.1-alpha" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + base64-sol "1.0.1" + hardhat-watcher "^2.1.1" + "@uniswap/v3-periphery@^1.0.1", "@uniswap/v3-periphery@^1.1.1": version "1.4.0" resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.4.0.tgz#9abb733fc596916718070c688b5f426fd8d01fe3" @@ -1563,7 +1702,7 @@ base64-sol "1.0.1" hardhat-watcher "^2.1.1" -"@uniswap/v3-sdk@^3.8.1": +"@uniswap/v3-sdk@^3.7.0", "@uniswap/v3-sdk@^3.7.1", "@uniswap/v3-sdk@^3.8.1": version "3.8.2" resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.8.2.tgz#6e10eebe0da851a4f6a5036a1e70a896a5c239d4" integrity sha512-JVrs1ZuWKP8JmYi5hxFliOnCFAEREwMpE7qB6rKPCo155/pIRyQ90lJfQZCJO7pP/webx5t5JFxCOytmDIp29Q== @@ -1972,6 +2111,13 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-retry@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + async@1.x, async@^1.4.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -2011,6 +2157,11 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +await-timeout@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/await-timeout/-/await-timeout-1.1.1.tgz#d42062ee6bc4eb271fe4d4f851eb658dae7e3906" + integrity sha512-gsDXAS6XVc4Jt+7S92MPX6Noq69bdeXUPEaXd8dk3+yVr629LTDLxNt4j1ycBbrU+AStK2PhKIyNIM+xzWMVOQ== + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -2838,6 +2989,31 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.3.0" +bunyan-blackhole@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/bunyan-blackhole/-/bunyan-blackhole-1.1.1.tgz#b9208586dc0b4e47f4f713215b1bddd65e4f6257" + integrity sha1-uSCFhtwLTkf09xMhWxvd1l5PYlc= + dependencies: + stream-blackhole "^1.0.3" + +bunyan-debug-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bunyan-debug-stream/-/bunyan-debug-stream-2.0.1.tgz#9bd7c7e30c7b2cf711317e9d37529b0464c3b164" + integrity sha512-MCEoqggU7NMt7f2O+PU8VkqfSkoQoa4lmN/OWhaRfqFRBF1Se2TOXQyLF6NxC+EtfrdthnquQe8jOe83fpEoGA== + dependencies: + colors "1.4.0" + exception-formatter "^1.0.4" + +bunyan@^1.8.15: + version "1.8.15" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + bytes@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" @@ -3186,7 +3362,7 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" -clone@2.1.2, clone@^2.0.0: +clone@2.1.2, clone@2.x, clone@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= @@ -3238,7 +3414,7 @@ colorette@^2.0.16: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== -colors@1.4.0, colors@^1.1.2: +colors@1.4.0, colors@^1.0.3, colors@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -3510,6 +3686,13 @@ cross-fetch@^2.1.0, cross-fetch@^2.1.1: node-fetch "2.6.1" whatwg-fetch "2.0.4" +cross-fetch@^3.0.6: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -3846,6 +4029,11 @@ dotenv@*, dotenv@^16.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411" integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q== +dotenv@^14.2.0: + version "14.3.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-14.3.2.tgz#7c30b3a5f777c79a3429cb2db358eef6751e8369" + integrity sha512-vwEppIphpFdvaMCaHfCEv9IgwcxMljMw2TnAQBB4VWPvzXQLTb82jwmdOKzlEVUL3gNFT4l4TPKO+Bn+sqcrVQ== + dotignore@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" @@ -3862,6 +4050,13 @@ drbg.js@^1.0.1: create-hash "^1.1.2" create-hmac "^1.1.4" +dtrace-provider@~0.8: + version "0.8.8" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" + integrity sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg== + dependencies: + nan "^2.14.0" + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -4672,7 +4867,7 @@ ethers@^5.0.1, ethers@^5.0.2, ethers@^5.5.2: "@ethersproject/web" "5.5.1" "@ethersproject/wordlists" "5.5.0" -ethers@^5.6.4: +ethers@^5.1.4, ethers@^5.6.4: version "5.6.4" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.6.4.tgz#23629e9a7d4bc5802dfb53d4da420d738744b53c" integrity sha512-62UIfxAQXdf67TeeOaoOoPctm5hUlYgfd0iW3wxfj7qRYKDcvvy0f+sJ3W2/Pyx77R8dblvejA8jokj+lS+ATQ== @@ -4747,6 +4942,13 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +exception-formatter@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/exception-formatter/-/exception-formatter-1.0.7.tgz#3291616b86fceabefa97aee6a4708032c6e3b96d" + integrity sha512-zV45vEsjytJrwfGq6X9qd1Ll56cW4NC2mhCO6lqwMk4ZpA1fZ6C3UiaQM/X7if+7wZFmCgss3ahp9B/uVFuLRw== + dependencies: + colors "^1.0.3" + execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -4866,6 +5068,11 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" +extract-files@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" + integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -5119,6 +5326,15 @@ form-data@^2.2.0, form-data@^2.3.3: combined-stream "^1.0.6" mime-types "^2.1.12" +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -5451,6 +5667,17 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" @@ -5548,6 +5775,20 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== +graphql-request@^3.4.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-3.7.0.tgz#c7406e537084f8b9788541e3e6704340ca13055b" + integrity sha512-dw5PxHCgBneN2DDNqpWu8QkbbJ07oOziy8z+bK/TAXufsOLaETuVO4GkXrbs0WjhdKhBMN3BkpN/RIvUHkmNUQ== + dependencies: + cross-fetch "^3.0.6" + extract-files "^9.0.0" + form-data "^3.0.0" + +graphql@^15.5.0: + version "15.8.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" + integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -6439,6 +6680,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +isnumber@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isnumber/-/isnumber-1.0.0.tgz#0e3f9759b581d99dd85086f0ec2a74909cfadd01" + integrity sha1-Dj+XWbWB2Z3YUIbw7Cp0kJz63QE= + isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -7427,6 +7673,11 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + minipass@^2.6.0, minipass@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" @@ -7469,7 +7720,14 @@ mkdirp@0.5.5, mkdirp@0.5.x, mkdirp@^0.5.1, mkdirp@^0.5.5: dependencies: minimist "^1.2.5" -mnemonist@^0.38.0: +mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mnemonist@^0.38.0, mnemonist@^0.38.3: version "0.38.5" resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.5.tgz#4adc7f4200491237fe0fa689ac0b86539685cade" integrity sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg== @@ -7541,6 +7799,11 @@ mock-fs@^4.1.0: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" integrity sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw== +moment@^2.19.3: + version "2.29.3" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3" + integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -7615,6 +7878,15 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mv@~2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI= + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + nan@^2.14.0: version "2.15.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" @@ -7652,6 +7924,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= + negotiator@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" @@ -7677,6 +7954,13 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-cache@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== + dependencies: + clone "2.x" + node-emoji@^1.10.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" @@ -7697,7 +7981,7 @@ node-fetch@2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-fetch@^2.6.1: +node-fetch@2.6.7, node-fetch@^2.6.1: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -8919,6 +9203,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -8943,6 +9232,13 @@ rimraf@^2.2.8, rimraf@^2.6.2, rimraf@^2.6.3: dependencies: glob "^7.1.3" +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto= + dependencies: + glob "^6.0.1" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -9006,6 +9302,11 @@ safe-event-emitter@^1.0.1: dependencies: events "^3.0.0" +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -9580,6 +9881,13 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +stats-lite@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/stats-lite/-/stats-lite-2.2.0.tgz#278a5571fa1d2e8b1691295dccc0235282393bbf" + integrity sha512-/Kz55rgUIv2KP2MKphwYT/NCuSfAlbbMRv2ZWw7wyXayu230zdtzhxxuXXcvsc6EmmhS8bSJl3uS1wmMHFumbA== + dependencies: + isnumber "~1.0.0" + "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -9590,6 +9898,11 @@ stealthy-require@^1.1.1: resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +stream-blackhole@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-blackhole/-/stream-blackhole-1.0.3.tgz#6fc2e2c2e9d9fde6be8c68d3db88de09802e4d63" + integrity sha1-b8LiwunZ/ea+jGjT24jeCYAuTWM= + stream-to-pull-stream@^1.7.1: version "1.7.3" resolved "https://registry.yarnpkg.com/stream-to-pull-stream/-/stream-to-pull-stream-1.7.3.tgz#4161aa2d2eb9964de60bfa1af7feaf917e874ece"