import hre from "hardhat"; import { ethers } from "hardhat"; import hardhatConfig from "../../../hardhat.config"; // Instadapp deployment and testing helpers import { deployAndEnableConnector } from "../../../scripts/tests/deployAndEnableConnector" import { buildDSAv2 } from "../../../scripts/tests/buildDSAv2" import { encodeSpells } from "../../../scripts/tests/encodeSpells" import { getMasterSigner } from "../../../scripts/tests/getMasterSigner" // Instadapp addresses/ABIs import { addresses } from "../../../scripts/tests/mainnet/addresses"; import { abis } from "../../../scripts/constant/abis"; // Instadapp Liquity Connector artifacts import { ConnectV2Liquity__factory, ConnectV2Basic__factory } from "../../../typechain"; // Instadapp uses a fake address to represent native ETH import { constants } from "../../../scripts/constant/constant"; import type { Signer, Contract, BigNumber } from "ethers"; const LIQUITY_CONNECTOR = "LIQUITY-v1-TEST"; const LUSD_GAS_COMPENSATION = hre.ethers.utils.parseUnits("200", 18); // 200 LUSD gas compensation repaid after loan repayment const LIQUIDATABLE_TROVES_BLOCK_NUMBER = 12478159; // Deterministic block number for tests to run against, if you change this, tests will break. const JUSTIN_SUN_ADDRESS = "0x903d12bf2c57a29f32365917c706ce0e1a84cce3"; // LQTY whale address const LIQUIDATABLE_TROVE_ADDRESS = "0xafbeb4cb97f3b08ec2fe07ef0dac15d37013a347"; // Trove which is liquidatable at blockNumber: LIQUIDATABLE_TROVES_BLOCK_NUMBER // @ts-ignore const MAX_GAS = hardhatConfig.networks.hardhat.blockGasLimit ?? 12000000; // Maximum gas limit (12000000) const INSTADAPP_BASIC_V1_CONNECTOR = "Basic-v1"; const ETH = constants.native_address const openTroveSpell = async ( dsa: any, signer: Signer, depositAmount: any, borrowAmount: any, upperHint: any, lowerHint: any, maxFeePercentage: any ) => { let address = await signer.getAddress(); const openTroveSpell = { connector: LIQUITY_CONNECTOR, method: "open", args: [ depositAmount, maxFeePercentage, borrowAmount, upperHint, lowerHint, [0, 0], [0, 0], ], }; return await dsa .connect(signer) .cast(...encodeSpells([openTroveSpell]), address, { value: depositAmount, }); }; const createDsaTrove = async ( dsa: any, signer: any, liquity: any, depositAmount = hre.ethers.utils.parseEther("5"), borrowAmount = hre.ethers.utils.parseUnits("2000", 18) ) => { const maxFeePercentage = hre.ethers.utils.parseUnits("0.5", 18); // 0.5% max fee const { upperHint, lowerHint } = await getTroveInsertionHints( depositAmount, borrowAmount, liquity ); return await openTroveSpell( dsa, signer, depositAmount, borrowAmount, upperHint, lowerHint, maxFeePercentage ); }; const sendToken = async (token: any, amount: any, from: any, to: any) => { await hre.network.provider.request({ method: "hardhat_impersonateAccount", params: [from], }); const signer = hre.ethers.provider.getSigner(from); return await token.connect(signer).transfer(to, amount); }; const resetInitialState = async (walletAddress: any, contracts: any, isDebug = false) => { const liquity = await deployAndConnect(contracts, isDebug); const dsa = await buildDSAv2(walletAddress); return [liquity, dsa]; }; const resetHardhatBlockNumber = async (blockNumber: number) => { return await hre.network.provider.request({ method: "hardhat_reset", params: [ { forking: { // @ts-ignore jsonRpcUrl: hre.config.networks.hardhat.forking.url, blockNumber, }, }, ], }); }; const deployAndConnect = async (contracts: any, isDebug = false) => { // Pin Liquity tests to a particular block number to create deterministic state (Ether price etc.) await resetHardhatBlockNumber(LIQUIDATABLE_TROVES_BLOCK_NUMBER); type Liquidity = { troveManager: Contract, borrowerOperations: Contract, stabilityPool: Contract, lusdToken: Contract, lqtyToken: Contract, activePool: Contract, priceFeed: Contract, hintHelpers: Contract, sortedTroves: Contract, staking: Contract, collSurplus: Contract, }; const liquity = {} as Liquidity const masterSigner = await getMasterSigner(); const instaConnectorsV2 = await ethers.getContractAt( abis.core.connectorsV2, addresses.core.connectorsV2 ); const connector = await deployAndEnableConnector({ connectorName: LIQUITY_CONNECTOR, contractArtifact: ConnectV2Liquity__factory, signer: masterSigner, connectors: instaConnectorsV2, }); isDebug && console.log(`${LIQUITY_CONNECTOR} Connector address`, connector.address); const basicConnector = await deployAndEnableConnector({ connectorName: "Basic-v1", contractArtifact: ConnectV2Basic__factory, signer: masterSigner, connectors: instaConnectorsV2, }); isDebug && console.log("Basic-v1 Connector address", basicConnector.address); liquity.troveManager = new ethers.Contract( contracts.TROVE_MANAGER_ADDRESS, contracts.TROVE_MANAGER_ABI, ethers.provider ); liquity.borrowerOperations = new ethers.Contract( contracts.BORROWER_OPERATIONS_ADDRESS, contracts.BORROWER_OPERATIONS_ABI, ethers.provider ); liquity.stabilityPool = new ethers.Contract( contracts.STABILITY_POOL_ADDRESS, contracts.STABILITY_POOL_ABI, ethers.provider ); liquity.lusdToken = new ethers.Contract( contracts.LUSD_TOKEN_ADDRESS, contracts.LUSD_TOKEN_ABI, ethers.provider ); liquity.lqtyToken = new ethers.Contract( contracts.LQTY_TOKEN_ADDRESS, contracts.LQTY_TOKEN_ABI, ethers.provider ); liquity.activePool = new ethers.Contract( contracts.ACTIVE_POOL_ADDRESS, contracts.ACTIVE_POOL_ABI, ethers.provider ); liquity.priceFeed = new ethers.Contract( contracts.PRICE_FEED_ADDRESS, contracts.PRICE_FEED_ABI, ethers.provider ); liquity.hintHelpers = new ethers.Contract( contracts.HINT_HELPERS_ADDRESS, contracts.HINT_HELPERS_ABI, ethers.provider ); liquity.sortedTroves = new ethers.Contract( contracts.SORTED_TROVES_ADDRESS, contracts.SORTED_TROVES_ABI, ethers.provider ); liquity.staking = new ethers.Contract( contracts.STAKING_ADDRESS, contracts.STAKING_ABI, ethers.provider ); liquity.collSurplus = new ethers.Contract( contracts.COLL_SURPLUS_ADDRESS, contracts.COLL_SURPLUS_ABI, ethers.provider ); return liquity; }; const getTroveInsertionHints = async (depositAmount: BigNumber, borrowAmount: BigNumber, liquity: any) => { const nominalCR = await liquity.hintHelpers.computeNominalCR( depositAmount, borrowAmount ); const { hintAddress, latestRandomSeed, } = await liquity.hintHelpers.getApproxHint(nominalCR, 50, 1298379, { gasLimit: MAX_GAS, }); randomSeed = latestRandomSeed; const { 0: upperHint, 1: lowerHint, } = await liquity.sortedTroves.findInsertPosition( nominalCR, hintAddress, hintAddress, { gasLimit: MAX_GAS, } ); return { upperHint, lowerHint, }; }; let randomSeed = 4223; const getRedemptionHints = async (amount: any, liquity: any) => { const ethPrice = await liquity.priceFeed.callStatic.fetchPrice(); const [ firstRedemptionHint, partialRedemptionHintNicr, ] = await liquity.hintHelpers.getRedemptionHints(amount, ethPrice, 0); const { hintAddress, latestRandomSeed, } = await liquity.hintHelpers.getApproxHint( partialRedemptionHintNicr, 50, randomSeed, { gasLimit: MAX_GAS, } ); randomSeed = latestRandomSeed; const { 0: upperHint, 1: lowerHint, } = await liquity.sortedTroves.findInsertPosition( partialRedemptionHintNicr, hintAddress, hintAddress, { gasLimit: MAX_GAS, } ); return { partialRedemptionHintNicr, firstRedemptionHint, upperHint, lowerHint, }; }; const redeem = async (amount: any, from: any, wallet: { address: any; }, liquity: any) => { await sendToken(liquity.lusdToken, amount, from, wallet.address); const { partialRedemptionHintNicr, firstRedemptionHint, upperHint, lowerHint, } = await getRedemptionHints(amount, liquity); const maxFeePercentage = ethers.utils.parseUnits("0.5", 18); // 0.5% max fee return await liquity.troveManager .connect(wallet) .redeemCollateral( amount, firstRedemptionHint, upperHint, lowerHint, partialRedemptionHintNicr, 0, maxFeePercentage, { gasLimit: MAX_GAS, // permit max gas } ); }; export default { deployAndConnect, resetInitialState, createDsaTrove, sendToken, getTroveInsertionHints, getRedemptionHints, redeem, LIQUITY_CONNECTOR, LUSD_GAS_COMPENSATION, JUSTIN_SUN_ADDRESS, LIQUIDATABLE_TROVE_ADDRESS, MAX_GAS, INSTADAPP_BASIC_V1_CONNECTOR, ETH, };