diff --git a/contracts/Foundry.sol b/contracts/Foundry.sol index 4620280b..a3b5cadc 100644 --- a/contracts/Foundry.sol +++ b/contracts/Foundry.sol @@ -342,16 +342,17 @@ contract Foundry is IFoundry, Ownable, Initializable { totalSupply_, meToken_.balancePooled ); + + uint256 targetAssetsReturned; // Logic for if we're switching to a new curve type // updating curveDetails if ( (hub_.updating && (hub_.targetCurve != address(0))) || (hub_.reconfigure) ) { - uint256 targetassetsReturned; if (hub_.targetCurve != address(0)) { // Means we are updating to a new curve type - targetassetsReturned = ICurve(hub_.targetCurve) + targetAssetsReturned = ICurve(hub_.targetCurve) .viewAssetsReturned( _meTokensBurned, meToken_.hubId, @@ -360,7 +361,7 @@ contract Foundry is IFoundry, Ownable, Initializable { ); } else { // Must mean we're updating curveDetails - targetassetsReturned = ICurve(hub_.curve) + targetAssetsReturned = ICurve(hub_.curve) .viewTargetAssetsReturned( _meTokensBurned, meToken_.hubId, @@ -370,10 +371,28 @@ contract Foundry is IFoundry, Ownable, Initializable { } rawAssetsReturned = WeightedAverage.calculate( rawAssetsReturned, - targetassetsReturned, + targetAssetsReturned, hub_.startTime, hub_.endTime ); + } else if (meToken_.targetHubId != 0) { + Details.Hub memory targetHub_ = hub.getDetails( + meToken_.targetHubId + ); + + // Calculate return assuming update is not happening + targetAssetsReturned = ICurve(targetHub_.curve).viewAssetsReturned( + _meTokensBurned, + meToken_.targetHubId, + totalSupply_, + meToken_.balancePooled + ); + rawAssetsReturned = WeightedAverage.calculate( + rawAssetsReturned, + targetAssetsReturned, + meToken_.startTime, + meToken_.endTime + ); } } diff --git a/test/contracts/migrations/UniswapSingleTransfer.ts b/test/contracts/migrations/UniswapSingleTransfer.ts index bcec01ba..b9e99024 100644 --- a/test/contracts/migrations/UniswapSingleTransfer.ts +++ b/test/contracts/migrations/UniswapSingleTransfer.ts @@ -247,6 +247,51 @@ const setup = async () => { ) ).to.be.revertedWith("Invalid _encodedMigrationArgs"); }); + it("should revert when try to approve already approved vaults", async () => { + await expect( + migrationRegistry.approve( + initialVault.address, + targetVault.address, + migration.address + ) + ).to.be.revertedWith("migration already approved"); + }); + it("should be able to unapprove migration vaults", async () => { + let tx = await migrationRegistry.unapprove( + initialVault.address, + targetVault.address, + migration.address + ); + await tx.wait(); + + // should revert to init resubscribe when unapproved + await expect( + meTokenRegistry + .connect(account1) + .initResubscribe( + meToken.address, + hubId2, + migration.address, + encodedMigrationArgs + ) + ).to.be.revertedWith("!approved"); + }); + it("should revert when try to unapprove already unapproved vaults", async () => { + await expect( + migrationRegistry.unapprove( + initialVault.address, + targetVault.address, + migration.address + ) + ).to.be.revertedWith("migration not approved"); + + // approve vaults again + const tx = await migrationRegistry.approve( + initialVault.address, + targetVault.address, + migration.address + ); + }); it("Set correct _ust values", async () => { await meTokenRegistry .connect(account1) diff --git a/test/integration/Hub/UpdateCurveDetails.ts b/test/integration/Hub/UpdateCurveDetails.ts index 89cbdb29..cc4b3eb5 100644 --- a/test/integration/Hub/UpdateCurveDetails.ts +++ b/test/integration/Hub/UpdateCurveDetails.ts @@ -307,7 +307,7 @@ const setup = async () => { toETHNumber(meTokenDetails.balancePooled), reserveWeight / MAX_WEIGHT ); - const targetassetsReturned = calculateCollateralReturned( + const targetAssetsReturned = calculateCollateralReturned( toETHNumber(balAfter), toETHNumber(meTokenTotalSupply), toETHNumber(meTokenDetails.balancePooled), @@ -337,7 +337,7 @@ const setup = async () => { (rawAssetsReturned * refundRatio.toNumber()) / MAX_WEIGHT; const calcWAvrgRes = weightedAverageSimulation( rawAssetsReturned, - targetassetsReturned, + targetAssetsReturned, startTime.toNumber(), endTime.toNumber(), block.timestamp @@ -389,7 +389,7 @@ const setup = async () => { toETHNumber(meTokenDetails.balancePooled), reserveWeight / MAX_WEIGHT ); - const targetassetsReturned = calculateCollateralReturned( + const targetAssetsReturned = calculateCollateralReturned( toETHNumber(metokenToBurn), toETHNumber(meTokenTotalSupply), toETHNumber(meTokenDetails.balancePooled), @@ -419,7 +419,7 @@ const setup = async () => { const calcWAvrgRes = weightedAverageSimulation( rawAssetsReturned, - targetassetsReturned, + targetAssetsReturned, startTime.toNumber(), endTime.toNumber(), block.timestamp @@ -475,7 +475,7 @@ const setup = async () => { toETHNumber(meTokenDetails.balancePooled), reserveWeight / MAX_WEIGHT ); - const targetassetsReturned = calculateCollateralReturned( + const targetAssetsReturned = calculateCollateralReturned( toETHNumber(metokenToBurn), toETHNumber(meTokenTotalSupply), toETHNumber(meTokenDetails.balancePooled), @@ -506,7 +506,7 @@ const setup = async () => { // the weighted average on the curve should be applied for owner and buyers const calcWAvrgRes = weightedAverageSimulation( rawAssetsReturned, - targetassetsReturned, + targetAssetsReturned, startTime.toNumber(), endTime.toNumber(), block.timestamp @@ -694,7 +694,7 @@ const setup = async () => { curve, targetCurve, } = await hub.getDetails(1); - const targetassetsReturned = calculateCollateralReturned( + const targetAssetsReturned = calculateCollateralReturned( toETHNumber(metokenToBurn), toETHNumber(meTokenTotalSupply), toETHNumber(meTokenDetails.balancePooled), @@ -735,7 +735,7 @@ const setup = async () => { // the weighted average on the curve should be applied for owner and buyers // but the owner gets a proportional share of the token burnt from the balanced locked const assetsReturned = - targetassetsReturned + + targetAssetsReturned + (toETHNumber(metokenToBurn) / toETHNumber(meTokenTotalSupply)) * toETHNumber(meTokenDetailsBeforeBurn.balanceLocked); @@ -808,7 +808,7 @@ const setup = async () => { curve, targetCurve, } = await hub.getDetails(1); - const targetassetsReturned = calculateCollateralReturned( + const targetAssetsReturned = calculateCollateralReturned( toETHNumber(metokenToBurn), toETHNumber(meTokenTotalSupply), toETHNumber(meTokenDetails.balancePooled), @@ -848,7 +848,7 @@ const setup = async () => { // as it is a buyer we apply the refund ratio const assetsReturned = - (targetassetsReturned * refundRatio.toNumber()) / MAX_WEIGHT; + (targetAssetsReturned * refundRatio.toNumber()) / MAX_WEIGHT; // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); @@ -1046,7 +1046,7 @@ const setup = async () => { toETHNumber(meTokenDetails.balancePooled), reserveWeight / MAX_WEIGHT ); - const targetassetsReturned = calculateCollateralReturned( + const targetAssetsReturned = calculateCollateralReturned( toETHNumber(balAfter), toETHNumber(meTokenTotalSupply), toETHNumber(meTokenDetails.balancePooled), @@ -1076,7 +1076,7 @@ const setup = async () => { (rawAssetsReturned * refundRatio.toNumber()) / MAX_WEIGHT; const calcWAvrgRes = weightedAverageSimulation( rawAssetsReturned, - targetassetsReturned, + targetAssetsReturned, startTime.toNumber(), endTime.toNumber(), block.timestamp @@ -1130,7 +1130,7 @@ const setup = async () => { toETHNumber(meTokenDetails.balancePooled), reserveWeight / MAX_WEIGHT ); - const targetassetsReturned = calculateCollateralReturned( + const targetAssetsReturned = calculateCollateralReturned( toETHNumber(metokenToBurn), toETHNumber(meTokenTotalSupply), toETHNumber(meTokenDetails.balancePooled), @@ -1160,7 +1160,7 @@ const setup = async () => { const calcWAvrgRes = weightedAverageSimulation( rawAssetsReturned, - targetassetsReturned, + targetAssetsReturned, startTime.toNumber(), endTime.toNumber(), block.timestamp @@ -1220,7 +1220,7 @@ const setup = async () => { toETHNumber(meTokenDetails.balancePooled), reserveWeight / MAX_WEIGHT ); - const targetassetsReturned = calculateCollateralReturned( + const targetAssetsReturned = calculateCollateralReturned( toETHNumber(metokenToBurn), toETHNumber(meTokenTotalSupply), toETHNumber(meTokenDetails.balancePooled), @@ -1252,7 +1252,7 @@ const setup = async () => { // the weighted average on the curve should be applied for owner and buyers const calcWAvrgRes = weightedAverageSimulation( rawAssetsReturned, - targetassetsReturned, + targetAssetsReturned, startTime.toNumber(), endTime.toNumber(), block.timestamp @@ -1431,7 +1431,7 @@ const setup = async () => { curve, targetCurve, } = await hub.getDetails(1); - const targetassetsReturned = calculateCollateralReturned( + const targetAssetsReturned = calculateCollateralReturned( toETHNumber(metokenToBurn), toETHNumber(meTokenTotalSupply), toETHNumber(meTokenDetails.balancePooled), @@ -1467,7 +1467,7 @@ const setup = async () => { // but the owner gets a proportional share of the token burnt from the balanced locked const assetsReturned = - targetassetsReturned + + targetAssetsReturned + (toETHNumber(metokenToBurn) / toETHNumber(meTokenTotalSupply)) * toETHNumber(meTokenDetailsBeforeBurn.balanceLocked); @@ -1544,7 +1544,7 @@ const setup = async () => { curve, targetCurve, } = await hub.getDetails(1); - const targetassetsReturned = calculateCollateralReturned( + const targetAssetsReturned = calculateCollateralReturned( toETHNumber(metokenToBurn), toETHNumber(meTokenTotalSupply), toETHNumber(meTokenDetails.balancePooled), @@ -1573,7 +1573,7 @@ const setup = async () => { // as it is a buyer we apply the refund ratio const assetsReturned = - (targetassetsReturned * refundRatio.toNumber()) / MAX_WEIGHT; + (targetAssetsReturned * refundRatio.toNumber()) / MAX_WEIGHT; // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); diff --git a/test/integration/MeTokenRegistry/ResubscribeCurveDetails.ts b/test/integration/MeTokenRegistry/ResubscribeCurveDetails.ts index 83090966..1372b7ee 100644 --- a/test/integration/MeTokenRegistry/ResubscribeCurveDetails.ts +++ b/test/integration/MeTokenRegistry/ResubscribeCurveDetails.ts @@ -1,21 +1,614 @@ -describe("MeToken Resubscribe - Same curve, new Curve Details", () => { - before(async () => {}); +import { ethers, getNamedAccounts } from "hardhat"; +import { hubSetup } from "../../utils/hubSetup"; +import { + calculateTokenReturned, + calculateCollateralReturned, + deploy, + getContractAt, + toETHNumber, + weightedAverageSimulation, + calculateTokenReturnedFromZero, +} from "../../utils/helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { BigNumber, ContractTransaction, Signer } from "ethers"; +import { CurveRegistry } from "../../../artifacts/types/CurveRegistry"; +import { ERC20 } from "../../../artifacts/types/ERC20"; +import { BancorABDK } from "../../../artifacts/types/BancorABDK"; +import { Foundry } from "../../../artifacts/types/Foundry"; +import { Hub } from "../../../artifacts/types/Hub"; +import { MeTokenRegistry } from "../../../artifacts/types/MeTokenRegistry"; +import { MigrationRegistry } from "../../../artifacts/types/MigrationRegistry"; +import { expect } from "chai"; +import { MeToken } from "../../../artifacts/types/MeToken"; +import { UniswapSingleTransferMigration } from "../../../artifacts/types/UniswapSingleTransferMigration"; +import { SingleAssetVault } from "../../../artifacts/types/SingleAssetVault"; +import { mineBlock, setAutomine } from "../../utils/hardhatNode"; - describe("Warmup", () => { - it("mint(): meTokens received based on initial Curve details", async () => {}); - it("burn() [buyer]: assets received based on initial Curve details", async () => {}); - it("burn() [owner]: assets received based on initial Curve details", async () => {}); - }); +const setup = async () => { + describe("MeToken Resubscribe - Same curve, new Curve Details", () => { + let tx: ContractTransaction; + let meTokenRegistry: MeTokenRegistry; + let bancorABDK: BancorABDK; + let migrationRegistry: MigrationRegistry; + let migration: UniswapSingleTransferMigration; + let singleAssetVault: SingleAssetVault; + let foundry: Foundry; + let hub: Hub; + let tokenHolder: Signer; + let dai: ERC20; + let weth: ERC20; + let daiWhale: Signer; + let meToken: MeToken; + let account0: SignerWithAddress; + let account1: SignerWithAddress; + let encodedCurveDetails1: string; + let encodedCurveDetails2: string; - describe("Duration", () => { - it("mint(): meTokens received based on weighted average curve details", async () => {}); - it("burn() [buyer]: assets received based on weighted average Curve details", async () => {}); - it("burn() [owner]: assets received based on weighted average Curve details", async () => {}); - }); + 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 baseY1 = PRECISION.div(1000); + const baseY2 = PRECISION.div(50); + const reserveWeight1 = MAX_WEIGHT / 10; + const reserveWeight2 = MAX_WEIGHT / 2; + const refundRatio = 5000; + const fees = 3000; + const tokenDepositedInETH = 100; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + + before(async () => { + let token: ERC20; + let DAI, WETH; + ({ DAI, WETH } = await getNamedAccounts()); + + const encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( + ["address"], + [DAI] + ); + encodedCurveDetails1 = ethers.utils.defaultAbiCoder.encode( + ["uint256", "uint32"], + [baseY1, reserveWeight1] + ); + encodedCurveDetails2 = ethers.utils.defaultAbiCoder.encode( + ["uint256", "uint32"], + [baseY2, reserveWeight2] + ); + const block = await ethers.provider.getBlock("latest"); + const earliestSwapTime = block.timestamp + 600 * 60; // 10h in future + const encodedMigrationArgs = ethers.utils.defaultAbiCoder.encode( + ["uint256", "uint24"], + [earliestSwapTime, fees] + ); + + // Register first and second hub + bancorABDK = await deploy("BancorABDK"); + ({ + token, + hub, + tokenHolder, + migrationRegistry, + singleAssetVault, + foundry, + account0, + account1, + meTokenRegistry, + } = await hubSetup( + encodedCurveDetails1, + encodedVaultArgs, + refundRatio, + bancorABDK + )); + dai = token; + weth = await getContractAt("ERC20", WETH); + daiWhale = tokenHolder; + + await hub.register( + account0.address, + WETH, + singleAssetVault.address, + bancorABDK.address, + refundRatio, + encodedCurveDetails2, + encodedVaultArgs + ); + + // set update/resubscribe times + await hub.setWarmup(hubWarmup); + await meTokenRegistry.setWarmup(warmup); + await meTokenRegistry.setDuration(duration); + await meTokenRegistry.setCooldown(coolDown); + + // Deploy uniswap migration and approve it to the registry + migration = await deploy( + "UniswapSingleTransferMigration", + undefined, + account0.address, + foundry.address, + hub.address, + meTokenRegistry.address, + migrationRegistry.address + ); + await migrationRegistry.approve( + singleAssetVault.address, + singleAssetVault.address, + migration.address + ); + + // Pre-load owner and buyer w/ DAI & WETH + await dai + .connect(daiWhale) + .transfer(account1.address, ethers.utils.parseEther("500")); + + await weth + .connect(tokenHolder) + .transfer(account1.address, ethers.utils.parseEther("500")); + + // Create meToken and subscribe to Hub1 + const name = "Carl meToken"; + const symbol = "CARL"; + await meTokenRegistry + .connect(account0) + .subscribe(name, symbol, hubId1, 0); + const meTokenAddr = await meTokenRegistry.getOwnerMeToken( + account0.address + ); + meToken = await getContractAt("MeToken", meTokenAddr); + + // initialize resubscription to hub 2 + tx = await meTokenRegistry + .connect(account0) + .initResubscribe( + meTokenAddr, + hubId2, + migration.address, + encodedMigrationArgs + ); + + const max = ethers.constants.MaxUint256; + 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); + }); + + describe("Warmup", () => { + before(async () => { + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const block = await ethers.provider.getBlock("latest"); + expect(metokenDetails.startTime).to.be.gt(block.timestamp); + }); + it("mint(): meTokens received based on initial Curve details", async () => { + const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); + const meTokenTotalSupplyBefore = await meToken.totalSupply(); + expect(meTokenTotalSupplyBefore).to.be.equal(0); + + const calculatedReturn = calculateTokenReturnedFromZero( + tokenDepositedInETH, + toETHNumber(baseY1), + reserveWeight1 / MAX_WEIGHT + ); + + await foundry + .connect(account1) + .mint(meToken.address, tokenDeposited, account0.address); + + const ownerMeTokenAfter = await meToken.balanceOf(account0.address); + const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + expect(toETHNumber(ownerMeTokenAfter)).to.be.approximately( + calculatedReturn, + 0.000000000000001 + ); + expect(meTokenTotalSupplyAfter).to.be.equal(ownerMeTokenAfter); + expect(vaultDAIAfter.sub(vaultDAIBefore)).to.equal(tokenDeposited); + }); + it("burn() [buyer]: assets received based on initial Curve details", async () => { + const ownerMeToken = await meToken.balanceOf(account0.address); + await meToken.transfer(account1.address, ownerMeToken.div(2)); + const buyerMeToken = await meToken.balanceOf(account1.address); + expect(buyerMeToken).to.be.equal(ownerMeToken.div(2)); + + const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const buyerDAIBefore = await dai.balanceOf(account1.address); + + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(buyerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight1 / MAX_WEIGHT + ); + const assetsReturned = (rawAssetsReturned * refundRatio) / MAX_WEIGHT; + + await foundry + .connect(account1) + .burn(meToken.address, buyerMeToken, account1.address); + + const buyerMeTokenAfter = await meToken.balanceOf(account1.address); + const buyerDAIAfter = await dai.balanceOf(account1.address); + const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + expect( + toETHNumber(buyerDAIAfter.sub(buyerDAIBefore)) + ).to.be.approximately(assetsReturned, 0.000000000000001); + expect(buyerMeTokenAfter).to.equal(0); + expect(toETHNumber(meTokenTotalSupplyAfter)).to.be.approximately( + toETHNumber(meTokenTotalSupply.div(2)), + 1e-18 + ); + expect(toETHNumber(vaultDAIBefore.sub(vaultDAIAfter))).to.approximately( + assetsReturned, + 0.000000000000001 + ); + }); + it("burn() [owner]: assets received based on initial Curve details", async () => { + const ownerMeToken = await meToken.balanceOf(account0.address); + const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const ownerDAIBefore = await dai.balanceOf(account0.address); + + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(ownerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight1 / MAX_WEIGHT + ); + const assetsReturned = + rawAssetsReturned + + (toETHNumber(ownerMeToken) / toETHNumber(meTokenTotalSupply)) * + toETHNumber(meTokenDetails.balanceLocked); + + await foundry + .connect(account0) + .burn(meToken.address, ownerMeToken, account0.address); + + const ownerMeTokenAfter = await meToken.balanceOf(account0.address); + const ownerDAIAfter = await dai.balanceOf(account0.address); + const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + expect(vaultDAIBefore.sub(vaultDAIAfter)).to.equal( + ownerDAIAfter.sub(ownerDAIBefore) + ); + expect( + toETHNumber(ownerDAIAfter.sub(ownerDAIBefore)) + ).to.be.approximately(assetsReturned, 0.000000000000001); + expect(ownerMeTokenAfter).to.equal(0); + expect(toETHNumber(meTokenTotalSupplyAfter)).to.equal(0); + }); + }); + + describe("Duration", () => { + before(async () => { + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + await mineBlock(metokenDetails.startTime.toNumber() + 2); + + const block = await ethers.provider.getBlock("latest"); + expect(metokenDetails.startTime).to.be.lt(block.timestamp); + }); + it("mint(): meTokens received based on weighted average curve details", async () => { + const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); + const migrationWETHBefore = await weth.balanceOf(migration.address); + const meTokenTotalSupplyBefore = await meToken.totalSupply(); + expect(meTokenTotalSupplyBefore).to.be.equal(0); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + + await setAutomine(false); + const block = await ethers.provider.getBlock("latest"); + + const calculatedReturn = calculateTokenReturnedFromZero( + tokenDepositedInETH, + toETHNumber(baseY1), + reserveWeight1 / MAX_WEIGHT + ); + const calculatedTargetReturn = calculateTokenReturnedFromZero( + tokenDepositedInETH, + toETHNumber(baseY2), + reserveWeight2 / MAX_WEIGHT + ); + + const calcWAvgRe = weightedAverageSimulation( + calculatedReturn, + calculatedTargetReturn, + meTokenDetails.startTime.toNumber(), + meTokenDetails.endTime.toNumber(), + block.timestamp + 1 + ); + + await foundry + .connect(account1) + .mint(meToken.address, tokenDeposited, account0.address); + + await mineBlock(block.timestamp + 1); + await setAutomine(true); - describe("Cooldown", () => { - it("mint(): assets received based on target Curve details", async () => {}); - it("burn() [buyer]: assets received based on target Curve details", async () => {}); - it("burn() [owner]: assets received based on target Curve details", async () => {}); + const ownerMeTokenAfter = await meToken.balanceOf(account0.address); + const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); + const migrationWETHAfter = await weth.balanceOf(migration.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + expect(toETHNumber(ownerMeTokenAfter)).to.be.approximately( + calcWAvgRe, + 0.000000000000001 + ); + expect(meTokenTotalSupplyAfter).to.be.equal(ownerMeTokenAfter); + expect(vaultDAIAfter.sub(vaultDAIBefore)).to.equal(0); // new asset goes to migration + expect(migrationWETHAfter.sub(migrationWETHBefore)).to.equal( + tokenDeposited + ); // new asset is WETH + }); + it("burn() [buyer]: assets received based on weighted average Curve details", async () => { + const ownerMeToken = await meToken.balanceOf(account0.address); + await meToken.transfer(account1.address, ownerMeToken.div(2)); + const buyerMeToken = await meToken.balanceOf(account1.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(account1.address); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + + await setAutomine(false); + const block = await ethers.provider.getBlock("latest"); + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(buyerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight1 / MAX_WEIGHT + ); + const targetAssetsReturned = calculateCollateralReturned( + toETHNumber(buyerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight2 / MAX_WEIGHT + ); + + await foundry + .connect(account1) + .burn(meToken.address, buyerMeToken, account1.address); + + const calcWAvgRes = weightedAverageSimulation( + rawAssetsReturned, + targetAssetsReturned, + meTokenDetails.startTime.toNumber(), + meTokenDetails.endTime.toNumber(), + block.timestamp + 1 + ); + + const assetsReturned = (calcWAvgRes * refundRatio) / MAX_WEIGHT; + + await mineBlock(block.timestamp + 1); + await setAutomine(true); + + const buyerMeTokenAfter = await meToken.balanceOf(account1.address); + const buyerWETHAfter = await weth.balanceOf(account1.address); + const migrationWETHAfter = await weth.balanceOf(migration.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + expect( + toETHNumber(buyerWETHAfter.sub(buyerWETHBefore)) + ).to.be.approximately(assetsReturned, 1e-15); + expect(buyerMeTokenAfter).to.equal(0); + expect(toETHNumber(meTokenTotalSupplyAfter)).to.be.approximately( + toETHNumber(meTokenTotalSupply.div(2)), + 1e-18 + ); + expect( + toETHNumber(migrationWETHBefore.sub(migrationWETHAfter)) + ).to.approximately(assetsReturned, 1e-15); + }); + it("burn() [owner]: assets received based on weighted average Curve details", async () => { + const ownerMeToken = await meToken.balanceOf(account0.address); + const migrationWETHBefore = await weth.balanceOf(migration.address); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const ownerWETHBefore = await weth.balanceOf(account0.address); + + await setAutomine(false); + const block = await ethers.provider.getBlock("latest"); + + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(ownerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight1 / MAX_WEIGHT + ); + const targetAssetsReturned = calculateCollateralReturned( + toETHNumber(ownerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight2 / MAX_WEIGHT + ); + + const calcWAvgRes = weightedAverageSimulation( + rawAssetsReturned, + targetAssetsReturned, + meTokenDetails.startTime.toNumber(), + meTokenDetails.endTime.toNumber(), + block.timestamp + 1 + ); + const assetsReturned = + calcWAvgRes + + (toETHNumber(ownerMeToken) / toETHNumber(meTokenTotalSupply)) * + toETHNumber(meTokenDetails.balanceLocked); + + await mineBlock(block.timestamp + 1); + await setAutomine(true); + await foundry + .connect(account0) + .burn(meToken.address, ownerMeToken, account0.address); + + const ownerMeTokenAfter = await meToken.balanceOf(account0.address); + const ownerWETHAfter = await weth.balanceOf(account0.address); + const migrationWETHAfter = await weth.balanceOf(migration.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + expect(migrationWETHBefore.sub(migrationWETHAfter)).to.equal( + ownerWETHAfter.sub(ownerWETHBefore) + ); + expect( + toETHNumber(ownerWETHAfter.sub(ownerWETHBefore)) + ).to.be.approximately(assetsReturned, 0.000000000000001); + expect(ownerMeTokenAfter).to.equal(0); + expect(toETHNumber(meTokenTotalSupplyAfter)).to.equal(0); + }); + }); + + describe("Cooldown", () => { + before(async () => { + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + await mineBlock(metokenDetails.endTime.toNumber() + 2); + + const block = await ethers.provider.getBlock("latest"); + expect(metokenDetails.endTime).to.be.lt(block.timestamp); + }); + it("mint(): assets received based on target Curve details", async () => { + const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); + const migrationWETHBefore = await weth.balanceOf(migration.address); + const meTokenTotalSupplyBefore = await meToken.totalSupply(); + expect(meTokenTotalSupplyBefore).to.be.equal(0); + + const calculatedTargetReturn = calculateTokenReturnedFromZero( + tokenDepositedInETH, + toETHNumber(baseY2), + reserveWeight2 / MAX_WEIGHT + ); + + await foundry + .connect(account1) + .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 meTokenTotalSupplyAfter = await meToken.totalSupply(); + + expect(toETHNumber(ownerMeTokenAfter)).to.be.approximately( + calculatedTargetReturn, + 0.000000000000001 + ); + expect(meTokenTotalSupplyAfter).to.be.equal(ownerMeTokenAfter); + expect(vaultWETHAfter.sub(vaultWETHBefore)).to.equal(tokenDeposited); + expect(migrationWETHAfter.sub(migrationWETHBefore)).to.equal(0); + }); + it("burn() [buyer]: assets received based on target Curve details", async () => { + const ownerMeToken = await meToken.balanceOf(account0.address); + await meToken.transfer(account1.address, ownerMeToken.div(2)); + const buyerMeToken = await meToken.balanceOf(account1.address); + expect(buyerMeToken).to.be.equal(ownerMeToken.div(2)); + + const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); + const meTokenTotalSupply = await meToken.totalSupply(); + const buyerWETHBefore = await weth.balanceOf(account1.address); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + + const targetAssetsReturned = calculateCollateralReturned( + toETHNumber(buyerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight2 / MAX_WEIGHT + ); + + await foundry + .connect(account1) + .burn(meToken.address, buyerMeToken, account1.address); + + const assetsReturned = + (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 meTokenTotalSupplyAfter = await meToken.totalSupply(); + + expect( + toETHNumber(buyerWETHAfter.sub(buyerWETHBefore)) + ).to.be.approximately(assetsReturned, 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(assetsReturned, 1e-15); + }); + it("burn() [owner]: assets received based on target Curve details", async () => { + const ownerMeToken = await meToken.balanceOf(account0.address); + const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const ownerWETHBefore = await weth.balanceOf(account0.address); + + const targetAssetsReturned = calculateCollateralReturned( + toETHNumber(ownerMeToken), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight2 / MAX_WEIGHT + ); + + await foundry + .connect(account0) + .burn(meToken.address, ownerMeToken, account0.address); + + const assetsReturned = + targetAssetsReturned + + (toETHNumber(ownerMeToken) / toETHNumber(meTokenTotalSupply)) * + toETHNumber(meTokenDetails.balanceLocked); + + const ownerMeTokenAfter = await meToken.balanceOf(account0.address); + const ownerWETHAfter = await weth.balanceOf(account0.address); + const vaultWETHAfter = await dai.balanceOf(singleAssetVault.address); + const meTokenTotalSupplyAfter = await meToken.totalSupply(); + + expect(vaultWETHBefore.sub(vaultWETHAfter)).to.equal( + ownerWETHAfter.sub(ownerWETHBefore) + ); + expect( + toETHNumber(ownerWETHAfter.sub(ownerWETHBefore)) + ).to.be.approximately(assetsReturned, 0.000000000000001); + expect(ownerMeTokenAfter).to.equal(0); + expect(toETHNumber(meTokenTotalSupplyAfter)).to.equal(0); + }); + }); }); +}; + +setup().then(() => { + run(); }); diff --git a/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts b/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts index 3e6095f2..16d42a0e 100644 --- a/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts +++ b/test/integration/MeTokenRegistry/ResubscribeRefundRatio.ts @@ -19,445 +19,472 @@ import { SingleAssetVault } from "../../../artifacts/types/SingleAssetVault"; import { mineBlock } from "../../utils/hardhatNode"; import { UniswapSingleTransferMigration } from "../../../artifacts/types/UniswapSingleTransferMigration"; -describe("MeToken Resubscribe - new RefundRatio", () => { - let meTokenRegistry: MeTokenRegistry; - let bancorABDK: BancorABDK; - let migrationRegistry: MigrationRegistry; - let singleAssetVault: SingleAssetVault; - let foundry: Foundry; - let hub: Hub; - let dai: ERC20; - let weth: ERC20; - let meToken: MeToken; - let tokenHolder: Signer; - let account0: SignerWithAddress; - let account1: SignerWithAddress; - let account2: SignerWithAddress; - let migration: UniswapSingleTransferMigration; - - const one = ethers.utils.parseEther("1"); - let baseY: BigNumber; - const MAX_WEIGHT = 1000000; - let encodedCurveDetails: string; - let encodedVaultArgs: string; - const firstHubId = 1; - const initialRefundRatio = ethers.utils.parseUnits("5000", 0); // 0.005% - const targetRefundRatio = ethers.utils.parseUnits("500000", 0); // 50% - const fees = 3000; - - let tokenDepositedInETH; - let tokenDeposited: BigNumber; - - before(async () => { - baseY = one.mul(1000); - const reserveWeight = MAX_WEIGHT / 2; - let DAI; - let WETH; - ({ DAI, WETH } = await getNamedAccounts()); - - encodedCurveDetails = ethers.utils.defaultAbiCoder.encode( - ["uint256", "uint32"], - [baseY, reserveWeight] - ); - encodedVaultArgs = ethers.utils.defaultAbiCoder.encode(["address"], [DAI]); - bancorABDK = await deploy("BancorABDK"); - - ({ - token: dai, - hub, - tokenHolder, - migrationRegistry, - singleAssetVault, - foundry, - account0, - account1, - account2, - meTokenRegistry, - } = await hubSetup( - encodedCurveDetails, - encodedVaultArgs, - initialRefundRatio.toNumber(), - bancorABDK - )); - - // Deploy uniswap migration and approve it to the registry - migration = await deploy( - "UniswapSingleTransferMigration", - undefined, - account0.address, - foundry.address, - hub.address, - meTokenRegistry.address, - migrationRegistry.address - ); - await migrationRegistry.approve( - singleAssetVault.address, - singleAssetVault.address, - migration.address - ); - - weth = await getContractAt("ERC20", WETH); - - // Pre-load owner and buyer w/ DAI - await dai - .connect(tokenHolder) - .transfer(account2.address, ethers.utils.parseEther("1000")); - - await weth - .connect(tokenHolder) - .transfer(account2.address, ethers.utils.parseEther("1000")); - - // Create meToken and subscribe to Hub1 - await meTokenRegistry - .connect(account0) - .subscribe("Carl meToken", "CARL", firstHubId, 0); - const meTokenAddr = await meTokenRegistry.getOwnerMeToken(account0.address); - meToken = await getContractAt("MeToken", meTokenAddr); - - // Create Hub2 w/ same args but different refund Ratio - await hub.register( - account0.address, - WETH, - singleAssetVault.address, - bancorABDK.address, - targetRefundRatio, - encodedCurveDetails, - encodedVaultArgs - ); - - await hub.setWarmup(7 * 60 * 24 * 24); // 1 week - await meTokenRegistry.setWarmup(2 * 60 * 24 * 24); // 2 days - await meTokenRegistry.setDuration(4 * 60 * 24 * 24); // 4 days - await meTokenRegistry.setCooldown(5 * 60 * 24 * 24); // 5 days - - const block = await ethers.provider.getBlock("latest"); - const earliestSwapTime = block.timestamp + 600 * 60; // 10h in future - const encodedMigrationArgs = ethers.utils.defaultAbiCoder.encode( - ["uint256", "uint24"], - [earliestSwapTime, fees] - ); - - await meTokenRegistry.initResubscribe( - meToken.address, - 2, - migration.address, - encodedMigrationArgs - ); - tokenDepositedInETH = 100; - tokenDeposited = ethers.utils.parseEther(tokenDepositedInETH.toString()); - - await dai - .connect(account2) - .approve(singleAssetVault.address, ethers.constants.MaxUint256); - await dai - .connect(account2) - .approve(migration.address, ethers.constants.MaxUint256); - await weth - .connect(account2) - .approve(singleAssetVault.address, ethers.constants.MaxUint256); - await weth - .connect(account2) - .approve(migration.address, ethers.constants.MaxUint256); - }); - - describe("Warmup", () => { - before(async () => { - const metokenDetails = await meTokenRegistry.getDetails(meToken.address); - const block = await ethers.provider.getBlock("latest"); - expect(metokenDetails.startTime).to.be.gt(block.timestamp); - }); - it("burn() [owner]: assets received do not apply refundRatio", async () => { - await foundry - .connect(account2) - .mint(meToken.address, tokenDeposited, account0.address); - - const ownerMeTokenBefore = await meToken.balanceOf(account0.address); - const ownerDAIBefore = await dai.balanceOf(account0.address); - - await foundry - .connect(account0) - .burn(meToken.address, ownerMeTokenBefore, account0.address); - - const totalSupply = await meToken.totalSupply(); - const metokenDetails = await meTokenRegistry.getDetails(meToken.address); - - const ownerMeTokenAfter = await meToken.balanceOf(account0.address); - const ownerDAIAfter = await dai.balanceOf(account0.address); - const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); - - expect(totalSupply).to.equal(0); - expect(ownerMeTokenAfter).to.equal(0); - expect(ownerDAIAfter.sub(ownerDAIBefore)).to.equal(tokenDeposited); - expect(vaultDAIAfter).to.equal(0); - expect(metokenDetails.balancePooled).to.equal(0); - expect(metokenDetails.balanceLocked).to.equal(0); - }); - it("burn() [buyer]: assets received based on initial refundRatio", async () => { - await foundry - .connect(account2) - .mint(meToken.address, tokenDeposited, account1.address); - - const buyerMeTokenBefore = await meToken.balanceOf(account1.address); - const buyerDAIBefore = await dai.balanceOf(account1.address); - const ownerDAIBefore = await dai.balanceOf(account0.address); - const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); - - await foundry - .connect(account1) // non owner - .burn(meToken.address, buyerMeTokenBefore, account1.address); - - const totalSupply = await meToken.totalSupply(); - const buyerMeTokenAfter = await meToken.balanceOf(account1.address); - const buyerDAIAfter = await dai.balanceOf(account1.address); - const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); - const metokenDetails = await meTokenRegistry.getDetails(meToken.address); - - const refundAmount = tokenDeposited.mul(initialRefundRatio).div(1e6); - - expect(totalSupply).to.equal(0); - expect(buyerMeTokenAfter).to.equal(0); - expect(buyerDAIAfter.sub(buyerDAIBefore)).to.equal(refundAmount); - expect(vaultDAIBefore.sub(vaultDAIAfter)).to.equal(refundAmount); - expect(metokenDetails.balancePooled).to.equal(0); - expect(metokenDetails.balanceLocked).to.gt(0); // due to refund ratio - }); - }); +const setup = async () => { + describe("MeToken Resubscribe - new RefundRatio", () => { + let meTokenRegistry: MeTokenRegistry; + let bancorABDK: BancorABDK; + let migrationRegistry: MigrationRegistry; + let singleAssetVault: SingleAssetVault; + let foundry: Foundry; + let hub: Hub; + let dai: ERC20; + let weth: ERC20; + let meToken: MeToken; + let tokenHolder: Signer; + let account0: SignerWithAddress; + let account1: SignerWithAddress; + let account2: SignerWithAddress; + let migration: UniswapSingleTransferMigration; + + const one = ethers.utils.parseEther("1"); + let baseY: BigNumber; + const MAX_WEIGHT = 1000000; + let encodedCurveDetails: string; + let encodedVaultArgs: string; + const firstHubId = 1; + const initialRefundRatio = ethers.utils.parseUnits("5000", 0); // 0.005% + const targetRefundRatio = ethers.utils.parseUnits("500000", 0); // 50% + const fees = 3000; + + let tokenDepositedInETH; + let tokenDeposited: BigNumber; - describe("Duration", () => { before(async () => { - const metokenDetails = await meTokenRegistry.getDetails(meToken.address); - await mineBlock(metokenDetails.startTime.toNumber() + 2); - - const block = await ethers.provider.getBlock("latest"); - expect(metokenDetails.startTime).to.be.lt(block.timestamp); - }); - it("burn() [owner]: assets received do not apply refundRatio", async () => { - const vaultDAIBeforeMint = await dai.balanceOf(singleAssetVault.address); - const tx = await foundry - .connect(account2) - .mint(meToken.address, tokenDeposited, account0.address); - - await tx.wait(); - - await expect(tx).to.emit(meTokenRegistry, "UpdateBalances"); + baseY = one.mul(1000); + const reserveWeight = MAX_WEIGHT / 2; + let DAI; + let WETH; + ({ DAI, WETH } = await getNamedAccounts()); + + encodedCurveDetails = ethers.utils.defaultAbiCoder.encode( + ["uint256", "uint32"], + [baseY, reserveWeight] + ); + encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( + ["address"], + [DAI] + ); + bancorABDK = await deploy("BancorABDK"); + + ({ + token: dai, + hub, + tokenHolder, + migrationRegistry, + singleAssetVault, + foundry, + account0, + account1, + account2, + meTokenRegistry, + } = await hubSetup( + encodedCurveDetails, + encodedVaultArgs, + initialRefundRatio.toNumber(), + bancorABDK + )); + + // Deploy uniswap migration and approve it to the registry + migration = await deploy( + "UniswapSingleTransferMigration", + undefined, + account0.address, + foundry.address, + hub.address, + meTokenRegistry.address, + migrationRegistry.address + ); + await migrationRegistry.approve( + singleAssetVault.address, + singleAssetVault.address, + migration.address + ); - const ownerMeTokenBefore = await meToken.balanceOf(account0.address); - const ownerDAIBefore = await dai.balanceOf(account0.address); - const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); - const ownerWETHBefore = await weth.balanceOf(account0.address); - const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); - const migrationDAIBefore = await dai.balanceOf(migration.address); - const migrationWETHBefore = await weth.balanceOf(migration.address); + weth = await getContractAt("ERC20", WETH); + // Pre-load owner and buyer w/ DAI + await dai + .connect(tokenHolder) + .transfer(account2.address, ethers.utils.parseEther("500")); - 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 + await weth + .connect(tokenHolder) + .transfer(account2.address, ethers.utils.parseEther("500")); - await foundry + // Create meToken and subscribe to Hub1 + await meTokenRegistry .connect(account0) - .burn(meToken.address, ownerMeTokenBefore, account0.address); - - const totalSupply = await meToken.totalSupply(); - const ownerMeTokenAfter = await meToken.balanceOf(account0.address); - const ownerDAIAfter = await dai.balanceOf(account0.address); - const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); - const ownerWETHAfter = await weth.balanceOf(account0.address); - const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); - const migrationDAIAfter = await dai.balanceOf(migration.address); - const migrationWETHAfter = await weth.balanceOf(migration.address); - const metokenDetails = await meTokenRegistry.getDetails(meToken.address); - - expect(totalSupply).to.equal(0); - expect(ownerMeTokenAfter).to.equal(0); // as all tokens are burned - expect(ownerDAIAfter).to.equal(ownerDAIBefore); // as owner receives new fund in weth - 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(metokenDetails.balancePooled).to.equal(0); - expect(metokenDetails.balanceLocked).to.equal(0); - }); - it("burn() [buyer]: assets received based on weighted average refundRatio", async () => { - const tx = await foundry - .connect(account2) - .mint(meToken.address, tokenDeposited, account1.address); - - await expect(tx).to.not.emit(meTokenRegistry, "UpdateBalances"); - - const buyerMeTokenBefore = await meToken.balanceOf(account1.address); - const buyerDAIBefore = await dai.balanceOf(account1.address); - const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); - const buyerWETHBefore = await weth.balanceOf(account1.address); - const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); - const migrationDAIBefore = await dai.balanceOf(migration.address); - const migrationWETHBefore = await weth.balanceOf(migration.address); - - expect(migrationWETHBefore).to.equal(tokenDeposited); - - await foundry - .connect(account1) // non owner - .burn(meToken.address, buyerMeTokenBefore, account1.address); - - const { startTime, endTime, targetHubId } = - await meTokenRegistry.getDetails(meToken.address); - const { refundRatio: targetRefundRatio } = await hub.getDetails( - targetHubId + .subscribe("Carl meToken", "CARL", firstHubId, 0); + const meTokenAddr = await meTokenRegistry.getOwnerMeToken( + account0.address ); - const block = await ethers.provider.getBlock("latest"); - const calculatedWeightedAvg = weightedAverageSimulation( - initialRefundRatio.toNumber(), - targetRefundRatio.toNumber(), - startTime.toNumber(), - endTime.toNumber(), - block.timestamp + meToken = await getContractAt("MeToken", meTokenAddr); + // Create Hub2 w/ same args but different refund Ratio + await hub.register( + account0.address, + WETH, + singleAssetVault.address, + bancorABDK.address, + targetRefundRatio, + encodedCurveDetails, + encodedVaultArgs ); + await hub.setWarmup(7 * 60 * 24 * 24); // 1 week + await meTokenRegistry.setWarmup(2 * 60 * 24 * 24); // 2 days + await meTokenRegistry.setDuration(4 * 60 * 24 * 24); // 4 days + await meTokenRegistry.setCooldown(5 * 60 * 24 * 24); // 5 days - const buyerMeTokenAfter = await meToken.balanceOf(account1.address); - const buyerDAIAfter = await dai.balanceOf(account1.address); - const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); - const buyerWETHAfter = await weth.balanceOf(account1.address); - const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); - const migrationDAIAfter = await dai.balanceOf(migration.address); - const migrationWETHAfter = await weth.balanceOf(migration.address); - const metokenDetails = await meTokenRegistry.getDetails(meToken.address); - const totalSupply = await meToken.totalSupply(); - - const refundAmount = tokenDeposited - .mul(Math.floor(calculatedWeightedAvg)) - .div(1e6); - - expect(totalSupply).to.equal(0); - expect(buyerMeTokenAfter).to.equal(0); // as all tokens are burned - expect(buyerDAIAfter).to.equal(buyerDAIBefore); // as buyer receives new fund in weth - 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(metokenDetails.balancePooled).to.equal(0); - expect(metokenDetails.balanceLocked).to.equal( - tokenDeposited.sub(refundAmount) + const block = await ethers.provider.getBlock("latest"); + const earliestSwapTime = block.timestamp + 600 * 60; // 10h in future + const encodedMigrationArgs = ethers.utils.defaultAbiCoder.encode( + ["uint256", "uint24"], + [earliestSwapTime, fees] ); - expect(buyerWETHAfter.sub(buyerWETHBefore)).to.equal(refundAmount); - expect(migrationWETHBefore.sub(migrationWETHAfter)).to.equal( - refundAmount + await meTokenRegistry.initResubscribe( + meToken.address, + 2, + migration.address, + encodedMigrationArgs ); + tokenDepositedInETH = 100; + tokenDeposited = ethers.utils.parseEther(tokenDepositedInETH.toString()); + await dai + .connect(account2) + .approve(singleAssetVault.address, ethers.constants.MaxUint256); + await dai + .connect(account2) + .approve(migration.address, ethers.constants.MaxUint256); + await weth + .connect(account2) + .approve(singleAssetVault.address, ethers.constants.MaxUint256); + await weth + .connect(account2) + .approve(migration.address, ethers.constants.MaxUint256); }); - }); - describe("Cooldown", () => { - before(async () => { - const metokenDetails = await meTokenRegistry.getDetails(meToken.address); - await mineBlock(metokenDetails.endTime.toNumber() + 2); - - const block = await ethers.provider.getBlock("latest"); - expect(metokenDetails.endTime).to.be.lt(block.timestamp); + describe("Warmup", () => { + before(async () => { + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const block = await ethers.provider.getBlock("latest"); + expect(metokenDetails.startTime).to.be.gt(block.timestamp); + }); + it("burn() [owner]: assets received do not apply refundRatio", async () => { + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account0.address); + + const ownerMeTokenBefore = await meToken.balanceOf(account0.address); + const ownerDAIBefore = await dai.balanceOf(account0.address); + + await foundry + .connect(account0) + .burn(meToken.address, ownerMeTokenBefore, account0.address); + + const totalSupply = await meToken.totalSupply(); + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + + const ownerMeTokenAfter = await meToken.balanceOf(account0.address); + const ownerDAIAfter = await dai.balanceOf(account0.address); + const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); + + expect(totalSupply).to.equal(0); + expect(ownerMeTokenAfter).to.equal(0); + expect(ownerDAIAfter.sub(ownerDAIBefore)).to.equal(tokenDeposited); + expect(vaultDAIAfter).to.equal(0); + expect(metokenDetails.balancePooled).to.equal(0); + expect(metokenDetails.balanceLocked).to.equal(0); + }); + it("burn() [buyer]: assets received based on initial refundRatio", async () => { + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account1.address); + + const buyerMeTokenBefore = await meToken.balanceOf(account1.address); + const buyerDAIBefore = await dai.balanceOf(account1.address); + const ownerDAIBefore = await dai.balanceOf(account0.address); + const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); + + await foundry + .connect(account1) // non owner + .burn(meToken.address, buyerMeTokenBefore, account1.address); + + const totalSupply = await meToken.totalSupply(); + const buyerMeTokenAfter = await meToken.balanceOf(account1.address); + const buyerDAIAfter = await dai.balanceOf(account1.address); + const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + + const refundAmount = tokenDeposited.mul(initialRefundRatio).div(1e6); + + expect(totalSupply).to.equal(0); + expect(buyerMeTokenAfter).to.equal(0); + expect(buyerDAIAfter.sub(buyerDAIBefore)).to.equal(refundAmount); + expect(vaultDAIBefore.sub(vaultDAIAfter)).to.equal(refundAmount); + expect(metokenDetails.balancePooled).to.equal(0); + expect(metokenDetails.balanceLocked).to.gt(0); // due to refund ratio + }); }); - it("burn() [owner]: assets received do not apply refundRatio", async () => { - const migrationWETHBeforeMint = await weth.balanceOf(migration.address); - - const tx = await foundry - .connect(account2) - .mint(meToken.address, tokenDeposited, account0.address); - - await tx.wait(); - - await expect(tx).to.not.emit(meTokenRegistry, "UpdateBalances"); - await expect(tx).to.emit(meTokenRegistry, "FinishResubscribe"); - const ownerMeTokenBefore = await meToken.balanceOf(account0.address); - const ownerDAIBefore = await dai.balanceOf(account0.address); - const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); - const ownerWETHBefore = await weth.balanceOf(account0.address); - const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); - const migrationDAIBefore = await dai.balanceOf(migration.address); - const migrationWETHBefore = await weth.balanceOf(migration.address); - - expect(migrationWETHBeforeMint).to.be.gt(0); // due to refund ration from last burn - expect(vaultWETHBefore).to.equal( - tokenDeposited.add(migrationWETHBeforeMint) - ); - expect(migrationWETHBefore).to.equal(0); // as all funds are transferred to vault - - await foundry - .connect(account0) - .burn(meToken.address, ownerMeTokenBefore, account0.address); - - const totalSupply = await meToken.totalSupply(); - const ownerMeTokenAfter = await meToken.balanceOf(account0.address); - const ownerDAIAfter = await dai.balanceOf(account0.address); - const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); - const ownerWETHAfter = await weth.balanceOf(account0.address); - const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); - const migrationDAIAfter = await dai.balanceOf(migration.address); - const migrationWETHAfter = await weth.balanceOf(migration.address); - const metokenDetails = await meTokenRegistry.getDetails(meToken.address); - - expect(totalSupply).to.equal(0); - expect(ownerMeTokenAfter).to.equal(0); // as all tokens are burned - expect(ownerDAIAfter).to.equal(ownerDAIBefore); // as owner receives new fund in weth - expect(vaultDAIBefore).to.equal(vaultDAIAfter); // as vault receives new fund in weth - expect(migrationDAIBefore).to.equal(migrationDAIAfter); // as migration receives no funds - expect(migrationWETHAfter).to.equal(migrationWETHBefore); // as migration receives no funds - - expect(vaultWETHAfter).to.equal(0); // as all token deposited goes to owner incl migration - expect(ownerWETHAfter.sub(ownerWETHBefore)).to.equal(vaultWETHBefore); // as all token deposited goes to owner plus migration - expect(metokenDetails.balancePooled).to.equal(0); - expect(metokenDetails.balanceLocked).to.equal(0); + describe("Duration", () => { + before(async () => { + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + await mineBlock(metokenDetails.startTime.toNumber() + 2); + + const block = await ethers.provider.getBlock("latest"); + expect(metokenDetails.startTime).to.be.lt(block.timestamp); + }); + it("burn() [owner]: assets received do not apply refundRatio", async () => { + const vaultDAIBeforeMint = await dai.balanceOf( + singleAssetVault.address + ); + const tx = await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account0.address); + + await tx.wait(); + + await expect(tx).to.emit(meTokenRegistry, "UpdateBalances"); + + const ownerMeTokenBefore = await meToken.balanceOf(account0.address); + const ownerDAIBefore = await dai.balanceOf(account0.address); + const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); + const ownerWETHBefore = await weth.balanceOf(account0.address); + const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); + const migrationDAIBefore = await dai.balanceOf(migration.address); + const migrationWETHBefore = await weth.balanceOf(migration.address); + + 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 + + await foundry + .connect(account0) + .burn(meToken.address, ownerMeTokenBefore, account0.address); + + const totalSupply = await meToken.totalSupply(); + const ownerMeTokenAfter = await meToken.balanceOf(account0.address); + const ownerDAIAfter = await dai.balanceOf(account0.address); + const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); + const ownerWETHAfter = await weth.balanceOf(account0.address); + const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); + const migrationDAIAfter = await dai.balanceOf(migration.address); + const migrationWETHAfter = await weth.balanceOf(migration.address); + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + + expect(totalSupply).to.equal(0); + expect(ownerMeTokenAfter).to.equal(0); // as all tokens are burned + expect(ownerDAIAfter).to.equal(ownerDAIBefore); // as owner receives new fund in weth + 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(metokenDetails.balancePooled).to.equal(0); + expect(metokenDetails.balanceLocked).to.equal(0); + }); + it("burn() [buyer]: assets received based on weighted average refundRatio", async () => { + const tx = await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account1.address); + + await expect(tx).to.not.emit(meTokenRegistry, "UpdateBalances"); + + const buyerMeTokenBefore = await meToken.balanceOf(account1.address); + const buyerDAIBefore = await dai.balanceOf(account1.address); + const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); + const buyerWETHBefore = await weth.balanceOf(account1.address); + const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); + const migrationDAIBefore = await dai.balanceOf(migration.address); + const migrationWETHBefore = await weth.balanceOf(migration.address); + + expect(migrationWETHBefore).to.equal(tokenDeposited); + + await foundry + .connect(account1) // non owner + .burn(meToken.address, buyerMeTokenBefore, account1.address); + + const { startTime, endTime, targetHubId } = + await meTokenRegistry.getDetails(meToken.address); + const { refundRatio: targetRefundRatio } = await hub.getDetails( + targetHubId + ); + const block = await ethers.provider.getBlock("latest"); + const calculatedWeightedAvg = weightedAverageSimulation( + initialRefundRatio.toNumber(), + targetRefundRatio.toNumber(), + startTime.toNumber(), + endTime.toNumber(), + block.timestamp + ); + + const buyerMeTokenAfter = await meToken.balanceOf(account1.address); + const buyerDAIAfter = await dai.balanceOf(account1.address); + const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); + const buyerWETHAfter = await weth.balanceOf(account1.address); + const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); + const migrationDAIAfter = await dai.balanceOf(migration.address); + const migrationWETHAfter = await weth.balanceOf(migration.address); + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const totalSupply = await meToken.totalSupply(); + + const refundAmount = tokenDeposited + .mul(Math.floor(calculatedWeightedAvg)) + .div(1e6); + + expect(totalSupply).to.equal(0); + expect(buyerMeTokenAfter).to.equal(0); // as all tokens are burned + expect(buyerDAIAfter).to.equal(buyerDAIBefore); // as buyer receives new fund in weth + 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(metokenDetails.balancePooled).to.equal(0); + expect(metokenDetails.balanceLocked).to.equal( + tokenDeposited.sub(refundAmount) + ); + expect(buyerWETHAfter.sub(buyerWETHBefore)).to.equal(refundAmount); + expect(migrationWETHBefore.sub(migrationWETHAfter)).to.equal( + refundAmount + ); + }); }); - it("burn() [buyer]: assets received based on targetRefundRatio", async () => { - const vaultWETHBeforeMint = await weth.balanceOf( - singleAssetVault.address - ); - const tx = await foundry - .connect(account2) - .mint(meToken.address, tokenDeposited, account1.address); - - await tx.wait(); - - await expect(tx).to.not.emit(meTokenRegistry, "UpdateBalances"); - await expect(tx).to.not.emit(meTokenRegistry, "FinishResubscribe"); - - const buyerMeTokenBefore = await meToken.balanceOf(account1.address); - const buyerDAIBefore = await dai.balanceOf(account1.address); - const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); - const buyerWETHBefore = await weth.balanceOf(account1.address); - const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); - const migrationDAIBefore = await dai.balanceOf(migration.address); - const migrationWETHBefore = await weth.balanceOf(migration.address); - - expect(vaultWETHBeforeMint).to.equal(0); - expect(vaultWETHBefore).to.equal(tokenDeposited); - - await foundry - .connect(account1) - .burn(meToken.address, buyerMeTokenBefore, account1.address); - - const totalSupply = await meToken.totalSupply(); - const buyerMeTokenAfter = await meToken.balanceOf(account1.address); - const buyerDAIAfter = await dai.balanceOf(account1.address); - const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); - const buyerWETHAfter = await weth.balanceOf(account1.address); - const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); - const migrationDAIAfter = await dai.balanceOf(migration.address); - const migrationWETHAfter = await weth.balanceOf(migration.address); - const metokenDetails = await meTokenRegistry.getDetails(meToken.address); - - const refundAmount = tokenDeposited.mul(targetRefundRatio).div(1e6); - - expect(totalSupply).to.equal(0); - expect(buyerMeTokenAfter).to.equal(0); // as all tokens are burned - expect(buyerDAIAfter).to.equal(buyerDAIBefore); // as buyer receives new fund in weth - expect(vaultDAIBefore).to.equal(vaultDAIAfter); // as vault receives new fund in weth - expect(migrationDAIBefore).to.equal(migrationDAIAfter); // as migration receives no funds - expect(migrationWETHAfter).to.equal(migrationWETHBefore); // as migration receives no funds - expect(vaultWETHAfter).to.equal(tokenDeposited.sub(refundAmount)); // refund ration token remains in vault - expect(buyerWETHAfter.sub(buyerWETHBefore)).to.equal(refundAmount); // buyer only receives refund ratio - expect(metokenDetails.balancePooled).to.equal(0); - expect(metokenDetails.balanceLocked).to.equal( - tokenDeposited.sub(refundAmount) - ); + describe("Cooldown", () => { + before(async () => { + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + await mineBlock(metokenDetails.endTime.toNumber() + 2); + + const block = await ethers.provider.getBlock("latest"); + expect(metokenDetails.endTime).to.be.lt(block.timestamp); + }); + it("burn() [owner]: assets received do not apply refundRatio", async () => { + const migrationWETHBeforeMint = await weth.balanceOf(migration.address); + + const tx = await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account0.address); + + await tx.wait(); + + await expect(tx).to.not.emit(meTokenRegistry, "UpdateBalances"); + await expect(tx).to.emit(meTokenRegistry, "FinishResubscribe"); + + const ownerMeTokenBefore = await meToken.balanceOf(account0.address); + const ownerDAIBefore = await dai.balanceOf(account0.address); + const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); + const ownerWETHBefore = await weth.balanceOf(account0.address); + const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); + const migrationDAIBefore = await dai.balanceOf(migration.address); + const migrationWETHBefore = await weth.balanceOf(migration.address); + + expect(migrationWETHBeforeMint).to.be.gt(0); // due to refund ration from last burn + expect(vaultWETHBefore).to.equal( + tokenDeposited.add(migrationWETHBeforeMint) + ); + expect(migrationWETHBefore).to.equal(0); // as all funds are transferred to vault + + await foundry + .connect(account0) + .burn(meToken.address, ownerMeTokenBefore, account0.address); + + const totalSupply = await meToken.totalSupply(); + const ownerMeTokenAfter = await meToken.balanceOf(account0.address); + const ownerDAIAfter = await dai.balanceOf(account0.address); + const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); + const ownerWETHAfter = await weth.balanceOf(account0.address); + const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); + const migrationDAIAfter = await dai.balanceOf(migration.address); + const migrationWETHAfter = await weth.balanceOf(migration.address); + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + + expect(totalSupply).to.equal(0); + expect(ownerMeTokenAfter).to.equal(0); // as all tokens are burned + expect(ownerDAIAfter).to.equal(ownerDAIBefore); // as owner receives new fund in weth + expect(vaultDAIBefore).to.equal(vaultDAIAfter); // as vault receives new fund in weth + expect(migrationDAIBefore).to.equal(migrationDAIAfter); // as migration receives no funds + expect(migrationWETHAfter).to.equal(migrationWETHBefore); // as migration receives no funds + + expect(vaultWETHAfter).to.equal(0); // as all token deposited goes to owner incl migration + expect(ownerWETHAfter.sub(ownerWETHBefore)).to.equal(vaultWETHBefore); // as all token deposited goes to owner plus migration + expect(metokenDetails.balancePooled).to.equal(0); + expect(metokenDetails.balanceLocked).to.equal(0); + }); + it("burn() [buyer]: assets received based on targetRefundRatio", async () => { + const vaultWETHBeforeMint = await weth.balanceOf( + singleAssetVault.address + ); + + const tx = await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account1.address); + + await tx.wait(); + + await expect(tx).to.not.emit(meTokenRegistry, "UpdateBalances"); + await expect(tx).to.not.emit(meTokenRegistry, "FinishResubscribe"); + + const buyerMeTokenBefore = await meToken.balanceOf(account1.address); + const buyerDAIBefore = await dai.balanceOf(account1.address); + const vaultDAIBefore = await dai.balanceOf(singleAssetVault.address); + const buyerWETHBefore = await weth.balanceOf(account1.address); + const vaultWETHBefore = await weth.balanceOf(singleAssetVault.address); + const migrationDAIBefore = await dai.balanceOf(migration.address); + const migrationWETHBefore = await weth.balanceOf(migration.address); + + expect(vaultWETHBeforeMint).to.equal(0); + expect(vaultWETHBefore).to.equal(tokenDeposited); + + await foundry + .connect(account1) + .burn(meToken.address, buyerMeTokenBefore, account1.address); + + const totalSupply = await meToken.totalSupply(); + const buyerMeTokenAfter = await meToken.balanceOf(account1.address); + const buyerDAIAfter = await dai.balanceOf(account1.address); + const vaultDAIAfter = await dai.balanceOf(singleAssetVault.address); + const buyerWETHAfter = await weth.balanceOf(account1.address); + const vaultWETHAfter = await weth.balanceOf(singleAssetVault.address); + const migrationDAIAfter = await dai.balanceOf(migration.address); + const migrationWETHAfter = await weth.balanceOf(migration.address); + const metokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + + const refundAmount = tokenDeposited.mul(targetRefundRatio).div(1e6); + + expect(totalSupply).to.equal(0); + expect(buyerMeTokenAfter).to.equal(0); // as all tokens are burned + expect(buyerDAIAfter).to.equal(buyerDAIBefore); // as buyer receives new fund in weth + expect(vaultDAIBefore).to.equal(vaultDAIAfter); // as vault receives new fund in weth + expect(migrationDAIBefore).to.equal(migrationDAIAfter); // as migration receives no funds + expect(migrationWETHAfter).to.equal(migrationWETHBefore); // as migration receives no funds + expect(vaultWETHAfter).to.equal(tokenDeposited.sub(refundAmount)); // refund ration token remains in vault + expect(buyerWETHAfter.sub(buyerWETHBefore)).to.equal(refundAmount); // buyer only receives refund ratio + expect(metokenDetails.balancePooled).to.equal(0); + expect(metokenDetails.balanceLocked).to.equal( + tokenDeposited.sub(refundAmount) + ); + }); }); }); +}; +setup().then(() => { + run(); });