diff --git a/simulations/vip-599/bscmainnet.ts b/simulations/vip-599/bscmainnet.ts new file mode 100644 index 000000000..932cd888f --- /dev/null +++ b/simulations/vip-599/bscmainnet.ts @@ -0,0 +1,429 @@ +import { expect } from "chai"; +import { BigNumber, Contract, Signer } from "ethers"; +import { parseUnits } from "ethers/lib/utils"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { initMainnetUser, setMaxStalePeriodInBinanceOracle, setMaxStalePeriodInChainlinkOracle } from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import { ALL_MARKETS, Actions, CORE_POOL_ID, TUSD_MARKET, vLINK, vip599 } from "../../vips/vip-599/bscmainnet"; +import COMPTROLLER_ABI from "../vip-587/abi/Comptroller.json"; +import ERC20_ABI from "../vip-587/abi/ERC20.json"; +import VTOKEN_ABI from "../vip-587/abi/VToken.json"; + +const { bscmainnet } = NETWORK_ADDRESSES; + +const BLOCK_NUMBER = 85550618; + +// E-Mode pool IDs (created in VIP-587) +const EMODE_LINK_POOL_ID = 4; + +// Stablecoin addresses (Core Pool) +const USDT = "0x55d398326f99059fF775485246999027B3197955"; +const vUSDT = "0xfD5840Cd36d94D7229439859C0112a4185BC0255"; +// LINK underlying +const LINK = "0xF8A0BF9cF54Bb92F17374d9e9A321E6a111a51bD"; +const LINK_WHALE = "0xF977814e90dA44bFA03b6295A0616a897441aceC"; + +// TUSD underlying +const TUSD = "0x40af3827F39D0EAcBF4A168f8D4ee67c121D11c9"; +const vTUSD = "0xBf762cd5991cA1DCdDaC9ae5C638F5B5Dc3Bee6E"; +const TUSD_WHALE = "0xF977814e90dA44bFA03b6295A0616a897441aceC"; + +// Map vToken address to underlying info (from vip-587 simulations) +const MARKET_INFO: Record = { + // LINK + "0x650b940a1033B8A1b1873f78730FcFC73ec11f1f": { + underlying: LINK, + whale: LINK_WHALE, + decimals: 18, + }, + // UNI + "0x27FF564707786720C71A2e5c1490A63266683612": { + underlying: "0xBf5140A22578168FD562DCcF235E5D43A02ce9B1", + whale: "0x27FF564707786720C71A2e5c1490A63266683612", + decimals: 18, + }, + // AAVE + "0x26DA28954763B92139ED49283625ceCAf52C6f94": { + underlying: "0xfb6115445Bff7b52FeB98650C87f44907E58f802", + whale: "0xF977814e90dA44bFA03b6295A0616a897441aceC", + decimals: 18, + }, + // DOGE + "0xec3422Ef92B2fb59e84c8B02Ba73F1fE84Ed8D71": { + underlying: "0xba2ae424d960c26247dd6c32edc70b295c744c43", + whale: "0x0000000000000000000000000000000000001004", + decimals: 8, + }, + // BCH + "0x5F0388EBc2B94FA8E123F404b79cCF5f40b29176": { + underlying: "0x8fF795a6F4D97E7887C79beA79aba5cc76444aDf", + whale: "0xF977814e90dA44bFA03b6295A0616a897441aceC", + decimals: 18, + }, + // TWT + "0x4d41a36D04D97785bcEA57b057C412b278e6Edcc": { + underlying: "0x4B0F1812e5Df2A09796481Ff14017e6005508003", + whale: "0x8808390062EBcA540ff10ee43DB60125bB061621", + decimals: 18, + }, + // ADA + "0x9A0AF7FDb2065Ce470D72664DE73cAE409dA28Ec": { + underlying: "0x3EE2200Efb3400fAbB9AacF31297cBdD1d435D47", + whale: "0x835678a611B28684005a5e2233695fB6cbbB0007", + decimals: 18, + }, + // LTC + "0x57A5297F2cB2c0AaC9D554660acd6D385Ab50c6B": { + underlying: "0x4338665CBB7B2485A8855A139b75D5e34AB0DB94", + whale: "0xF977814e90dA44bFA03b6295A0616a897441aceC", + decimals: 18, + }, + // FIL + "0xf91d58b5aE142DAcC749f58A49FCBac340Cb0343": { + underlying: "0x0D8Ce2A99Bb6e3B7Db580eD848240e4a0F9aE153", + whale: "0xF977814e90dA44bFA03b6295A0616a897441aceC", + decimals: 18, + }, + // TRX + "0xC5D3466aA484B040eE977073fcF337f2c00071c1": { + underlying: "0xCE7de646e7208a4Ef112cb6ed5038FA6cC6b12e3", + whale: "0xCa266910d92a313E5F9eb1AfFC462bcbb7d9c4A9", + decimals: 6, + }, + // DOT + "0x1610bc33319e9398de5f57B33a5b184c806aD217": { + underlying: "0x7083609fCE4d1d8Dc0C979AAb8c869Ea2C873402", + whale: "0xF977814e90dA44bFA03b6295A0616a897441aceC", + decimals: 18, + }, + // THE + "0x86e06EAfa6A1eA631Eab51DE500E3D474933739f": { + underlying: "0xF4C8E32EaDEC4BFe97E0F595AdD0f4450a863a11", + whale: "0xfBBF371C9B0B994EebFcC977CEf603F7f31c070D", + decimals: 18, + }, + // TUSD + [vTUSD]: { + underlying: TUSD, + whale: TUSD_WHALE, + decimals: 18, + }, +}; + +// Oracle staleness period (100 years) +const MAX_STALE_PERIOD = 3153600000; + +forking(BLOCK_NUMBER, async () => { + let comptroller: Contract; + let linkToken: Contract; + let vLinkToken: Contract; + let usdtToken: Contract; + let vUsdtToken: Contract; + + // Pre-VIP borrower: supplies LINK collateral + borrows LINK in Core Pool (before VIP disables borrowing) + let corePoolBorrower: Signer; + let corePoolBorrowerAddress: string; + const linkBorrowAmount = parseUnits("1", 18); + + before(async () => { + comptroller = new ethers.Contract(bscmainnet.UNITROLLER, COMPTROLLER_ABI, ethers.provider); + linkToken = new ethers.Contract(LINK, ERC20_ABI, ethers.provider); + vLinkToken = new ethers.Contract(vLINK, VTOKEN_ABI, ethers.provider); + usdtToken = new ethers.Contract(USDT, ERC20_ABI, ethers.provider); + vUsdtToken = new ethers.Contract(vUSDT, VTOKEN_ABI, ethers.provider); + + // Set oracle staleness for assets used in functional tests + const oracleAssets = [ + { underlying: LINK, symbol: "LINK" }, + { underlying: USDT, symbol: "USDT" }, + ]; + for (const asset of oracleAssets) { + await setMaxStalePeriodInChainlinkOracle( + bscmainnet.CHAINLINK_ORACLE, + asset.underlying, + ethers.constants.AddressZero, + bscmainnet.NORMAL_TIMELOCK, + MAX_STALE_PERIOD, + ); + await setMaxStalePeriodInChainlinkOracle( + bscmainnet.REDSTONE_ORACLE, + asset.underlying, + ethers.constants.AddressZero, + bscmainnet.NORMAL_TIMELOCK, + MAX_STALE_PERIOD, + ); + await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, asset.symbol, MAX_STALE_PERIOD); + } + + // Setup Core Pool borrower BEFORE VIP (while borrowing is still allowed) + corePoolBorrowerAddress = "0x0000000000000000000000000000000000000099"; + corePoolBorrower = await initMainnetUser(corePoolBorrowerAddress, parseUnits("10", 18)); + + const linkWhale = await initMainnetUser(LINK_WHALE, parseUnits("1", 18)); + const supplyAmount = parseUnits("100", 18); + await linkToken.connect(linkWhale).transfer(corePoolBorrowerAddress, supplyAmount); + await linkToken.connect(corePoolBorrower).approve(vLINK, supplyAmount); + await comptroller.connect(corePoolBorrower).enterMarkets([vLINK]); + await vLinkToken.connect(corePoolBorrower).mint(supplyAmount); + + // Borrow a small amount of LINK against LINK collateral (CF=0.63 pre-VIP) + await vLinkToken.connect(corePoolBorrower).borrow(linkBorrowAmount); + }); + + describe("Pre-VIP behavior", async () => { + for (const market of ALL_MARKETS) { + it(`${market.symbol} should have non-zero collateral factor in Core Pool`, async () => { + const data = await comptroller.poolMarkets(CORE_POOL_ID, market.vToken); + expect(data.collateralFactorMantissa).to.be.gt(0); + }); + + it(`${market.symbol} should have borrowing enabled in Core Pool`, async () => { + const data = await comptroller.poolMarkets(CORE_POOL_ID, market.vToken); + expect(data.isBorrowAllowed).to.equal(true); + }); + } + + it("Core Pool borrower should have an active LINK borrow", async () => { + const borrowBalance = await vLinkToken.callStatic.borrowBalanceCurrent(corePoolBorrowerAddress); + expect(borrowBalance).to.be.gte(linkBorrowAmount); + }); + }); + + testVip("VIP-599", await vip599(), { + callbackAfterExecution: async txResponse => { + await expect(txResponse).to.emit(comptroller, "NewCollateralFactor"); + await expect(txResponse).to.emit(comptroller, "BorrowAllowedUpdated"); + }, + }); + + describe("Post-VIP behavior", async () => { + for (const market of ALL_MARKETS) { + describe(`${market.symbol}`, () => { + it("should have collateral factor set to 0 in Core Pool", async () => { + const data = await comptroller.poolMarkets(CORE_POOL_ID, market.vToken); + expect(data.collateralFactorMantissa).to.equal(0); + }); + + it("should keep liquidation threshold unchanged", async () => { + const data = await comptroller.poolMarkets(CORE_POOL_ID, market.vToken); + expect(data.liquidationThresholdMantissa).to.equal(market.liquidationThreshold); + }); + + it("should have borrowing disabled in Core Pool", async () => { + const data = await comptroller.poolMarkets(CORE_POOL_ID, market.vToken); + expect(data.isBorrowAllowed).to.equal(false); + }); + }); + } + + describe("TUSD should have mint action paused", () => { + it("should have mint action paused", async () => { + const paused = await comptroller.actionPaused(TUSD_MARKET.vToken, Actions.MINT); + expect(paused).to.be.true; + }); + + it("should revert when trying to mint vTUSD", async () => { + const tusdWhale = await initMainnetUser(TUSD_WHALE, parseUnits("1", 18)); + const tusdToken = new ethers.Contract(TUSD, ERC20_ABI, ethers.provider); + const vTusdToken = new ethers.Contract(vTUSD, VTOKEN_ABI, ethers.provider); + const mintAmount = parseUnits("1", 18); + + await tusdToken.connect(tusdWhale).approve(vTUSD, mintAmount); + await expect(vTusdToken.connect(tusdWhale).mint(mintAmount)).to.be.reverted; + }); + }); + + describe("Functional tests: borrow should be blocked", () => { + let user: Signer; + let userAddress: string; + + before(async () => { + userAddress = "0x000000000000000000000000000000000000dEaD"; + user = await initMainnetUser(userAddress, parseUnits("10", 18)); + }); + + for (const market of ALL_MARKETS) { + const info = MARKET_INFO[market.vToken]; + if (!info) continue; + + it(`${market.symbol} should revert when trying to borrow`, async () => { + const vToken = new ethers.Contract(market.vToken, VTOKEN_ABI, ethers.provider); + const borrowAmount = parseUnits("1", info.decimals); + + await expect(vToken.connect(user).borrow(borrowAmount)).to.be.revertedWithCustomError( + comptroller, + "BorrowNotAllowedInPool", + ); + }); + } + }); + + describe("E-Mode pools remain functional after Core Pool changes", () => { + let emodeUser: Signer; + let emodeUserAddress: string; + + before(async () => { + emodeUserAddress = "0x0000000000000000000000000000000000000001"; + emodeUser = await initMainnetUser(emodeUserAddress, parseUnits("10", 18)); + + // Fund user with LINK + const linkWhale = await initMainnetUser(LINK_WHALE, parseUnits("1", 18)); + await linkToken.connect(linkWhale).transfer(emodeUserAddress, parseUnits("50", 18)); + + // Enter LINK E-Mode pool (pool 4) + await comptroller.connect(emodeUser).enterPool(EMODE_LINK_POOL_ID); + + // Supply LINK as collateral in E-Mode + await linkToken.connect(emodeUser).approve(vLINK, parseUnits("50", 18)); + await comptroller.connect(emodeUser).enterMarkets([vLINK]); + await vLinkToken.connect(emodeUser).mint(parseUnits("50", 18)); + }); + + it("should allow supplying LINK in E-Mode pool", async () => { + expect(await vLinkToken.balanceOf(emodeUserAddress)).to.be.gt(0); + }); + + it("LINK should retain non-zero CF in E-Mode pool (pool 4)", async () => { + const data = await comptroller.poolMarkets(EMODE_LINK_POOL_ID, vLINK); + expect(data.collateralFactorMantissa).to.equal(parseUnits("0.63", 18)); + expect(data.isBorrowAllowed).to.equal(true); + }); + + it("should allow borrowing USDT against LINK collateral in E-Mode", async () => { + const borrowAmount = parseUnits("1", 18); + await expect(vUsdtToken.connect(emodeUser).borrow(borrowAmount)).to.not.be.reverted; + expect(await usdtToken.balanceOf(emodeUserAddress)).to.be.gte(borrowAmount); + }); + + it("should have positive account liquidity in E-Mode", async () => { + const [error, liquidity, shortfall] = await comptroller.getAccountLiquidity(emodeUserAddress); + expect(error).to.equal(0); + expect(liquidity).to.be.gt(0); + expect(shortfall).to.equal(0); + }); + }); + + describe("Core Pool: supply works, LT > 0 increases health factor", () => { + let supplier: Signer; + let supplierAddress: string; + + before(async () => { + supplierAddress = "0x0000000000000000000000000000000000000002"; + supplier = await initMainnetUser(supplierAddress, parseUnits("10", 18)); + + // Fund user with LINK + const linkWhale = await initMainnetUser(LINK_WHALE, parseUnits("1", 18)); + await linkToken.connect(linkWhale).transfer(supplierAddress, parseUnits("50", 18)); + }); + + it("should allow minting vLINK (supply) in Core Pool", async () => { + const mintAmount = parseUnits("50", 18); + await linkToken.connect(supplier).approve(vLINK, mintAmount); + await comptroller.connect(supplier).enterMarkets([vLINK]); + + await expect(vLinkToken.connect(supplier).mint(mintAmount)).to.not.be.reverted; + expect(await vLinkToken.balanceOf(supplierAddress)).to.be.gt(0); + }); + + it("should have positive health margin despite CF=0 (getAccountLiquidity uses LT)", async () => { + // getAccountLiquidity uses LT for the health calculation, NOT CF. + // With CF=0 and LT=0.63, supply still contributes to account health. + const [error, liquidity, shortfall] = await comptroller.getAccountLiquidity(supplierAddress); + expect(error).to.equal(0); + expect(liquidity).to.be.gt(0); // LT > 0 → positive health margin + expect(shortfall).to.equal(0); + }); + + it("supplying more should increase health factor for existing borrowers (LT > 0)", async () => { + // The Core Pool borrower (set up pre-VIP) has 100 LINK supply + 1 LINK borrow. + // After VIP, CF=0 but LT=0.63 — supply still contributes to health factor. + const [error, , shortfall] = await comptroller.getAccountLiquidity(corePoolBorrowerAddress); + expect(error).to.equal(0); + expect(shortfall).to.equal(0); // Position is healthy (LT-based health margin) + + // Record health margin before additional supply + const [, liquidityBefore] = await comptroller.getAccountLiquidity(corePoolBorrowerAddress); + expect(liquidityBefore).to.be.gt(0); + + // Supply additional LINK to the borrower's position + const additionalAmount = parseUnits("50", 18); + const linkWhale = await initMainnetUser(LINK_WHALE, parseUnits("1", 18)); + await linkToken.connect(linkWhale).transfer(corePoolBorrowerAddress, additionalAmount); + await linkToken.connect(corePoolBorrower).approve(vLINK, additionalAmount); + await vLinkToken.connect(corePoolBorrower).mint(additionalAmount); + + // Health margin should increase: more supply with LT > 0 → better health + const [errorAfter, liquidityAfter, shortfallAfter] = await comptroller.getAccountLiquidity( + corePoolBorrowerAddress, + ); + expect(errorAfter).to.equal(0); + expect(shortfallAfter).to.equal(0); + expect(liquidityAfter).to.be.gt(liquidityBefore); + }); + }); + + describe("Core Pool: existing debt can be repaid", () => { + it("should still have an active LINK borrow after VIP", async () => { + const borrowBalance = await vLinkToken.callStatic.borrowBalanceCurrent(corePoolBorrowerAddress); + expect(borrowBalance).to.be.gt(0); + }); + + it("should allow repaying existing LINK borrow", async () => { + // Ensure borrower has enough LINK to cover debt + interest + const borrowBalance: BigNumber = await vLinkToken.callStatic.borrowBalanceCurrent(corePoolBorrowerAddress); + const buffer = borrowBalance.add(parseUnits("1", 18)); // extra buffer for interest accrual + const currentBalance: BigNumber = await linkToken.balanceOf(corePoolBorrowerAddress); + if (currentBalance.lt(buffer)) { + const linkWhale = await initMainnetUser(LINK_WHALE, parseUnits("1", 18)); + await linkToken.connect(linkWhale).transfer(corePoolBorrowerAddress, buffer.sub(currentBalance)); + } + + // Use MaxUint256 to repay exact outstanding balance (avoids dust from interest accrual) + await linkToken.connect(corePoolBorrower).approve(vLINK, ethers.constants.MaxUint256); + await expect(vLinkToken.connect(corePoolBorrower).repayBorrow(ethers.constants.MaxUint256)).to.not.be.reverted; + + const borrowBalanceAfter = await vLinkToken.callStatic.borrowBalanceCurrent(corePoolBorrowerAddress); + expect(borrowBalanceAfter).to.equal(0); + }); + + it("should NOT allow new borrows after repaying", async () => { + await expect(vLinkToken.connect(corePoolBorrower).borrow(parseUnits("1", 18))).to.be.revertedWithCustomError( + comptroller, + "BorrowNotAllowedInPool", + ); + }); + }); + + describe("Core Pool: users can still withdraw", () => { + it("should allow redeeming vLINK (withdraw) from Core Pool", async () => { + // The Core Pool borrower has repaid all debt above, so they can freely redeem + const vTokenBalance: BigNumber = await vLinkToken.balanceOf(corePoolBorrowerAddress); + expect(vTokenBalance).to.be.gt(0); + + const linkBalanceBefore: BigNumber = await linkToken.balanceOf(corePoolBorrowerAddress); + await expect(vLinkToken.connect(corePoolBorrower).redeem(vTokenBalance)).to.not.be.reverted; + + const linkBalanceAfter: BigNumber = await linkToken.balanceOf(corePoolBorrowerAddress); + expect(linkBalanceAfter).to.be.gt(linkBalanceBefore); + + const vTokenBalanceAfter = await vLinkToken.balanceOf(corePoolBorrowerAddress); + expect(vTokenBalanceAfter).to.equal(0); + }); + + it("should allow partial redeem via redeemUnderlying", async () => { + // Use the supplier (from borrow-power test) who still has vLINK + const supplierAddress = "0x0000000000000000000000000000000000000002"; + const supplier = await initMainnetUser(supplierAddress, parseUnits("1", 18)); + + const redeemAmount = parseUnits("10", 18); + const linkBalanceBefore: BigNumber = await linkToken.balanceOf(supplierAddress); + await expect(vLinkToken.connect(supplier).redeemUnderlying(redeemAmount)).to.not.be.reverted; + + const linkBalanceAfter: BigNumber = await linkToken.balanceOf(supplierAddress); + expect(linkBalanceAfter.sub(linkBalanceBefore)).to.equal(redeemAmount); + }); + }); + }); +}); diff --git a/simulations/vip-599/bsctestnet.ts b/simulations/vip-599/bsctestnet.ts new file mode 100644 index 000000000..6dc7c1759 --- /dev/null +++ b/simulations/vip-599/bsctestnet.ts @@ -0,0 +1,62 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { forking, testVip } from "src/vip-framework"; + +import { CORE_POOL_ID, MARKETS_TO_DISABLE, vip599 } from "../../vips/vip-599/bsctestnet"; +import COMPTROLLER_ABI from "../vip-587/abi/Comptroller.json"; + +const { bsctestnet } = NETWORK_ADDRESSES; + +const BLOCK_NUMBER = 94679311; + +forking(BLOCK_NUMBER, async () => { + let comptroller: Contract; + + before(async () => { + comptroller = new ethers.Contract(bsctestnet.UNITROLLER, COMPTROLLER_ABI, ethers.provider); + }); + + describe("Pre-VIP behavior", async () => { + for (const market of MARKETS_TO_DISABLE) { + it(`${market.symbol} should have non-zero collateral factor in Core Pool`, async () => { + const data = await comptroller.poolMarkets(CORE_POOL_ID, market.vToken); + expect(data.collateralFactorMantissa).to.be.gt(0); + }); + + it(`${market.symbol} should have borrowing enabled in Core Pool`, async () => { + const data = await comptroller.poolMarkets(CORE_POOL_ID, market.vToken); + expect(data.isBorrowAllowed).to.equal(true); + }); + } + }); + + testVip("VIP-599", await vip599(), { + callbackAfterExecution: async txResponse => { + await expect(txResponse).to.emit(comptroller, "NewCollateralFactor"); + await expect(txResponse).to.emit(comptroller, "BorrowAllowedUpdated"); + }, + }); + + describe("Post-VIP behavior", async () => { + for (const market of MARKETS_TO_DISABLE) { + describe(`${market.symbol}`, () => { + it("should have collateral factor set to 0 in Core Pool", async () => { + const data = await comptroller.poolMarkets(CORE_POOL_ID, market.vToken); + expect(data.collateralFactorMantissa).to.equal(0); + }); + + it("should keep liquidation threshold unchanged", async () => { + const data = await comptroller.poolMarkets(CORE_POOL_ID, market.vToken); + expect(data.liquidationThresholdMantissa).to.equal(market.liquidationThreshold); + }); + + it("should have borrowing disabled in Core Pool", async () => { + const data = await comptroller.poolMarkets(CORE_POOL_ID, market.vToken); + expect(data.isBorrowAllowed).to.equal(false); + }); + }); + } + }); +}); diff --git a/vips/vip-599/bscmainnet.ts b/vips/vip-599/bscmainnet.ts new file mode 100644 index 000000000..9cd9023a2 --- /dev/null +++ b/vips/vip-599/bscmainnet.ts @@ -0,0 +1,152 @@ +import { parseUnits } from "ethers/lib/utils"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +const { bscmainnet } = NETWORK_ADDRESSES; + +// Core Pool vToken addresses for migrated assets (from vip-587 part 1) +export const vLINK = "0x650b940a1033B8A1b1873f78730FcFC73ec11f1f"; +export const vUNI = "0x27FF564707786720C71A2e5c1490A63266683612"; +export const vAAVE = "0x26DA28954763B92139ED49283625ceCAf52C6f94"; +export const vDOGE = "0xec3422Ef92B2fb59e84c8B02Ba73F1fE84Ed8D71"; +export const vBCH = "0x5F0388EBc2B94FA8E123F404b79cCF5f40b29176"; +export const vTWT = "0x4d41a36D04D97785bcEA57b057C412b278e6Edcc"; +export const vADA = "0x9A0AF7FDb2065Ce470D72664DE73cAE409dA28Ec"; + +// Core Pool vToken addresses for migrated assets (from vip-588 part 2) +export const vLTC = "0x57A5297F2cB2c0AaC9D554660acd6D385Ab50c6B"; +export const vFIL = "0xf91d58b5aE142DAcC749f58A49FCBac340Cb0343"; +export const vTRX = "0xC5D3466aA484B040eE977073fcF337f2c00071c1"; +export const vDOT = "0x1610bc33319e9398de5f57B33a5b184c806aD217"; +export const vTHE = "0x86e06EAfa6A1eA631Eab51DE500E3D474933739f"; + +// TUSD — pausing in Core Pool (no e-mode migration) +export const vTUSD = "0xBf762cd5991cA1DCdDaC9ae5C638F5B5Dc3Bee6E"; + +// Migrated assets — CF and borrow disabled in Core Pool (still active in E-Mode pools) +export const MARKETS_TO_DISABLE = [ + { symbol: "LINK", vToken: vLINK, liquidationThreshold: parseUnits("0.63", 18) }, + { symbol: "UNI", vToken: vUNI, liquidationThreshold: parseUnits("0.55", 18) }, + { symbol: "AAVE", vToken: vAAVE, liquidationThreshold: parseUnits("0.55", 18) }, + { symbol: "DOGE", vToken: vDOGE, liquidationThreshold: parseUnits("0.43", 18) }, + { symbol: "BCH", vToken: vBCH, liquidationThreshold: parseUnits("0.6", 18) }, + { symbol: "TWT", vToken: vTWT, liquidationThreshold: parseUnits("0.5", 18) }, + { symbol: "ADA", vToken: vADA, liquidationThreshold: parseUnits("0.63", 18) }, + { symbol: "LTC", vToken: vLTC, liquidationThreshold: parseUnits("0.63", 18) }, + { symbol: "FIL", vToken: vFIL, liquidationThreshold: parseUnits("0.63", 18) }, + { symbol: "TRX", vToken: vTRX, liquidationThreshold: parseUnits("0.525", 18) }, + { symbol: "DOT", vToken: vDOT, liquidationThreshold: parseUnits("0.65", 18) }, + { symbol: "THE", vToken: vTHE, liquidationThreshold: parseUnits("0.53", 18) }, +]; + +// TUSD — fully paused in Core Pool (no e-mode migration, safe to pause actions) +export const TUSD_MARKET = { symbol: "TUSD", vToken: vTUSD, liquidationThreshold: parseUnits("0.75", 18) }; + +export const Actions = { + MINT: 0, +}; + +// All markets whose CF and borrow will be disabled +export const ALL_MARKETS = [...MARKETS_TO_DISABLE, TUSD_MARKET]; + +export const CORE_POOL_ID = 0; + +export const vip599 = () => { + const meta = { + version: "v2", + title: "VIP-599 [BNB Chain] Phase 2 Asset Migration to Isolation Mode Pools + Pause TUSD", + description: `This proposal implements **Phase 2** of the asset migration discussed in the [Venus community forum](https://community.venus.io/t/asset-migration-from-core-pool-to-isolated-e-mode/5648), moving selected assets from the **Core Pool** to **Isolated E-Mode pools (Isolation Mode)**, and pausing **TUSD** in the Core Pool. + +Phase 2 continues the migration process by moving a group of lower-utilisation or higher-volatility assets out of the Core Pool and into an **Isolation Mode group**. This approach allows the protocol to retain support for these assets while reducing systemic risk to the Core Pool. + +After the migration, the **Isolation group** will consist of the migrated assets together with **USDT and USDC**, allowing users to continue interacting with these markets while leveraging stablecoin liquidity from the Core Pool. + +In addition, **TUSD** will be **paused in the Core Pool** due to its **very low supply and borrow levels**. + +#### Changes + +#### 1. Asset Migration to Isolation Mode + +The following assets will be migrated from the **Core Pool** to the **Isolation Mode group**: + +- **LINK** +- **UNI** +- **AAVE** +- **DOGE** +- **BCH** +- **TWT** +- **ADA** +- **LTC** +- **FIL** +- **MATIC** +- **TRX** +- **DOT** +- **THE** + +For all migrated assets, the following adjustments will be applied in the **Core Pool**: + +- **Loan-to-Value (LTV)** will be set to **0** +- **Borrowable status** will be set to **false** + +The corresponding markets in **Isolation Mode** will retain the same parameters previously used in the Core Pool: + +- **Loan-to-Value (LTV)** in the corresponding Isolation Mode +- **Liquidation Threshold (LT)** +- **Supply Cap** +- **Borrow Cap** +- **Collateral Cap** +- **Borrowable status** in the corresponding Isolation Mode +- **Collateral status** +- **Interest Rate Model (IRM)** + +#### 2. Pause TUSD in the Core Pool + +**TUSD** will be **paused in the Core Pool** and will **not be migrated** to an Isolation Mode group. + +Given the **very low utilisation and limited activity**, pausing the market helps streamline the Core Pool while reducing maintenance overhead for underutilised assets. + +#### Summary + +If approved, this VIP will: + +- Migrate **13 assets** from the **Core Pool** to **Isolation Mode pools** and maintain existing parameters for all migrated markets +- **Pause the TUSD market** in the Core Pool due to low utilisation +- Improve **risk segmentation** while preserving user access to migrated assets via isolated markets + +#### References + +- [VIP-587: Phase 1](https://github.com/VenusProtocol/vips/pull/661) +- [Community post](https://community.venus.io/t/asset-migration-from-core-pool-to-isolated-e-mode/5648)`, + forDescription: "I agree that Venus Protocol should proceed with this proposal", + againstDescription: "I do not think that Venus Protocol should proceed with this proposal", + abstainDescription: "I am indifferent to whether Venus Protocol proceeds or not", + }; + + return makeProposal( + [ + // Set Collateral Factor to 0 for all markets (keep LT unchanged) + ...ALL_MARKETS.map(market => ({ + target: bscmainnet.UNITROLLER, + signature: "setCollateralFactor(uint96,address,uint256,uint256)", + params: [CORE_POOL_ID, market.vToken, 0, market.liquidationThreshold], + })), + // Disable borrowing in Core Pool (pool 0) for all markets + ...ALL_MARKETS.map(market => ({ + target: bscmainnet.UNITROLLER, + signature: "setIsBorrowAllowed(uint96,address,bool)", + params: [CORE_POOL_ID, market.vToken, false], + })), + // Pause mint action for TUSD in Core Pool + { + target: bscmainnet.UNITROLLER, + signature: "_setActionsPaused(address[],uint8[],bool)", + params: [[vTUSD], [Actions.MINT], true], + }, + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip599; diff --git a/vips/vip-599/bsctestnet.ts b/vips/vip-599/bsctestnet.ts new file mode 100644 index 000000000..547aedcd5 --- /dev/null +++ b/vips/vip-599/bsctestnet.ts @@ -0,0 +1,62 @@ +import { parseUnits } from "ethers/lib/utils"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +const { bsctestnet } = NETWORK_ADDRESSES; + +// Core Pool vToken addresses for migrated assets (testnet) +export const vUNI = "0x171B468b52d7027F12cEF90cd065d6776a25E24e"; +export const vAAVE = "0x714db6c38A17883964B68a07d56cE331501d9eb6"; +export const vDOGE = "0xF912d3001CAf6DC4ADD366A62Cc9115B4303c9A9"; +export const vTWT = "0x95DaED37fdD3F557b3A5cCEb7D50Be65b36721DF"; +export const vADA = "0x37C28DE42bA3d22217995D146FC684B2326Ede64"; +export const vLTC = "0xAfc13BC065ABeE838540823431055D2ea52eBA52"; +export const vTRX = "0x6AF3Fdb3282c5bb6926269Db10837fa8Aec67C04"; +export const vTHE = "0x39A239F5117BFaC7a1b0b3A517c454113323451d"; + +// Markets to disable in Core Pool with their current Liquidation Thresholds (keep LT unchanged) +export const MARKETS_TO_DISABLE = [ + { symbol: "UNI", vToken: vUNI, liquidationThreshold: parseUnits("0.55", 18) }, + { symbol: "AAVE", vToken: vAAVE, liquidationThreshold: parseUnits("0.55", 18) }, + { symbol: "DOGE", vToken: vDOGE, liquidationThreshold: parseUnits("0.8", 18) }, + { symbol: "TWT", vToken: vTWT, liquidationThreshold: parseUnits("0.5", 18) }, + { symbol: "ADA", vToken: vADA, liquidationThreshold: parseUnits("0.6", 18) }, + { symbol: "LTC", vToken: vLTC, liquidationThreshold: parseUnits("0.65", 18) }, + { symbol: "TRX", vToken: vTRX, liquidationThreshold: parseUnits("0.6", 18) }, + { symbol: "THE", vToken: vTHE, liquidationThreshold: parseUnits("0.53", 18) }, +]; + +export const CORE_POOL_ID = 0; + +export const vip599 = () => { + const meta = { + version: "v2", + title: "VIP-599 [BNB Chain] Phase 2 Asset Migration to Isolation Mode Pools + Pause TUSD", + description: "VIP-599 [BNB Chain] Phase 2 Asset Migration to Isolation Mode Pools + Pause TUSD", + forDescription: "I agree that Venus Protocol should proceed with this proposal", + againstDescription: "I do not think that Venus Protocol should proceed with this proposal", + abstainDescription: "I am indifferent to whether Venus Protocol proceeds or not", + }; + + return makeProposal( + [ + // Set Collateral Factor to 0 for all migrated assets in Core Pool (keep LT unchanged) + ...MARKETS_TO_DISABLE.map(market => ({ + target: bsctestnet.UNITROLLER, + signature: "setCollateralFactor(uint96,address,uint256,uint256)", + params: [CORE_POOL_ID, market.vToken, 0, market.liquidationThreshold], + })), + // Disable borrowing in Core Pool (pool 0) for all migrated assets + ...MARKETS_TO_DISABLE.map(market => ({ + target: bsctestnet.UNITROLLER, + signature: "setIsBorrowAllowed(uint96,address,bool)", + params: [CORE_POOL_ID, market.vToken, false], + })), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip599;