diff --git a/test/polygon/mstable/mstable.helpers.ts b/test/polygon/mstable/mstable.helpers.ts new file mode 100644 index 00000000..9378fa57 --- /dev/null +++ b/test/polygon/mstable/mstable.helpers.ts @@ -0,0 +1,133 @@ +import hre, { ethers } from "hardhat"; +import { IERC20Minimal__factory } from "../../../typechain"; +import { BigNumber as BN } from "ethers"; + +const DEAD_ADDRESS = "0x0000000000000000000000000000000000000001"; +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const DEFAULT_DECIMALS = 18; + +interface TokenData { + tokenAddress: string; + tokenWhaleAddress?: string; + feederPool?: string; +} + +const getToken = (tokenSymbol: string): TokenData => { + switch (tokenSymbol) { + case "mUSD": + return { + tokenAddress: "0xe840b73e5287865eec17d250bfb1536704b43b21", + tokenWhaleAddress: "0x4393b9c542bf79e5235180d6da1915c0f9bc02c3" + }; + + case "DAI": + return { + tokenAddress: "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", + tokenWhaleAddress: "0x49854708A8c42eEB837A97Dd97D597890CEb1334" + }; + case "imUSD": + return { + tokenAddress: "0x5290Ad3d83476CA6A2b178Cd9727eE1EF72432af" + }; + + case "imUSDVault": + return { + tokenAddress: "0x32aBa856Dc5fFd5A56Bcd182b13380e5C855aa29" + }; + + case "FRAX": + return { + tokenAddress: "0x104592a158490a9228070E0A8e5343B499e125D0", + tokenWhaleAddress: "0xAE0f77C239f72da36d4dA20a4bBdaAe4Ca48e03F", + feederPool: "0xb30a907084ac8a0d25dddab4e364827406fd09f0" + }; + + default: + throw new Error(`Token ${tokenSymbol} not supported`); + } +}; + +const sendToken = async (token: string, amount: any, from: string, to: string): Promise => { + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: [from] + }); + const [signer] = await ethers.getSigners(); + const sender = hre.ethers.provider.getSigner(from); + + await signer.sendTransaction({ + to: from, + value: ethers.utils.parseEther("1") + }); + + return await IERC20Minimal__factory.connect(token, sender).transfer(to, amount); +}; + +const fundWallet = async (token: string, amount: any, to: string) => { + const { tokenAddress, tokenWhaleAddress } = getToken(token); + await sendToken(tokenAddress, amount, tokenWhaleAddress!, to); +}; + +const calcMinOut = (amount: BN, slippage: number): BN => { + const value = simpleToExactAmount(1 - slippage); + const minOut = amount.mul(value).div(ethers.BigNumber.from(10).pow(DEFAULT_DECIMALS)); + return minOut; +}; + +const simpleToExactAmount = (amount: number | string | BN, decimals: number | BN = DEFAULT_DECIMALS): BN => { + // Code is largely lifted from the guts of web3 toWei here: + // https://github.com/ethjs/ethjs-unit/blob/master/src/index.js + let amountString = amount.toString(); + const decimalsBN = BN.from(decimals); + + if (decimalsBN.gt(100)) { + throw new Error(`Invalid decimals amount`); + } + + const scale = BN.from(10).pow(decimals); + const scaleString = scale.toString(); + + // Is it negative? + const negative = amountString.substring(0, 1) === "-"; + if (negative) { + amountString = amountString.substring(1); + } + + if (amountString === ".") { + throw new Error(`Error converting number ${amountString} to precise unit, invalid value`); + } + + // Split it into a whole and fractional part + // eslint-disable-next-line prefer-const + let [whole, fraction, ...rest] = amountString.split("."); + if (rest.length > 0) { + throw new Error(`Error converting number ${amountString} to precise unit, too many decimal points`); + } + + if (!whole) { + whole = "0"; + } + if (!fraction) { + fraction = "0"; + } + + if (fraction.length > scaleString.length - 1) { + throw new Error(`Error converting number ${amountString} to precise unit, too many decimal places`); + } + + while (fraction.length < scaleString.length - 1) { + fraction += "0"; + } + + const wholeBN = BN.from(whole); + const fractionBN = BN.from(fraction); + let result = wholeBN.mul(scale).add(fractionBN); + + if (negative) { + result = result.mul("-1"); + } + + return result; +}; + +export { fundWallet, getToken, simpleToExactAmount, DEAD_ADDRESS, ZERO_ADDRESS, calcMinOut }; diff --git a/test/polygon/mstable/mstable.test.ts b/test/polygon/mstable/mstable.test.ts new file mode 100644 index 00000000..3a134ebf --- /dev/null +++ b/test/polygon/mstable/mstable.test.ts @@ -0,0 +1,197 @@ +import { expect } from "chai"; +import hre from "hardhat"; +const { web3, deployments, waffle, ethers } = hre; +const { provider, deployContract } = waffle; + +import { deployAndEnableConnector } from "../../../scripts/tests/deployAndEnableConnector"; +import { buildDSAv2 } from "../../../scripts/tests/buildDSAv2"; +import { encodeSpells } from "../../../scripts/tests/encodeSpells"; +import { getMasterSigner } from "../../../scripts/tests/getMasterSigner"; +import { addLiquidity } from "../../../scripts/tests/addLiquidity"; + +import { addresses } from "../../../scripts/tests/polygon/addresses"; +import { abis } from "../../../scripts/constant/abis"; +import { tokens } from "../../../scripts/tests/polygon/tokens"; +import type { Signer, Contract, BigNumber } from "ethers"; + +import { ConnectV2mStable__factory, IERC20Minimal__factory, IERC20Minimal } from "../../../typechain"; + +import { fundWallet, getToken, simpleToExactAmount, DEAD_ADDRESS, calcMinOut } from "./mstable.helpers"; + +describe("MStable", async () => { + const connectorName = "MStable"; + + let dsaWallet0: Contract; + let masterSigner: Signer; + let instaConnectorsV2: Contract; + let connector: Contract; + + let mUsdToken: IERC20Minimal = IERC20Minimal__factory.connect(getToken("mUSD").tokenAddress, provider); + let daiToken: IERC20Minimal = IERC20Minimal__factory.connect(getToken("DAI").tokenAddress, provider); + let fraxToken: IERC20Minimal = IERC20Minimal__factory.connect(getToken("FRAX").tokenAddress, provider); + let imUsdToken: IERC20Minimal = IERC20Minimal__factory.connect(getToken("imUSD").tokenAddress, provider); + let imUsdVault: IERC20Minimal = IERC20Minimal__factory.connect(getToken("imUSDVault").tokenAddress, provider); + + const wallets = provider.getWallets(); + const [wallet0, wallet1, wallet2, wallet3] = wallets; + + const toEther = (amount: BigNumber) => ethers.utils.formatEther(amount); + + before(async () => { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + // @ts-ignore + jsonRpcUrl: hre.config.networks.hardhat.forking.url, + blockNumber: 23059414 + } + } + ] + }); + + masterSigner = await getMasterSigner(); + instaConnectorsV2 = await ethers.getContractAt(abis.core.connectorsV2, addresses.core.connectorsV2); + connector = await deployAndEnableConnector({ + connectorName, + contractArtifact: ConnectV2mStable__factory, + signer: masterSigner, + connectors: instaConnectorsV2 + }); + + console.log("Connector address", connector.address); + }); + it("should deploy", async () => { + expect(instaConnectorsV2.address).to.be.properAddress; + expect(connector.address).to.be.properAddress; + expect(await masterSigner.getAddress()).to.be.properAddress; + }); + describe("DSA wallet", async () => { + it("Should build DSA v2", async () => { + dsaWallet0 = await buildDSAv2(wallet0.address); + expect(dsaWallet0.address).to.be.properAddress; + }); + it("Deposit ETH and tokens into DSA Wallet", async () => { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: simpleToExactAmount(10) + }); + + const fundAmount = simpleToExactAmount(10000); + + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte(ethers.utils.parseEther("10")); + + await fundWallet("mUSD", fundAmount, dsaWallet0.address); + await fundWallet("DAI", fundAmount, dsaWallet0.address); + await fundWallet("FRAX", fundAmount, dsaWallet0.address); + + expect(await mUsdToken.balanceOf(dsaWallet0.address)).to.be.gte(fundAmount); + expect(await daiToken.balanceOf(dsaWallet0.address)).to.be.gte(fundAmount); + expect(await fraxToken.balanceOf(dsaWallet0.address)).to.be.gte(fundAmount); + + // No deposits prior + expect(await imUsdToken.balanceOf(dsaWallet0.address)).to.be.eq(0); + expect(await imUsdVault.balanceOf(dsaWallet0.address)).to.be.eq(0); + }); + + describe("Main", async () => { + it("Should deposit mUSD to Vault successfully", async () => { + const depositAmount = simpleToExactAmount(100); + + console.log(dsaWallet0.address); + + const mUsdBalanceBefore = await mUsdToken.balanceOf(dsaWallet0.address); + console.log("mUSD balance before: ", toEther(mUsdBalanceBefore)); + + const imUsdVaultBalanceBefore = await imUsdVault.balanceOf(dsaWallet0.address); + console.log("imUSD Vault balance before: ", toEther(imUsdVaultBalanceBefore)); + + const spells = [ + { + connector: connectorName, + method: "deposit", + args: [mUsdToken.address, depositAmount] + } + ]; + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), DEAD_ADDRESS); + + const mUsdBalanceAfter = await mUsdToken.balanceOf(dsaWallet0.address); + console.log("mUSD balance after: ", toEther(mUsdBalanceAfter)); + + const imUsdBalance = await imUsdToken.balanceOf(dsaWallet0.address); + console.log("imUSD balance: ", toEther(imUsdBalance)); + + const imUsdVaultBalance = await imUsdVault.balanceOf(dsaWallet0.address); + console.log("imUSD Vault balance: ", toEther(imUsdVaultBalance)); + + // Should have something in the vault but no imUSD + expect(await imUsdToken.balanceOf(dsaWallet0.address)).to.be.eq(0); + expect(await imUsdVault.balanceOf(dsaWallet0.address)).to.be.gt(0); + expect(mUsdBalanceAfter).to.eq(mUsdBalanceBefore.sub(depositAmount)); + }); + it("Should deposit DAI to Vault successfully (mUSD bAsset)", async () => { + const depositAmount = simpleToExactAmount(100); + const minOut = calcMinOut(depositAmount, 0.02); + + const daiBalanceBefore = await daiToken.balanceOf(dsaWallet0.address); + console.log("DAI balance before: ", toEther(daiBalanceBefore)); + const spells = [ + { + connector: connectorName, + method: "deposit", + args: [daiToken.address, depositAmount, minOut] + } + ]; + const imUsdVaultBalanceBefore = await imUsdVault.balanceOf(dsaWallet0.address); + console.log("imUSD Vault balance before: ", toEther(imUsdVaultBalanceBefore)); + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), DEAD_ADDRESS); + + const daiBalanceAfter = await daiToken.balanceOf(dsaWallet0.address); + console.log("DAI balance after: ", toEther(daiBalanceAfter)); + + const imUsdVaultBalanceAfter = await imUsdVault.balanceOf(dsaWallet0.address); + console.log("imUSD Vault balance after: ", toEther(imUsdVaultBalanceAfter)); + + expect(imUsdVaultBalanceAfter).to.be.gt(imUsdVaultBalanceBefore); + expect(await imUsdToken.balanceOf(dsaWallet0.address)).to.be.eq(0); + expect(daiBalanceAfter).to.eq(daiBalanceBefore.sub(depositAmount)); + }); + it.skip("Should deposit FRAX to Vault successfully (via Feeder Pool)", async () => { + const depositAmount = simpleToExactAmount(100); + const minOut = calcMinOut(depositAmount, 0.02); + + const fraxBalanceBefore = await fraxToken.balanceOf(dsaWallet0.address); + console.log("FRAX balance before: ", toEther(fraxBalanceBefore)); + + const spells = [ + { + connector: connectorName, + method: "deposit", + args: [fraxToken.address, depositAmount, minOut, getToken("FRAX").feederPool] + } + ]; + + const imUsdVaultBalanceBefore = await imUsdVault.balanceOf(dsaWallet0.address); + console.log("imUSD Vault balance before: ", toEther(imUsdVaultBalanceBefore)); + + const tx = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), DEAD_ADDRESS); + + const fraxBalanceAfter = await fraxToken.balanceOf(dsaWallet0.address); + console.log("FRAX balance after: ", toEther(fraxBalanceAfter)); + + const imUsdVaultBalanceAfter = await imUsdVault.balanceOf(dsaWallet0.address); + console.log("imUSD Vault balance after: ", toEther(imUsdVaultBalanceAfter)); + + expect(imUsdVaultBalanceAfter).to.be.gt(imUsdVaultBalanceBefore); + expect(await imUsdToken.balanceOf(dsaWallet0.address)).to.be.eq(0); + expect(fraxBalanceAfter).to.eq(fraxBalanceBefore.sub(depositAmount)); + }); + it.skip("Should withdraw from Vault to mUSD", async () => {}); + it.skip("Should withdraw from Vault to DAI (mUSD bAsset)", async () => {}); + it.skip("Should withdraw from Vault to FRAX (via Feeder Pool)", async () => {}); + }); + }); +});