diff --git a/scripts/deploy.ts b/scripts/deploy.ts index e2fe83a7..5fdb63c0 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -17,7 +17,7 @@ const ETHERSCAN_CHAIN_IDS = [1, 3, 4, 5, 42]; const SUPPORTED_NETWORK = [1, 4, 100, 31337]; const deployDir = "deployment"; const contracts: any[] = []; -const REFUND_RATIO = 50000; +const REFUND_RATIO = 800000; const PRECISION = BigNumber.from(10).pow(18); const MAX_WEIGHT = 1000000; const RESERVE_WEIGHT = BigNumber.from(MAX_WEIGHT).div(2).toString(); diff --git a/test/integration/Hub/UpdateCurveDetails.ts b/test/integration/Hub/UpdateCurveDetails.ts index 1a90eb60..bd57083f 100644 --- a/test/integration/Hub/UpdateCurveDetails.ts +++ b/test/integration/Hub/UpdateCurveDetails.ts @@ -21,872 +21,183 @@ import { expect } from "chai"; import { MeToken } from "../../../artifacts/types/MeToken"; import { UniswapSingleTransferMigration } from "../../../artifacts/types/UniswapSingleTransferMigration"; import { SingleAssetVault } from "../../../artifacts/types/SingleAssetVault"; -import { passDays, passHours, passSeconds } from "../../utils/hardhatNode"; +import { + mineBlock, + passDays, + passHours, + passSeconds, + setAutomine, + setNextBlockTimestamp, +} from "../../utils/hardhatNode"; import { ICurve } from "../../../artifacts/types/ICurve"; import { start } from "repl"; -describe("Hub - update CurveDetails", () => { - let meTokenRegistry: MeTokenRegistry; - let bancorABDK: BancorABDK; - let updatedBancorABDK: BancorABDK; - let curveRegistry: CurveRegistry; - let migrationRegistry: MigrationRegistry; - let singleAssetVault: SingleAssetVault; - let foundry: Foundry; - let hub: Hub; - let token: ERC20; - let dai: ERC20; - let meToken: MeToken; - let account0: SignerWithAddress; - let tokenHolder: Signer; - let account1: SignerWithAddress; - let account2: SignerWithAddress; - let account3: SignerWithAddress; - const one = ethers.utils.parseEther("1"); - let baseY: BigNumber; - let baseYNum: number; - let updatedBaseYNum: number; - let updatedBaseY: BigNumber; - let reserveWeight: number; - let updatedReserveWeight: number; - const MAX_WEIGHT = 1000000; - let encodedCurveDetails: string; - const firstHubId = 1; - const refundRatio = 5000; - before(async () => { - updatedBaseYNum = 10000; - updatedBaseY = one.mul(updatedBaseYNum); - updatedReserveWeight = MAX_WEIGHT / 10; - baseYNum = 1000; - baseY = one.mul(baseYNum); - reserveWeight = MAX_WEIGHT / 2; - let DAI; - ({ DAI } = await getNamedAccounts()); - - encodedCurveDetails = ethers.utils.defaultAbiCoder.encode( - ["uint256", "uint32"], - [baseY, reserveWeight] - ); - const encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( - ["address"], - [DAI] - ); - bancorABDK = await deploy("BancorABDK"); - - ({ - token, - hub, - curveRegistry, - migrationRegistry, - singleAssetVault, - tokenHolder, - foundry, - account0, - account1, - account2, - account3, - meTokenRegistry, - } = await hubSetup( - encodedCurveDetails, - encodedVaultArgs, - refundRatio, - bancorABDK - )); - dai = token; - const detail = await bancorABDK.getBancorDetails(firstHubId); - expect(detail.reserveWeight).to.equal(reserveWeight); - // Deploy uniswap migration and approve it to the registry - const 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 - await token - .connect(tokenHolder) - .transfer(account2.address, ethers.utils.parseEther("1000")); - // Create meToken and subscribe to Hub1 - const name = "Carl0 meToken"; - const symbol = "CARL"; - - const tx = await meTokenRegistry - .connect(account0) - .subscribe(name, symbol, firstHubId, 0); - const meTokenAddr = await meTokenRegistry.getOwnerMeToken(account0.address); - meToken = await getContractAt("MeToken", meTokenAddr); - // Register Hub2 w/ same args but different refund Ratio - - const tokenDeposited = ethers.utils.parseEther("100"); - await token.connect(account2).approve(foundry.address, tokenDeposited); - const balBefore = await meToken.balanceOf(account2.address); - const vaultBalBefore = await token.balanceOf(singleAssetVault.address); - await foundry - .connect(account2) - .mint(meTokenAddr, tokenDeposited, account2.address); - const balAfter = await meToken.balanceOf(account2.address); - const vaultBalAfter = await token.balanceOf(singleAssetVault.address); - expect(vaultBalAfter.sub(vaultBalBefore)).to.equal(tokenDeposited); - //setWarmup for 2 days - let warmup = await hub.getWarmup(); - expect(warmup).to.equal(0); - await hub.setWarmup(172800); - - warmup = await hub.getWarmup(); - expect(warmup).to.equal(172800); - let cooldown = await hub.getCooldown(); - expect(cooldown).to.equal(0); - //setCooldown for 1 day - await hub.setCooldown(86400); - cooldown = await hub.getCooldown(); - expect(cooldown).to.equal(86400); - - let duration = await hub.getDuration(); - expect(duration).to.equal(0); - //setDuration for 1 week - await hub.setDuration(604800); - duration = await hub.getDuration(); - expect(duration).to.equal(604800); - }); - - describe("Warmup", () => { - it("should revert if targetCurve is the current curve", async () => { - const updatedEncodedCurveDetails = ethers.utils.defaultAbiCoder.encode( - ["uint256", "uint32"], - [updatedBaseY, updatedReserveWeight] - ); - await expect( - hub.initUpdate( - firstHubId, - bancorABDK.address, - 0, - updatedEncodedCurveDetails - ) - ).to.be.revertedWith("targetCurve==curve"); - }); - it("Assets received based on initial curveDetails", async () => { - const updatedEncodedCurveDetails = ethers.utils.defaultAbiCoder.encode( - ["uint256", "uint32"], - [updatedBaseY, updatedReserveWeight] - ); - updatedBancorABDK = await deploy("BancorABDK"); - await curveRegistry.approve(updatedBancorABDK.address); - await hub.initUpdate( - firstHubId, - updatedBancorABDK.address, - 0, - updatedEncodedCurveDetails - ); - - const detail = await updatedBancorABDK.getBancorDetails(firstHubId); - expect(detail.reserveWeight).to.equal(updatedReserveWeight); - - const tokenDepositedInETH = 100; - const tokenDeposited = ethers.utils.parseEther( - tokenDepositedInETH.toString() - ); - - await token.connect(account2).approve(foundry.address, tokenDeposited); - const balBefore = await meToken.balanceOf(account0.address); - const balDaiBefore = await token.balanceOf(account0.address); - const vaultBalBefore = await token.balanceOf(singleAssetVault.address); - const meTokenTotalSupply = await meToken.totalSupply(); - const calculatedReturn = calculateTokenReturned( - tokenDepositedInETH, - toETHNumber(meTokenTotalSupply), - toETHNumber(vaultBalBefore), - reserveWeight / MAX_WEIGHT - ); - await foundry - .connect(account2) - .mint(meToken.address, tokenDeposited, account0.address); - const balAfter = await meToken.balanceOf(account0.address); - const vaultBalAfter = await token.balanceOf(singleAssetVault.address); - - expect(toETHNumber(balAfter)).to.be.approximately( - calculatedReturn, - 0.000000000000001 - ); - expect(vaultBalAfter.sub(vaultBalBefore)).to.equal(tokenDeposited); - const balDaiAcc1Before = await token.balanceOf(account1.address); - - //send half burnt by owner - await foundry - .connect(account0) - .burn(meToken.address, balAfter, account0.address); - const balDaiAfter = await token.balanceOf(account0.address); - const vaultBalAfterBurn = await token.balanceOf(singleAssetVault.address); - - // we have less DAI in the vault cos they have been sent to the burner - expect(vaultBalAfter.sub(vaultBalAfterBurn)).to.equal( - balDaiAfter.sub(balDaiBefore) - ); - // buyer - const balAcc1Before = await meToken.balanceOf(account1.address); - await token.connect(account1).approve(foundry.address, tokenDeposited); - await foundry - .connect(account1) - .mint(meToken.address, tokenDeposited, account1.address); - const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); - - expect(vaultBalAfterMint.sub(vaultBalAfterBurn)).to.equal(tokenDeposited); - - const balAcc1After = await meToken.balanceOf(account1.address); - expect(balAcc1After.sub(balAcc1Before)).to.equal( - balAfter.sub(balBefore).sub(ethers.utils.parseUnits("1", "wei")) - ); - //send half burnt by buyer - await foundry - .connect(account1) - .burn(meToken.address, balAcc1After, account1.address); - const balDaiAcc1After = await token.balanceOf(account1.address); - - const vaultBalAfterBuyerBurn = await token.balanceOf( - singleAssetVault.address - ); - // we have less DAI in the vault cos they have been sent to the burner - expect(vaultBalAfterMint.sub(vaultBalAfterBuyerBurn)).to.equal( - balDaiAcc1After.sub(balDaiAcc1Before.sub(tokenDeposited)) - ); - expect( - Number( - ethers.utils.formatEther( - tokenDeposited.sub(balDaiAcc1Before.sub(balDaiAcc1After)) - ) - ) - ).to.equal((tokenDepositedInETH * refundRatio) / MAX_WEIGHT); - }); - }); - - describe("Duration", () => { +const setup = async () => { + describe("Hub - update CurveDetails", () => { + let meTokenRegistry: MeTokenRegistry; + let bancorABDK: BancorABDK; + let updatedBancorABDK: BancorABDK; + let curveRegistry: CurveRegistry; + let migrationRegistry: MigrationRegistry; + let singleAssetVault: SingleAssetVault; + let foundry: Foundry; + let hub: Hub; + let token: ERC20; + let dai: ERC20; + let meToken: MeToken; + let account0: SignerWithAddress; + let tokenHolder: Signer; + let account1: SignerWithAddress; + let account2: SignerWithAddress; + let account3: SignerWithAddress; + const one = ethers.utils.parseEther("1"); + let baseY: BigNumber; + let baseYNum: number; + let updatedBaseYNum: number; + let updatedBaseY: BigNumber; + let reserveWeight: number; + let updatedReserveWeight: number; + const MAX_WEIGHT = 1000000; + let encodedCurveDetails: string; + const firstHubId = 1; + const refundRatio = 5000; before(async () => { - await passHours(1); - }); - it("Assets received for buyer based on weighted average burning full supply ", async () => { - //move forward 3 Days - await passDays(3); - const tokenDepositedInETH = 100; - const tokenDeposited = ethers.utils.parseEther( - tokenDepositedInETH.toString() - ); - await token.connect(account2).approve(foundry.address, tokenDeposited); - const vaultBalBefore = await token.balanceOf(singleAssetVault.address); - - // send token to owner - await foundry - .connect(account2) - .mint(meToken.address, tokenDeposited, account2.address); - const balDaiAfterMint = await token.balanceOf(account2.address); - const balAfter = await meToken.balanceOf(account2.address); - - const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); - expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); - // burnt by owner - await meToken.connect(account2).approve(foundry.address, balAfter); - - const rawAssetsReturnedFromFoundry = - await foundry.calculateRawAssetsReturned(meToken.address, balAfter); - const balBefore = await meToken.balanceOf(account0.address); - const balDaiBefore = await token.balanceOf(account0.address); - const vaultBalBeforeBurn = await token.balanceOf( - singleAssetVault.address - ); - const meTokenTotalSupply = await meToken.totalSupply(); - const meTokenDetails = await meTokenRegistry.getDetails(meToken.address); - - const rawAssetsReturned = calculateCollateralReturned( - toETHNumber(balAfter), - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - reserveWeight / MAX_WEIGHT - ); - const targetassetsReturned = calculateCollateralReturned( - toETHNumber(balAfter), - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - updatedReserveWeight / MAX_WEIGHT - ); - await foundry - .connect(account2) - .burn(meToken.address, balAfter, account2.address); - const balDaiAfterBurn = await token.balanceOf(account2.address); - const meTokenDetailsAfterBurn = await meTokenRegistry.getDetails( - meToken.address - ); - const { - active, - refundRatio, - updating, - startTime, - endTime, - endCooldown, - reconfigure, - targetRefundRatio, - } = await hub.getDetails(1); - expect(active).to.be.true; - expect(updating).to.be.true; - const block = await ethers.provider.getBlock("latest"); - const assetsReturned = - (rawAssetsReturned * refundRatio.toNumber()) / MAX_WEIGHT; - const calcWAvrgRes = weightedAverageSimulation( - rawAssetsReturned, - targetassetsReturned, - startTime.toNumber(), - endTime.toNumber(), - block.timestamp - ); - const calculatedReturn = ethers.utils - .parseEther(`${assetsReturned}`) - .mul(BigNumber.from(Math.floor(calcWAvrgRes))) - .div(BigNumber.from(10 ** 6)); - // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn - // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); - expect( - toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) - ).to.be.approximately(assetsReturned, 0.000000000000001); - }); - it("Assets received for buyer based on weighted average not burning full supply ", async () => { - const tokenDepositedInETH = 100; - const tokenDeposited = ethers.utils.parseEther( - tokenDepositedInETH.toString() - ); - await token.connect(account2).approve(foundry.address, tokenDeposited); - const vaultBalBefore = await token.balanceOf(singleAssetVault.address); + updatedBaseYNum = 10000; + updatedBaseY = one.mul(updatedBaseYNum); + updatedReserveWeight = MAX_WEIGHT / 10; + baseYNum = 1000; + baseY = one.mul(baseYNum); + reserveWeight = MAX_WEIGHT / 2; + let DAI; + ({ DAI } = await getNamedAccounts()); - // send token to owner - await foundry - .connect(account2) - .mint(meToken.address, tokenDeposited, account2.address); - const balDaiAfterMint = await token.balanceOf(account2.address); - const balAfter = await meToken.balanceOf(account2.address); - - const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); - expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); - // burnt by owner - await meToken.connect(account2).approve(foundry.address, balAfter); - - const rawAssetsReturnedFromFoundry = - await foundry.calculateRawAssetsReturned(meToken.address, balAfter); - const balBefore = await meToken.balanceOf(account0.address); - const balDaiBefore = await token.balanceOf(account0.address); - const vaultBalBeforeBurn = await token.balanceOf( - singleAssetVault.address - ); - const meTokenTotalSupply = await meToken.totalSupply(); - const meTokenDetails = await meTokenRegistry.getDetails(meToken.address); - const metokenToBurn = balAfter.div(2); - const rawAssetsReturned = calculateCollateralReturned( - toETHNumber(metokenToBurn), - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - reserveWeight / MAX_WEIGHT - ); - const targetassetsReturned = calculateCollateralReturned( - toETHNumber(metokenToBurn), - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - updatedReserveWeight / MAX_WEIGHT - ); - - await foundry - .connect(account2) - .burn(meToken.address, metokenToBurn, account2.address); - const balDaiAfterBurn = await token.balanceOf(account2.address); - const meTokenDetailsAfterBurn = await meTokenRegistry.getDetails( - meToken.address - ); - const { - active, - refundRatio, - updating, - startTime, - endTime, - endCooldown, - reconfigure, - targetRefundRatio, - } = await hub.getDetails(1); - expect(active).to.be.true; - expect(updating).to.be.true; - const block = await ethers.provider.getBlock("latest"); - - const calcWAvrgRes = weightedAverageSimulation( - rawAssetsReturned, - targetassetsReturned, - startTime.toNumber(), - endTime.toNumber(), - block.timestamp - ); - const assetsReturned = - (calcWAvrgRes * refundRatio.toNumber()) / MAX_WEIGHT; - const calculatedReturn = ethers.utils - .parseEther(`${assetsReturned}`) - .mul(BigNumber.from(Math.floor(calcWAvrgRes))) - .div(BigNumber.from(10 ** 6)); - // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn - // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); - expect( - toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) - ).to.be.approximately(assetsReturned, 0.000000000000001); - }); - it("Assets received for owner based on weighted average not burning full supply ", async () => { - // TODO: calculate weighted refundRatio based on current time relative to duration - const tokenDepositedInETH = 100; - const tokenDeposited = ethers.utils.parseEther( - tokenDepositedInETH.toString() - ); - await token - .connect(account1) - .transfer(account0.address, ethers.utils.parseEther("100")); - await token.approve(foundry.address, tokenDeposited); - const vaultBalBefore = await token.balanceOf(singleAssetVault.address); - - // send token to owner - await foundry.mint(meToken.address, tokenDeposited, account0.address); - const balDaiAfterMint = await token.balanceOf(account0.address); - const balAfter = await meToken.balanceOf(account0.address); - - const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); - expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); - // burnt by owner - await meToken.connect(account0).approve(foundry.address, balAfter); - - const rawAssetsReturnedFromFoundry = - await foundry.calculateRawAssetsReturned(meToken.address, balAfter); - const balBefore = await meToken.balanceOf(account0.address); - const balDaiBefore = await token.balanceOf(account0.address); - const vaultBalBeforeBurn = await token.balanceOf( - singleAssetVault.address - ); - const meTokenTotalSupply = await meToken.totalSupply(); - const meTokenDetails = await meTokenRegistry.getDetails(meToken.address); - const metokenToBurn = balAfter.div(2); - const rawAssetsReturned = calculateCollateralReturned( - toETHNumber(metokenToBurn), - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - reserveWeight / MAX_WEIGHT - ); - const targetassetsReturned = calculateCollateralReturned( - toETHNumber(metokenToBurn), - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - updatedReserveWeight / MAX_WEIGHT + encodedCurveDetails = ethers.utils.defaultAbiCoder.encode( + ["uint256", "uint32"], + [baseY, reserveWeight] ); - const meTokenDetailsBeforeBurn = await meTokenRegistry.getDetails( - meToken.address + const encodedVaultArgs = ethers.utils.defaultAbiCoder.encode( + ["address"], + [DAI] ); - await foundry - .connect(account0) - .burn(meToken.address, metokenToBurn, account0.address); - - const balDaiAfterBurn = await token.balanceOf(account0.address); - - const { - active, + bancorABDK = await deploy("BancorABDK"); + + ({ + token, + hub, + curveRegistry, + migrationRegistry, + singleAssetVault, + tokenHolder, + foundry, + account0, + account1, + account2, + account3, + meTokenRegistry, + } = await hubSetup( + encodedCurveDetails, + encodedVaultArgs, refundRatio, - updating, - startTime, - endTime, - endCooldown, - reconfigure, - targetRefundRatio, - } = await hub.getDetails(1); - expect(active).to.be.true; - expect(updating).to.be.true; - const block = await ethers.provider.getBlock("latest"); - // the weighted average on the curve should be applied for owner and buyers - const calcWAvrgRes = weightedAverageSimulation( - rawAssetsReturned, - targetassetsReturned, - startTime.toNumber(), - endTime.toNumber(), - block.timestamp + bancorABDK + )); + dai = token; + const detail = await bancorABDK.getBancorDetails(firstHubId); + expect(detail.reserveWeight).to.equal(reserveWeight); + // Deploy uniswap migration and approve it to the registry + const migration = await deploy( + "UniswapSingleTransferMigration", + undefined, + account0.address, + foundry.address, + hub.address, + meTokenRegistry.address, + migrationRegistry.address ); - // but the owner gets a proportional share of the token burnt from the balanced locked - const assetsReturned = - calcWAvrgRes + - (toETHNumber(metokenToBurn) / toETHNumber(meTokenTotalSupply)) * - toETHNumber(meTokenDetailsBeforeBurn.balanceLocked); - - const calculatedReturn = ethers.utils - .parseEther(`${assetsReturned}`) - .mul(BigNumber.from(Math.floor(calcWAvrgRes))) - .div(BigNumber.from(10 ** 6)); - - // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn - // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); - expect( - toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) - ).to.be.approximately(assetsReturned, 0.0000000000001); - }); - it("mint(): assets received based on weighted average", async () => { - const tokenDepositedInETH = 100; - const tokenDeposited = ethers.utils.parseEther( - tokenDepositedInETH.toString() + await migrationRegistry.approve( + singleAssetVault.address, + singleAssetVault.address, + migration.address ); - await token.connect(account2).approve(foundry.address, tokenDeposited); - const vaultBalBefore = await token.balanceOf(singleAssetVault.address); - const tokenMinted = await foundry.calculateMeTokensMinted( - meToken.address, - tokenDeposited - ); - - const balBefore = await meToken.balanceOf(account3.address); - - const meTokenTotalSupply = await meToken.totalSupply(); - const meTokenDetails = await meTokenRegistry.getDetails(meToken.address); - const calcTokenReturn = calculateTokenReturned( - tokenDepositedInETH, - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - reserveWeight / MAX_WEIGHT - ); - - const calcTargetTokenReturn = calculateTokenReturned( - tokenDepositedInETH, - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - updatedReserveWeight / MAX_WEIGHT - ); - const { active, updating, startTime, endTime } = await hub.getDetails(1); - expect(active).to.be.true; - expect(updating).to.be.true; - const block = await ethers.provider.getBlock("latest"); - const calcWAvrgRes = weightedAverageSimulation( - calcTokenReturn, - calcTargetTokenReturn, - startTime.toNumber(), - endTime.toNumber(), - block.timestamp - ); - // buyer mint metokens - await foundry - .connect(account2) - .mint(meToken.address, tokenDeposited, account3.address); - const balDaiAfterMint = await token.balanceOf(account2.address); - const balAfter = await meToken.balanceOf(account3.address); - const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); - expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); - expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( - toETHNumber(tokenMinted), - 0.00001 - ); - expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( - calcWAvrgRes, - 0.00001 - ); - }); - }); - describe("Cooldown", () => { - it("initUpdate() cannot be called", async () => { - const { - active, - refundRatio, - updating, - startTime, - endTime, - endCooldown, - reconfigure, - targetRefundRatio, - } = await hub.getDetails(1); - expect(active).to.be.true; - expect(updating).to.be.true; - expect(reconfigure).to.be.false; - const block = await ethers.provider.getBlock("latest"); - - //Block.timestamp should be between endtime and endCooldown - // move forward to cooldown - await passSeconds(endTime.sub(block.timestamp).toNumber() + 1); - await expect( - hub.initUpdate( - 1, - bancorABDK.address, - 1000, - ethers.utils.toUtf8Bytes("") - ) - ).to.be.revertedWith("Still cooling down"); - }); - - it("burn() and mint() by owner should use the targetCurve", async () => { - // TODO: calculate weighted refundRatio based on current time relative to duration - const tokenDepositedInETH = 100; - const tokenDeposited = ethers.utils.parseEther( - tokenDepositedInETH.toString() - ); + // Pre-load owner and buyer w/ DAI await token - .connect(account1) - .transfer(account0.address, ethers.utils.parseEther("100")); - await token.approve(foundry.address, tokenDeposited); - const vaultBalBefore = await token.balanceOf(singleAssetVault.address); - const balBefore = await meToken.balanceOf(account0.address); + .connect(tokenHolder) + .transfer(account2.address, ethers.utils.parseEther("1000")); + // Create meToken and subscribe to Hub1 + const name = "Carl0 meToken"; + const symbol = "CARL"; - const tokenMinted = await foundry.calculateMeTokensMinted( - meToken.address, - tokenDeposited - ); - let meTokenTotalSupply = await meToken.totalSupply(); - let meTokenDetails = await meTokenRegistry.getDetails(meToken.address); - // the updated curve should be applied - const calcTargetTokenReturn = calculateTokenReturned( - tokenDepositedInETH, - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - updatedReserveWeight / MAX_WEIGHT - ); - // send token to owner - await foundry.mint(meToken.address, tokenDeposited, account0.address); - const balDaiAfterMint = await token.balanceOf(account0.address); - const balAfter = await meToken.balanceOf(account0.address); - expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( - calcTargetTokenReturn, - 0.0000000000001 - ); - expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( - toETHNumber(tokenMinted), - 0.0000000000001 - ); - const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); - expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); - // burnt by owner - await meToken.connect(account0).approve(foundry.address, balAfter); - - meTokenTotalSupply = await meToken.totalSupply(); - meTokenDetails = await meTokenRegistry.getDetails(meToken.address); - const metokenToBurn = balAfter.div(2); - const rawAssetsReturnedFromFoundry = - await foundry.calculateRawAssetsReturned( - meToken.address, - metokenToBurn - ); - const { - active, - refundRatio, - updating, - startTime, - endTime, - endCooldown, - reconfigure, - targetRefundRatio, - curve, - targetCurve, - } = await hub.getDetails(1); - const targetassetsReturned = calculateCollateralReturned( - toETHNumber(metokenToBurn), - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - updatedReserveWeight / MAX_WEIGHT - ); - const xtargetassetsReturned = calculateCollateralReturned( - toETHNumber(metokenToBurn), - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - reserveWeight / MAX_WEIGHT - ); - - const meTokenDetailsBeforeBurn = await meTokenRegistry.getDetails( - meToken.address - ); - - await foundry + const tx = await meTokenRegistry .connect(account0) - .burn(meToken.address, metokenToBurn, account0.address); - - const balDaiAfterBurn = await token.balanceOf(account0.address); - const currentCurve = await getContractAt("BancorABDK", curve); - const hubTargetCurve = await getContractAt( - "BancorABDK", - targetCurve - ); - const block = await ethers.provider.getBlock("latest"); - expect(updatedBancorABDK.address).to.equal(currentCurve.address); - expect(hubTargetCurve.address).to.equal(ethers.constants.AddressZero); - expect(endCooldown).to.be.gt(block.timestamp); - expect(active).to.be.true; - expect(updating).to.be.false; - expect(reconfigure).to.be.false; - - // 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 + - (toETHNumber(metokenToBurn) / toETHNumber(meTokenTotalSupply)) * - toETHNumber(meTokenDetailsBeforeBurn.balanceLocked); - - // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn - // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); - expect( - toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) - ).to.be.approximately(assetsReturned, 0.00000000001); - }); - it("burn() and mint() by buyer should use the targetCurve", async () => { - // TODO: calculate weighted refundRatio based on current time relative to duration - const tokenDepositedInETH = 100; - const tokenDeposited = ethers.utils.parseEther( - tokenDepositedInETH.toString() + .subscribe(name, symbol, firstHubId, 0); + const meTokenAddr = await meTokenRegistry.getOwnerMeToken( + account0.address ); + meToken = await getContractAt("MeToken", meTokenAddr); + // Register Hub2 w/ same args but different refund Ratio + const tokenDeposited = ethers.utils.parseEther("100"); await token.connect(account2).approve(foundry.address, tokenDeposited); - const vaultBalBefore = await token.balanceOf(singleAssetVault.address); const balBefore = await meToken.balanceOf(account2.address); - - const tokenMinted = await foundry.calculateMeTokensMinted( - meToken.address, - tokenDeposited - ); - let meTokenTotalSupply = await meToken.totalSupply(); - let meTokenDetails = await meTokenRegistry.getDetails(meToken.address); - // the updated curve should be applied - const calcTargetTokenReturn = calculateTokenReturned( - tokenDepositedInETH, - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - updatedReserveWeight / MAX_WEIGHT - ); - // send token to owner + const vaultBalBefore = await token.balanceOf(singleAssetVault.address); await foundry .connect(account2) - .mint(meToken.address, tokenDeposited, account2.address); - const balDaiAfterMint = await token.balanceOf(account2.address); + .mint(meTokenAddr, tokenDeposited, account2.address); const balAfter = await meToken.balanceOf(account2.address); - expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( - toETHNumber(tokenMinted), - 0.0000000000001 - ); - expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( - calcTargetTokenReturn, - 0.0000000000001 - ); - - const vaultBalAfterMint = await token.balanceOf(singleAssetVault.address); - expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); - // burnt by owner - await meToken.connect(account2).approve(foundry.address, balAfter); - - meTokenTotalSupply = await meToken.totalSupply(); - meTokenDetails = await meTokenRegistry.getDetails(meToken.address); - const metokenToBurn = balAfter.div(2); - const rawAssetsReturnedFromFoundry = - await foundry.calculateRawAssetsReturned( - meToken.address, - metokenToBurn - ); - const { - active, - refundRatio, - updating, - startTime, - endTime, - endCooldown, - reconfigure, - targetRefundRatio, - curve, - targetCurve, - } = await hub.getDetails(1); - const targetassetsReturned = calculateCollateralReturned( - toETHNumber(metokenToBurn), - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - updatedReserveWeight / MAX_WEIGHT - ); - const normalAssetsReturned = calculateCollateralReturned( - toETHNumber(metokenToBurn), - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - reserveWeight / MAX_WEIGHT - ); - - const meTokenDetailsBeforeBurn = await meTokenRegistry.getDetails( - meToken.address - ); - - await foundry - .connect(account2) - .burn(meToken.address, metokenToBurn, account2.address); - - const balDaiAfterBurn = await token.balanceOf(account2.address); - const currentCurve = await getContractAt("BancorABDK", curve); - const hubTargetCurve = await getContractAt( - "BancorABDK", - targetCurve - ); - const block = await ethers.provider.getBlock("latest"); - expect(updatedBancorABDK.address).to.equal(currentCurve.address); - expect(hubTargetCurve.address).to.equal(ethers.constants.AddressZero); - expect(endCooldown).to.be.gt(block.timestamp); - expect(active).to.be.true; - expect(updating).to.be.false; - expect(reconfigure).to.be.false; - - // as it is a buyer we apply the refund ratio - const assetsReturned = - (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); - expect( - toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) - ).to.be.approximately(assetsReturned, 0.00000000001); + const vaultBalAfter = await token.balanceOf(singleAssetVault.address); + expect(vaultBalAfter.sub(vaultBalBefore)).to.equal(tokenDeposited); + //setWarmup for 2 days + let warmup = await hub.getWarmup(); + expect(warmup).to.equal(0); + await hub.setWarmup(172800); + + warmup = await hub.getWarmup(); + expect(warmup).to.equal(172800); + let cooldown = await hub.getCooldown(); + expect(cooldown).to.equal(0); + //setCooldown for 1 day + await hub.setCooldown(86400); + cooldown = await hub.getCooldown(); + expect(cooldown).to.equal(86400); + + let duration = await hub.getDuration(); + expect(duration).to.equal(0); + //setDuration for 1 week + await hub.setDuration(604800); + duration = await hub.getDuration(); + expect(duration).to.equal(604800); }); - }); - - describe("When reconfiguring", () => { - before(async () => { - const { endTime, endCooldown, refundRatio, startTime } = - await hub.getDetails(1); - const block = await ethers.provider.getBlock("latest"); - expect(block.timestamp).to.be.gt(endTime); - //expect(block.timestamp).to.be.lt(endCooldown); - - await passSeconds(endCooldown.sub(block.timestamp).toNumber() + 1); - reserveWeight = updatedReserveWeight; - updatedReserveWeight = 750000; - - encodedCurveDetails = ethers.utils.defaultAbiCoder.encode( - ["uint32"], - [updatedReserveWeight] - ); - await hub.initUpdate( - 1, - ethers.constants.AddressZero, - 0, - encodedCurveDetails - ); - const block2 = await ethers.provider.getBlock("latest"); - const details = await hub.getDetails(1); - expect(details.curve).to.equal(updatedBancorABDK.address); - expect(details.targetCurve).to.equal(ethers.constants.AddressZero); - expect(details.endTime).to.be.gt(0); - expect(details.endTime).to.be.gt(block.timestamp); - expect(details.refundRatio).to.to.equal(refundRatio); - expect(details.targetRefundRatio).to.to.equal(0); - expect(details.active).to.be.true; - expect(details.updating).to.be.true; - expect(details.reconfigure).to.be.true; - // we are warming up - expect(block2.timestamp).to.be.lt(details.startTime); - }); describe("Warmup", () => { + it("should revert if targetCurve is the current curve", async () => { + const updatedEncodedCurveDetails = ethers.utils.defaultAbiCoder.encode( + ["uint256", "uint32"], + [updatedBaseY, updatedReserveWeight] + ); + await expect( + hub.initUpdate( + firstHubId, + bancorABDK.address, + 0, + updatedEncodedCurveDetails + ) + ).to.be.revertedWith("targetCurve==curve"); + }); it("Assets received based on initial curveDetails", async () => { - const details = await hub.getDetails(1); - - const currentCurve = await getContractAt( - "BancorABDK", - details.curve + const updatedEncodedCurveDetails = ethers.utils.defaultAbiCoder.encode( + ["uint256", "uint32"], + [updatedBaseY, updatedReserveWeight] + ); + updatedBancorABDK = await deploy("BancorABDK"); + await curveRegistry.approve(updatedBancorABDK.address); + await hub.initUpdate( + firstHubId, + updatedBancorABDK.address, + 0, + updatedEncodedCurveDetails ); - expect(currentCurve.address).to.equal(updatedBancorABDK.address); + const detail = await updatedBancorABDK.getBancorDetails(firstHubId); - expect(detail.targetReserveWeight).to.equal(updatedReserveWeight); + expect(detail.reserveWeight).to.equal(updatedReserveWeight); const tokenDepositedInETH = 100; const tokenDeposited = ethers.utils.parseEther( @@ -898,29 +209,19 @@ describe("Hub - update CurveDetails", () => { const balDaiBefore = await token.balanceOf(account0.address); const vaultBalBefore = await token.balanceOf(singleAssetVault.address); const meTokenTotalSupply = await meToken.totalSupply(); - const meTokenDetails = await meTokenRegistry.getDetails( - meToken.address - ); const calculatedReturn = calculateTokenReturned( tokenDepositedInETH, toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), + toETHNumber(vaultBalBefore), reserveWeight / MAX_WEIGHT ); - - const targetReturn = calculateTokenReturned( - tokenDepositedInETH, - toETHNumber(meTokenTotalSupply), - toETHNumber(meTokenDetails.balancePooled), - updatedReserveWeight / MAX_WEIGHT - ); await foundry .connect(account2) .mint(meToken.address, tokenDeposited, account0.address); const balAfter = await meToken.balanceOf(account0.address); const vaultBalAfter = await token.balanceOf(singleAssetVault.address); - expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( + expect(toETHNumber(balAfter)).to.be.approximately( calculatedReturn, 0.000000000000001 ); @@ -930,7 +231,7 @@ describe("Hub - update CurveDetails", () => { //send half burnt by owner await foundry .connect(account0) - .burn(meToken.address, balAfter.sub(balBefore), account0.address); + .burn(meToken.address, balAfter, account0.address); const balDaiAfter = await token.balanceOf(account0.address); const vaultBalAfterBurn = await token.balanceOf( singleAssetVault.address @@ -956,7 +257,7 @@ describe("Hub - update CurveDetails", () => { const balAcc1After = await meToken.balanceOf(account1.address); expect(balAcc1After.sub(balAcc1Before)).to.equal( - balAfter.sub(balBefore) + balAfter.sub(balBefore).sub(ethers.utils.parseUnits("1", "wei")) ); //send half burnt by buyer await foundry @@ -980,6 +281,7 @@ describe("Hub - update CurveDetails", () => { ).to.equal((tokenDepositedInETH * refundRatio) / MAX_WEIGHT); }); }); + describe("Duration", () => { before(async () => { await passHours(1); @@ -1061,13 +363,15 @@ describe("Hub - update CurveDetails", () => { endTime.toNumber(), block.timestamp ); - const calcWithRefundRatio = - (calcWAvrgRes * refundRatio.toNumber()) / MAX_WEIGHT; + const calculatedReturn = ethers.utils + .parseEther(`${assetsReturned}`) + .mul(BigNumber.from(Math.floor(calcWAvrgRes))) + .div(BigNumber.from(10 ** 6)); // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); expect( toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) - ).to.be.approximately(calcWithRefundRatio, 0.0000000000001); + ).to.be.approximately(assetsReturned, 0.000000000000001); }); it("Assets received for buyer based on weighted average not burning full supply ", async () => { const tokenDepositedInETH = 100; @@ -1207,7 +511,6 @@ describe("Hub - update CurveDetails", () => { const meTokenDetailsBeforeBurn = await meTokenRegistry.getDetails( meToken.address ); - await foundry .connect(account0) .burn(meToken.address, metokenToBurn, account0.address); @@ -1245,6 +548,7 @@ describe("Hub - update CurveDetails", () => { .parseEther(`${assetsReturned}`) .mul(BigNumber.from(Math.floor(calcWAvrgRes))) .div(BigNumber.from(10 ** 6)); + // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); expect( @@ -1258,6 +562,8 @@ describe("Hub - update CurveDetails", () => { ); await token.connect(account2).approve(foundry.address, tokenDeposited); const vaultBalBefore = await token.balanceOf(singleAssetVault.address); + await setAutomine(false); + const block = await ethers.provider.getBlock("latest"); const tokenMinted = await foundry.calculateMeTokensMinted( meToken.address, tokenDeposited @@ -1269,6 +575,7 @@ describe("Hub - update CurveDetails", () => { const meTokenDetails = await meTokenRegistry.getDetails( meToken.address ); + const calcTokenReturn = calculateTokenReturned( tokenDepositedInETH, toETHNumber(meTokenTotalSupply), @@ -1287,34 +594,42 @@ describe("Hub - update CurveDetails", () => { ); expect(active).to.be.true; expect(updating).to.be.true; - const block = await ethers.provider.getBlock("latest"); + + // take into account the time when + // the mint transaction will be included const calcWAvrgRes = weightedAverageSimulation( calcTokenReturn, calcTargetTokenReturn, startTime.toNumber(), endTime.toNumber(), - block.timestamp + block.timestamp + 1 ); + // to be precise we set the next block timestamp to be the same of when we ask for tokenMinted + // await setNextBlockTimestamp(block.timestamp); // buyer mint metokens await foundry .connect(account2) .mint(meToken.address, tokenDeposited, account3.address); + await mineBlock(block.timestamp + 1); + await setAutomine(true); const balDaiAfterMint = await token.balanceOf(account2.address); const balAfter = await meToken.balanceOf(account3.address); const vaultBalAfterMint = await token.balanceOf( singleAssetVault.address ); expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal(tokenDeposited); + expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( toETHNumber(tokenMinted), 0.00001 ); expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( calcWAvrgRes, - 0.00001 + 0.00000000000001 ); }); }); + describe("Cooldown", () => { it("initUpdate() cannot be called", async () => { const { @@ -1329,7 +644,7 @@ describe("Hub - update CurveDetails", () => { } = await hub.getDetails(1); expect(active).to.be.true; expect(updating).to.be.true; - expect(reconfigure).to.be.true; + expect(reconfigure).to.be.false; const block = await ethers.provider.getBlock("latest"); //Block.timestamp should be between endtime and endCooldown @@ -1344,6 +659,7 @@ describe("Hub - update CurveDetails", () => { ) ).to.be.revertedWith("Still cooling down"); }); + it("burn() and mint() by owner should use the targetCurve", async () => { // TODO: calculate weighted refundRatio based on current time relative to duration const tokenDepositedInETH = 100; @@ -1463,13 +779,11 @@ describe("Hub - update CurveDetails", () => { }); it("burn() and mint() by buyer should use the targetCurve", async () => { // TODO: calculate weighted refundRatio based on current time relative to duration - const tokenDepositedInETH = 10; + const tokenDepositedInETH = 100; const tokenDeposited = ethers.utils.parseEther( tokenDepositedInETH.toString() ); - await token - .connect(tokenHolder) - .transfer(account2.address, tokenDeposited); + await token.connect(account2).approve(foundry.address, tokenDeposited); const vaultBalBefore = await token.balanceOf(singleAssetVault.address); const balBefore = await meToken.balanceOf(account2.address); @@ -1578,5 +892,763 @@ describe("Hub - update CurveDetails", () => { ).to.be.approximately(assetsReturned, 0.00000000001); }); }); + + describe("When reconfiguring", () => { + before(async () => { + const { endTime, endCooldown, refundRatio, startTime } = + await hub.getDetails(1); + const block = await ethers.provider.getBlock("latest"); + expect(block.timestamp).to.be.gt(endTime); + //expect(block.timestamp).to.be.lt(endCooldown); + + await passSeconds(endCooldown.sub(block.timestamp).toNumber() + 1); + reserveWeight = updatedReserveWeight; + updatedReserveWeight = 750000; + + encodedCurveDetails = ethers.utils.defaultAbiCoder.encode( + ["uint32"], + [updatedReserveWeight] + ); + + await hub.initUpdate( + 1, + ethers.constants.AddressZero, + 0, + encodedCurveDetails + ); + const block2 = await ethers.provider.getBlock("latest"); + const details = await hub.getDetails(1); + expect(details.curve).to.equal(updatedBancorABDK.address); + expect(details.targetCurve).to.equal(ethers.constants.AddressZero); + expect(details.endTime).to.be.gt(0); + expect(details.endTime).to.be.gt(block.timestamp); + expect(details.refundRatio).to.to.equal(refundRatio); + expect(details.targetRefundRatio).to.to.equal(0); + expect(details.active).to.be.true; + expect(details.updating).to.be.true; + expect(details.reconfigure).to.be.true; + // we are warming up + expect(block2.timestamp).to.be.lt(details.startTime); + }); + describe("Warmup", () => { + it("Assets received based on initial curveDetails", async () => { + const details = await hub.getDetails(1); + + const currentCurve = await getContractAt( + "BancorABDK", + details.curve + ); + expect(currentCurve.address).to.equal(updatedBancorABDK.address); + const detail = await updatedBancorABDK.getBancorDetails(firstHubId); + expect(detail.targetReserveWeight).to.equal(updatedReserveWeight); + + const tokenDepositedInETH = 100; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + + await token + .connect(account2) + .approve(foundry.address, tokenDeposited); + const balBefore = await meToken.balanceOf(account0.address); + const balDaiBefore = await token.balanceOf(account0.address); + const vaultBalBefore = await token.balanceOf( + singleAssetVault.address + ); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const calculatedReturn = calculateTokenReturned( + tokenDepositedInETH, + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight / MAX_WEIGHT + ); + + const targetReturn = calculateTokenReturned( + tokenDepositedInETH, + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + updatedReserveWeight / MAX_WEIGHT + ); + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account0.address); + const balAfter = await meToken.balanceOf(account0.address); + const vaultBalAfter = await token.balanceOf(singleAssetVault.address); + + expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( + calculatedReturn, + 0.000000000000001 + ); + expect(vaultBalAfter.sub(vaultBalBefore)).to.equal(tokenDeposited); + const balDaiAcc1Before = await token.balanceOf(account1.address); + + //send half burnt by owner + await foundry + .connect(account0) + .burn(meToken.address, balAfter.sub(balBefore), account0.address); + const balDaiAfter = await token.balanceOf(account0.address); + const vaultBalAfterBurn = await token.balanceOf( + singleAssetVault.address + ); + + // we have less DAI in the vault cos they have been sent to the burner + expect(vaultBalAfter.sub(vaultBalAfterBurn)).to.equal( + balDaiAfter.sub(balDaiBefore) + ); + // buyer + const balAcc1Before = await meToken.balanceOf(account1.address); + await token + .connect(account1) + .approve(foundry.address, tokenDeposited); + await foundry + .connect(account1) + .mint(meToken.address, tokenDeposited, account1.address); + const vaultBalAfterMint = await token.balanceOf( + singleAssetVault.address + ); + + expect(vaultBalAfterMint.sub(vaultBalAfterBurn)).to.equal( + tokenDeposited + ); + + const balAcc1After = await meToken.balanceOf(account1.address); + expect(balAcc1After.sub(balAcc1Before)).to.equal( + balAfter.sub(balBefore) + ); + //send half burnt by buyer + await foundry + .connect(account1) + .burn(meToken.address, balAcc1After, account1.address); + const balDaiAcc1After = await token.balanceOf(account1.address); + + const vaultBalAfterBuyerBurn = await token.balanceOf( + singleAssetVault.address + ); + // we have less DAI in the vault cos they have been sent to the burner + expect(vaultBalAfterMint.sub(vaultBalAfterBuyerBurn)).to.equal( + balDaiAcc1After.sub(balDaiAcc1Before.sub(tokenDeposited)) + ); + expect( + Number( + ethers.utils.formatEther( + tokenDeposited.sub(balDaiAcc1Before.sub(balDaiAcc1After)) + ) + ) + ).to.equal((tokenDepositedInETH * refundRatio) / MAX_WEIGHT); + }); + }); + describe("Duration", () => { + before(async () => { + await passHours(1); + }); + it("Assets received for buyer based on weighted average burning full supply ", async () => { + //move forward 3 Days + await passDays(3); + const tokenDepositedInETH = 100; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + await token + .connect(account2) + .approve(foundry.address, tokenDeposited); + const vaultBalBefore = await token.balanceOf( + singleAssetVault.address + ); + + // send token to owner + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account2.address); + const balDaiAfterMint = await token.balanceOf(account2.address); + const balAfter = await meToken.balanceOf(account2.address); + + const vaultBalAfterMint = await token.balanceOf( + singleAssetVault.address + ); + expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal( + tokenDeposited + ); + // burnt by owner + await meToken.connect(account2).approve(foundry.address, balAfter); + + const rawAssetsReturnedFromFoundry = + await foundry.calculateRawAssetsReturned(meToken.address, balAfter); + const balBefore = await meToken.balanceOf(account0.address); + const balDaiBefore = await token.balanceOf(account0.address); + const vaultBalBeforeBurn = await token.balanceOf( + singleAssetVault.address + ); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(balAfter), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight / MAX_WEIGHT + ); + const targetassetsReturned = calculateCollateralReturned( + toETHNumber(balAfter), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + updatedReserveWeight / MAX_WEIGHT + ); + await foundry + .connect(account2) + .burn(meToken.address, balAfter, account2.address); + const balDaiAfterBurn = await token.balanceOf(account2.address); + const meTokenDetailsAfterBurn = await meTokenRegistry.getDetails( + meToken.address + ); + const { + active, + refundRatio, + updating, + startTime, + endTime, + endCooldown, + reconfigure, + targetRefundRatio, + } = await hub.getDetails(1); + expect(active).to.be.true; + expect(updating).to.be.true; + const block = await ethers.provider.getBlock("latest"); + const assetsReturned = + (rawAssetsReturned * refundRatio.toNumber()) / MAX_WEIGHT; + const calcWAvrgRes = weightedAverageSimulation( + rawAssetsReturned, + targetassetsReturned, + startTime.toNumber(), + endTime.toNumber(), + block.timestamp + ); + const calcWithRefundRatio = + (calcWAvrgRes * refundRatio.toNumber()) / MAX_WEIGHT; + // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn + // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); + expect( + toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) + ).to.be.approximately(calcWithRefundRatio, 0.0000000000001); + }); + it("Assets received for buyer based on weighted average not burning full supply ", async () => { + const tokenDepositedInETH = 100; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + await token + .connect(account2) + .approve(foundry.address, tokenDeposited); + const vaultBalBefore = await token.balanceOf( + singleAssetVault.address + ); + + // send token to owner + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account2.address); + const balDaiAfterMint = await token.balanceOf(account2.address); + const balAfter = await meToken.balanceOf(account2.address); + + const vaultBalAfterMint = await token.balanceOf( + singleAssetVault.address + ); + expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal( + tokenDeposited + ); + // burnt by owner + await meToken.connect(account2).approve(foundry.address, balAfter); + + const rawAssetsReturnedFromFoundry = + await foundry.calculateRawAssetsReturned(meToken.address, balAfter); + const balBefore = await meToken.balanceOf(account0.address); + const balDaiBefore = await token.balanceOf(account0.address); + const vaultBalBeforeBurn = await token.balanceOf( + singleAssetVault.address + ); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const metokenToBurn = balAfter.div(2); + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(metokenToBurn), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight / MAX_WEIGHT + ); + const targetassetsReturned = calculateCollateralReturned( + toETHNumber(metokenToBurn), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + updatedReserveWeight / MAX_WEIGHT + ); + + await foundry + .connect(account2) + .burn(meToken.address, metokenToBurn, account2.address); + const balDaiAfterBurn = await token.balanceOf(account2.address); + const meTokenDetailsAfterBurn = await meTokenRegistry.getDetails( + meToken.address + ); + const { + active, + refundRatio, + updating, + startTime, + endTime, + endCooldown, + reconfigure, + targetRefundRatio, + } = await hub.getDetails(1); + expect(active).to.be.true; + expect(updating).to.be.true; + const block = await ethers.provider.getBlock("latest"); + + const calcWAvrgRes = weightedAverageSimulation( + rawAssetsReturned, + targetassetsReturned, + startTime.toNumber(), + endTime.toNumber(), + block.timestamp + ); + const assetsReturned = + (calcWAvrgRes * refundRatio.toNumber()) / MAX_WEIGHT; + const calculatedReturn = ethers.utils + .parseEther(`${assetsReturned}`) + .mul(BigNumber.from(Math.floor(calcWAvrgRes))) + .div(BigNumber.from(10 ** 6)); + // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn + // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); + expect( + toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) + ).to.be.approximately(assetsReturned, 0.000000000000001); + }); + it("Assets received for owner based on weighted average not burning full supply ", async () => { + // TODO: calculate weighted refundRatio based on current time relative to duration + const tokenDepositedInETH = 100; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + await token + .connect(account1) + .transfer(account0.address, ethers.utils.parseEther("100")); + await token.approve(foundry.address, tokenDeposited); + const vaultBalBefore = await token.balanceOf( + singleAssetVault.address + ); + + // send token to owner + await foundry.mint(meToken.address, tokenDeposited, account0.address); + const balDaiAfterMint = await token.balanceOf(account0.address); + const balAfter = await meToken.balanceOf(account0.address); + + const vaultBalAfterMint = await token.balanceOf( + singleAssetVault.address + ); + expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal( + tokenDeposited + ); + // burnt by owner + await meToken.connect(account0).approve(foundry.address, balAfter); + + const rawAssetsReturnedFromFoundry = + await foundry.calculateRawAssetsReturned(meToken.address, balAfter); + const balBefore = await meToken.balanceOf(account0.address); + const balDaiBefore = await token.balanceOf(account0.address); + const vaultBalBeforeBurn = await token.balanceOf( + singleAssetVault.address + ); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const metokenToBurn = balAfter.div(2); + const rawAssetsReturned = calculateCollateralReturned( + toETHNumber(metokenToBurn), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight / MAX_WEIGHT + ); + const targetassetsReturned = calculateCollateralReturned( + toETHNumber(metokenToBurn), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + updatedReserveWeight / MAX_WEIGHT + ); + const meTokenDetailsBeforeBurn = await meTokenRegistry.getDetails( + meToken.address + ); + + await foundry + .connect(account0) + .burn(meToken.address, metokenToBurn, account0.address); + + const balDaiAfterBurn = await token.balanceOf(account0.address); + + const { + active, + refundRatio, + updating, + startTime, + endTime, + endCooldown, + reconfigure, + targetRefundRatio, + } = await hub.getDetails(1); + expect(active).to.be.true; + expect(updating).to.be.true; + const block = await ethers.provider.getBlock("latest"); + // the weighted average on the curve should be applied for owner and buyers + const calcWAvrgRes = weightedAverageSimulation( + rawAssetsReturned, + targetassetsReturned, + startTime.toNumber(), + endTime.toNumber(), + block.timestamp + ); + // but the owner gets a proportional share of the token burnt from the balanced locked + const assetsReturned = + calcWAvrgRes + + (toETHNumber(metokenToBurn) / toETHNumber(meTokenTotalSupply)) * + toETHNumber(meTokenDetailsBeforeBurn.balanceLocked); + + const calculatedReturn = ethers.utils + .parseEther(`${assetsReturned}`) + .mul(BigNumber.from(Math.floor(calcWAvrgRes))) + .div(BigNumber.from(10 ** 6)); + // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn + // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); + expect( + toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) + ).to.be.approximately(assetsReturned, 0.0000000000001); + }); + it("mint(): assets received based on weighted average", async () => { + const tokenDepositedInETH = 100000; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + await token + .connect(tokenHolder) + .transfer(account2.address, tokenDeposited); + await token + .connect(account2) + .approve(foundry.address, tokenDeposited); + const vaultBalBefore = await token.balanceOf( + singleAssetVault.address + ); + await setAutomine(false); + const block = await ethers.provider.getBlock("latest"); + const tokenMinted = await foundry.calculateMeTokensMinted( + meToken.address, + tokenDeposited + ); + const mrd = await meTokenRegistry.getDetails(meToken.address); + const hd = await hub.getDetails(mrd.hubId); + let balBefore = await meToken.balanceOf(account3.address); + // await meToken.connect(account3).transfer(account0.address, balBefore); + //balBefore = await meToken.balanceOf(account3.address); + // expect(balBefore).to.equal(0); + const meTokenTotalSupply = await meToken.totalSupply(); + const meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + const calcTokenReturn = calculateTokenReturned( + tokenDepositedInETH, + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + reserveWeight / MAX_WEIGHT + ); + + const calcTargetTokenReturn = calculateTokenReturned( + tokenDepositedInETH, + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + updatedReserveWeight / MAX_WEIGHT + ); + const { active, updating, startTime, endTime } = await hub.getDetails( + 1 + ); + expect(active).to.be.true; + expect(updating).to.be.true; + + // take into account the time when + // the mint transaction will be included + const calcWAvrgRes = weightedAverageSimulation( + calcTokenReturn, + calcTargetTokenReturn, + startTime.toNumber(), + endTime.toNumber(), + block.timestamp + 1 + ); + + // buyer mint metokens + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account3.address); + // to be precise we set the next block timestamp to be the same of when we ask for tokenMinted + await mineBlock(block.timestamp + 1); + await setAutomine(true); + const balAfter = await meToken.balanceOf(account3.address); + const vaultBalAfterMint = await token.balanceOf( + singleAssetVault.address + ); + expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal( + tokenDeposited + ); + // not that precise because block timestamp is a little bit different of when we asked for tokenMinted + expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( + toETHNumber(tokenMinted), + 0.01 + ); + expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( + calcWAvrgRes, + 0.0000000000001 + ); + }); + }); + describe("Cooldown", () => { + it("initUpdate() cannot be called", async () => { + const { active, updating, endTime, reconfigure } = + await hub.getDetails(1); + expect(active).to.be.true; + expect(updating).to.be.true; + expect(reconfigure).to.be.true; + const block = await ethers.provider.getBlock("latest"); + + //Block.timestamp should be between endtime and endCooldown + // move forward to cooldown + await passSeconds(endTime.sub(block.timestamp).toNumber() + 1); + await expect( + hub.initUpdate( + 1, + bancorABDK.address, + 1000, + ethers.utils.toUtf8Bytes("") + ) + ).to.be.revertedWith("Still cooling down"); + }); + it("burn() and mint() by owner should use the targetCurve", async () => { + // TODO: calculate weighted refundRatio based on current time relative to duration + const tokenDepositedInETH = 100; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + await token + .connect(account1) + .transfer(account0.address, ethers.utils.parseEther("100")); + await token.approve(foundry.address, tokenDeposited); + const vaultBalBefore = await token.balanceOf( + singleAssetVault.address + ); + const balBefore = await meToken.balanceOf(account0.address); + + const tokenMinted = await foundry.calculateMeTokensMinted( + meToken.address, + tokenDeposited + ); + let meTokenTotalSupply = await meToken.totalSupply(); + let meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + // the updated curve should be applied + const calcTargetTokenReturn = calculateTokenReturned( + tokenDepositedInETH, + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + updatedReserveWeight / MAX_WEIGHT + ); + // send token to owner + await foundry.mint(meToken.address, tokenDeposited, account0.address); + const balDaiAfterMint = await token.balanceOf(account0.address); + const balAfter = await meToken.balanceOf(account0.address); + expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( + calcTargetTokenReturn, + 0.0000000000001 + ); + expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( + toETHNumber(tokenMinted), + 0.0000000000001 + ); + const vaultBalAfterMint = await token.balanceOf( + singleAssetVault.address + ); + expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal( + tokenDeposited + ); + // burnt by owner + await meToken.connect(account0).approve(foundry.address, balAfter); + + meTokenTotalSupply = await meToken.totalSupply(); + meTokenDetails = await meTokenRegistry.getDetails(meToken.address); + const metokenToBurn = balAfter.div(2); + const { + active, + updating, + endCooldown, + reconfigure, + curve, + targetCurve, + } = await hub.getDetails(1); + const targetassetsReturned = calculateCollateralReturned( + toETHNumber(metokenToBurn), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + updatedReserveWeight / MAX_WEIGHT + ); + + const meTokenDetailsBeforeBurn = await meTokenRegistry.getDetails( + meToken.address + ); + + await foundry + .connect(account0) + .burn(meToken.address, metokenToBurn, account0.address); + + const balDaiAfterBurn = await token.balanceOf(account0.address); + const currentCurve = await getContractAt( + "BancorABDK", + curve + ); + const hubTargetCurve = await getContractAt( + "BancorABDK", + targetCurve + ); + const block = await ethers.provider.getBlock("latest"); + expect(updatedBancorABDK.address).to.equal(currentCurve.address); + expect(hubTargetCurve.address).to.equal(ethers.constants.AddressZero); + expect(endCooldown).to.be.gt(block.timestamp); + expect(active).to.be.true; + expect(updating).to.be.false; + expect(reconfigure).to.be.false; + + // 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 + + (toETHNumber(metokenToBurn) / toETHNumber(meTokenTotalSupply)) * + toETHNumber(meTokenDetailsBeforeBurn.balanceLocked); + + // we get the calcWAvrgRes percentage of the tokens returned by the Metokens burn + // expect(balDaiAfterBurn.sub(balDaiAfterMint)).to.equal(calculatedReturn); + expect( + toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) + ).to.be.approximately(assetsReturned, 0.00000000001); + }); + it("burn() and mint() by buyer should use the targetCurve", async () => { + // TODO: calculate weighted refundRatio based on current time relative to duration + const tokenDepositedInETH = 10; + const tokenDeposited = ethers.utils.parseEther( + tokenDepositedInETH.toString() + ); + await token + .connect(tokenHolder) + .transfer(account2.address, tokenDeposited); + await token + .connect(account2) + .approve(foundry.address, tokenDeposited); + const vaultBalBefore = await token.balanceOf( + singleAssetVault.address + ); + const balBefore = await meToken.balanceOf(account2.address); + + const tokenMinted = await foundry.calculateMeTokensMinted( + meToken.address, + tokenDeposited + ); + let meTokenTotalSupply = await meToken.totalSupply(); + let meTokenDetails = await meTokenRegistry.getDetails( + meToken.address + ); + // the updated curve should be applied + const calcTargetTokenReturn = calculateTokenReturned( + tokenDepositedInETH, + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + updatedReserveWeight / MAX_WEIGHT + ); + // send token to owner + await foundry + .connect(account2) + .mint(meToken.address, tokenDeposited, account2.address); + const balDaiAfterMint = await token.balanceOf(account2.address); + const balAfter = await meToken.balanceOf(account2.address); + expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( + toETHNumber(tokenMinted), + 0.0000000000001 + ); + expect(toETHNumber(balAfter.sub(balBefore))).to.be.approximately( + calcTargetTokenReturn, + 0.0000000000001 + ); + + const vaultBalAfterMint = await token.balanceOf( + singleAssetVault.address + ); + expect(vaultBalAfterMint.sub(vaultBalBefore)).to.equal( + tokenDeposited + ); + // burnt by owner + await meToken.connect(account2).approve(foundry.address, balAfter); + + meTokenTotalSupply = await meToken.totalSupply(); + meTokenDetails = await meTokenRegistry.getDetails(meToken.address); + const metokenToBurn = balAfter.div(2); + const { + active, + refundRatio, + updating, + endCooldown, + reconfigure, + curve, + targetCurve, + } = await hub.getDetails(1); + const targetassetsReturned = calculateCollateralReturned( + toETHNumber(metokenToBurn), + toETHNumber(meTokenTotalSupply), + toETHNumber(meTokenDetails.balancePooled), + updatedReserveWeight / MAX_WEIGHT + ); + await foundry + .connect(account2) + .burn(meToken.address, metokenToBurn, account2.address); + + const balDaiAfterBurn = await token.balanceOf(account2.address); + const currentCurve = await getContractAt( + "BancorABDK", + curve + ); + const hubTargetCurve = await getContractAt( + "BancorABDK", + targetCurve + ); + const block = await ethers.provider.getBlock("latest"); + expect(updatedBancorABDK.address).to.equal(currentCurve.address); + expect(hubTargetCurve.address).to.equal(ethers.constants.AddressZero); + expect(endCooldown).to.be.gt(block.timestamp); + expect(active).to.be.true; + expect(updating).to.be.false; + expect(reconfigure).to.be.false; + + // as it is a buyer we apply the refund ratio + const assetsReturned = + (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); + expect( + toETHNumber(balDaiAfterBurn.sub(balDaiAfterMint)) + ).to.be.approximately(assetsReturned, 0.00000000001); + }); + }); + }); }); +}; +setup().then(() => { + run(); }); diff --git a/test/utils/hardhatNode.ts b/test/utils/hardhatNode.ts index 0b5e31ae..b1f82b54 100644 --- a/test/utils/hardhatNode.ts +++ b/test/utils/hardhatNode.ts @@ -36,6 +36,20 @@ export async function mineBlock(timestamp: number): Promise { params: [timestamp], }); } + +export async function setNextBlockTimestamp(timestamp: number): Promise { + await network.provider.request({ + method: "evm_setNextBlockTimestamp", + params: [timestamp], + }); +} +export async function setAutomine(automine: boolean): Promise { + await network.provider.request({ + method: "evm_setAutomine", + params: [automine], + }); +} + export async function latestBlockNumber(): Promise<{ number: number; timestamp: number; diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index 87b5af2f..80ad3ed9 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -1,6 +1,6 @@ import { BigNumber } from "@ethersproject/bignumber"; import { Decimal } from "decimal.js"; -import { BaseContract, Contract } from "@ethersproject/contracts"; +import { Contract } from "@ethersproject/contracts"; import { Libraries } from "@nomiclabs/hardhat-ethers/types"; import { ethers } from "hardhat"; @@ -341,9 +341,15 @@ export const calculateTokenReturned = ( balancePooled: number, reserveWeight: number ) => { - const num = 1.0 + collateralAmount / balancePooled; - const res = meTokenSupply * (num ** reserveWeight - 1); - return res; + const _collateralAmount = new Decimal(collateralAmount); + const _balancePooled = new Decimal(balancePooled); + const _reserveWeight = new Decimal(reserveWeight); + const _meTokenSupply = new Decimal(meTokenSupply); + const num = one.add(_collateralAmount.div(_balancePooled)); + + const res = _meTokenSupply.mul(num.pow(_reserveWeight).sub(one)); + //const res = _meTokenSupply * (num ** reserveWeight - 1); + return res.toNumber(); }; // Return = _balancePooled * (1 - (1 - _meTokensBurned/_supply) ^ (1 / (_reserveWeight / 1000000)))