From 9a409f45b79c05d604e96f4f533e3c5117ad911f Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 21 Jan 2026 16:55:55 +0530 Subject: [PATCH 1/7] feat: add vip and simulation for bsctestnet --- simulations/vip-600/abi/SwapRouter.json | 618 ++++++++++++++++++++++++ simulations/vip-600/bsctestnet.ts | 46 ++ vips/vip-600/bsctestnet.ts | 31 ++ 3 files changed, 695 insertions(+) create mode 100644 simulations/vip-600/abi/SwapRouter.json create mode 100644 simulations/vip-600/bsctestnet.ts create mode 100644 vips/vip-600/bsctestnet.ts diff --git a/simulations/vip-600/abi/SwapRouter.json b/simulations/vip-600/abi/SwapRouter.json new file mode 100644 index 000000000..ab208ac75 --- /dev/null +++ b/simulations/vip-600/abi/SwapRouter.json @@ -0,0 +1,618 @@ +[ + { + "inputs": [ + { + "internalType": "contract IComptroller", + "name": "_comptroller", + "type": "address" + }, + { + "internalType": "contract SwapHelper", + "name": "_swapHelper", + "type": "address" + }, + { + "internalType": "contract IWBNB", + "name": "_wrappedNative", + "type": "address" + }, + { + "internalType": "address", + "name": "_nativeVToken", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + } + ], + "name": "InsufficientAmountOut", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "MarketNotListed", + "type": "error" + }, + { + "inputs": [], + "name": "NativeTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "NoTokensReceived", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "errorCode", + "type": "uint256" + } + ], + "name": "RepayFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "errorCode", + "type": "uint256" + } + ], + "name": "SupplyFailed", + "type": "error" + }, + { + "inputs": [], + "name": "SwapFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "UnauthorizedNativeSender", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountRepaid", + "type": "uint256" + } + ], + "name": "SwapAndRepay", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenOut", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountSupplied", + "type": "uint256" + } + ], + "name": "SwapAndSupply", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "SweepNative", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "SweepToken", + "type": "event" + }, + { + "inputs": [], + "name": "COMPTROLLER", + "outputs": [ + { + "internalType": "contract IComptroller", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "NATIVE_TOKEN_ADDR", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "NATIVE_VTOKEN", + "outputs": [ + { + "internalType": "contract IVBNB", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SWAP_HELPER", + "outputs": [ + { + "internalType": "contract SwapHelper", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "WRAPPED_NATIVE", + "outputs": [ + { + "internalType": "contract IWBNB", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "swapCallData", + "type": "bytes" + } + ], + "name": "swapAndRepay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "swapCallData", + "type": "bytes" + } + ], + "name": "swapAndRepayFull", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "swapCallData", + "type": "bytes" + } + ], + "name": "swapAndSupply", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "swapCallData", + "type": "bytes" + } + ], + "name": "swapNativeAndRepay", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "bytes", + "name": "swapCallData", + "type": "bytes" + } + ], + "name": "swapNativeAndRepayFull", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "minAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "swapCallData", + "type": "bytes" + } + ], + "name": "swapNativeAndSupply", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "sweepNative", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20Upgradeable", + "name": "token", + "type": "address" + } + ], + "name": "sweepToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/simulations/vip-600/bsctestnet.ts b/simulations/vip-600/bsctestnet.ts new file mode 100644 index 000000000..72911634a --- /dev/null +++ b/simulations/vip-600/bsctestnet.ts @@ -0,0 +1,46 @@ +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 { SWAP_HELPER, SWAP_ROUTER, UNITROLLER, vip600 } from "../../vips/vip-600/bsctestnet"; +import SWAP_ROUTER_ABI from "./abi/SwapRouter.json"; + +const { bsctestnet } = NETWORK_ADDRESSES; + +forking(85710269, async () => { + let swapRouter: Contract; + + before(async () => { + swapRouter = await ethers.getContractAt(SWAP_ROUTER_ABI, SWAP_ROUTER); + }); + + describe("Pre-VIP behavior", () => { + it("SwapRouter should have NORMAL_TIMELOCK as pending owner", async () => { + expect(await swapRouter.pendingOwner()).to.equal(bsctestnet.NORMAL_TIMELOCK); + }); + + describe("SwapRouter configuration", () => { + it("should have correct COMPTROLLER", async () => { + expect(await swapRouter.COMPTROLLER()).to.equal(UNITROLLER); + }); + + it("should have correct swapHelper", async () => { + expect(await swapRouter.SWAP_HELPER()).to.equal(SWAP_HELPER); + }); + }); + }); + + testVip("VIP-600", await vip600(), {}); + + describe("Post-VIP behavior", () => { + it("SwapRouter should have NORMAL_TIMELOCK as owner", async () => { + expect(await swapRouter.owner()).to.equal(bsctestnet.NORMAL_TIMELOCK); + }); + + it("SwapRouter should have zero address as pending owner", async () => { + expect(await swapRouter.pendingOwner()).to.equal(ethers.constants.AddressZero); + }); + }); +}); diff --git a/vips/vip-600/bsctestnet.ts b/vips/vip-600/bsctestnet.ts new file mode 100644 index 000000000..52f927d30 --- /dev/null +++ b/vips/vip-600/bsctestnet.ts @@ -0,0 +1,31 @@ +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +export const UNITROLLER = "0x94d1820b2D1c7c7452A163983Dc888CEC546b77D"; +export const SWAP_ROUTER = "0xd3F226acA3210990DBA3f410b74E36b08F31FCf2"; +export const SWAP_HELPER = "0x55Fa097cA59BAc70C1ba488BEb11A5F6bf7019Eb"; + +export const vip600 = () => { + const meta = { + version: "v2", + title: "VIP-600 [BNB Chain] Implementation of SwapRouter", + description: `Implements SwapRouter contract and transfers its ownership to Venus Protocol on BNB Chain.`, + forDescription: "Execute", + againstDescription: "Do not execute", + abstainDescription: "Abstain", + }; + + return makeProposal( + [ + { + target: SWAP_ROUTER, + signature: "acceptOwnership()", + params: [], + }, + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip600; From 61cb0851e4719fcc3545926d1fe37b942a509e63 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 21 Jan 2026 17:07:14 +0530 Subject: [PATCH 2/7] feat: add vip and simulation for bscmainnet --- simulations/vip-600/bscmainnet.ts | 46 +++++++++++++++++++++++++++++++ src/vip-framework/index.ts | 2 +- vips/vip-600/bscmainnet.ts | 31 +++++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 simulations/vip-600/bscmainnet.ts create mode 100644 vips/vip-600/bscmainnet.ts diff --git a/simulations/vip-600/bscmainnet.ts b/simulations/vip-600/bscmainnet.ts new file mode 100644 index 000000000..82dea5108 --- /dev/null +++ b/simulations/vip-600/bscmainnet.ts @@ -0,0 +1,46 @@ +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 { SWAP_HELPER, SWAP_ROUTER, UNITROLLER, vip600 } from "../../vips/vip-600/bscmainnet"; +import SWAP_ROUTER_ABI from "./abi/SwapRouter.json"; + +const { bscmainnet } = NETWORK_ADDRESSES; + +forking(76556273, async () => { + let swapRouter: Contract; + + before(async () => { + swapRouter = await ethers.getContractAt(SWAP_ROUTER_ABI, SWAP_ROUTER); + }); + + describe("Pre-VIP behavior", () => { + it("SwapRouter should have NORMAL_TIMELOCK as pending owner", async () => { + expect(await swapRouter.pendingOwner()).to.equal(bscmainnet.NORMAL_TIMELOCK); + }); + + describe("SwapRouter configuration", () => { + it("should have correct COMPTROLLER", async () => { + expect(await swapRouter.COMPTROLLER()).to.equal(UNITROLLER); + }); + + it("should have correct swapHelper", async () => { + expect(await swapRouter.SWAP_HELPER()).to.equal(SWAP_HELPER); + }); + }); + }); + + testVip("VIP-600", await vip600(), {}); + + describe("Post-VIP behavior", () => { + it("SwapRouter should have NORMAL_TIMELOCK as owner", async () => { + expect(await swapRouter.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); + }); + + it("SwapRouter should have zero address as pending owner", async () => { + expect(await swapRouter.pendingOwner()).to.equal(ethers.constants.AddressZero); + }); + }); +}); diff --git a/src/vip-framework/index.ts b/src/vip-framework/index.ts index de2aa7f1f..5176b2df4 100644 --- a/src/vip-framework/index.ts +++ b/src/vip-framework/index.ts @@ -30,7 +30,7 @@ const OMNICHAIN_PROPOSAL_SENDER = getOmnichainProposalSenderAddress(); const OMNICHAIN_GOVERNANCE_EXECUTOR = NETWORK_ADDRESSES[FORKED_NETWORK as REMOTE_NETWORKS].OMNICHAIN_GOVERNANCE_EXECUTOR; -const VOTING_PERIOD = 115200; +const VOTING_PERIOD = 192384; export const { DEFAULT_PROPOSER_ADDRESS, diff --git a/vips/vip-600/bscmainnet.ts b/vips/vip-600/bscmainnet.ts new file mode 100644 index 000000000..82b2dcf5e --- /dev/null +++ b/vips/vip-600/bscmainnet.ts @@ -0,0 +1,31 @@ +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +export const UNITROLLER = "0xfD36E2c2a6789Db23113685031d7F16329158384"; +export const SWAP_ROUTER = "0xde7E4f67Af577F29e5F3B995f9e67FD425F73621"; +export const SWAP_HELPER = "0xD79be25aEe798Aa34A9Ba1230003d7499be29A24"; + +export const vip600 = () => { + const meta = { + version: "v2", + title: "VIP-600 [BNB Chain] Implementation of SwapRouter", + description: `Implements SwapRouter contract and transfers its ownership to Venus Protocol on BNB Chain.`, + forDescription: "Execute", + againstDescription: "Do not execute", + abstainDescription: "Abstain", + }; + + return makeProposal( + [ + { + target: SWAP_ROUTER, + signature: "acceptOwnership()", + params: [], + }, + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip600; From 6ba414efabbab9aadfe7444a607e680d3f4a424f Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 27 Jan 2026 13:10:14 +0530 Subject: [PATCH 3/7] test: detail simulation for swaps --- simulations/vip-600/abi/Comptroller.json | 4145 ++++++++++++++++++++++ simulations/vip-600/abi/ERC20.json | 134 + simulations/vip-600/abi/VToken.json | 1747 +++++++++ simulations/vip-600/bscmainnet.ts | 721 +++- 4 files changed, 6733 insertions(+), 14 deletions(-) create mode 100644 simulations/vip-600/abi/Comptroller.json create mode 100644 simulations/vip-600/abi/ERC20.json create mode 100644 simulations/vip-600/abi/VToken.json diff --git a/simulations/vip-600/abi/Comptroller.json b/simulations/vip-600/abi/Comptroller.json new file mode 100644 index 000000000..5ca3a7000 --- /dev/null +++ b/simulations/vip-600/abi/Comptroller.json @@ -0,0 +1,4145 @@ +[ + { + "inputs": [], + "name": "AlreadyInSelectedPool", + "type": "error" + }, + { + "inputs": [], + "name": "ArrayLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "BorrowNotAllowedInPool", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPoolLabel", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + } + ], + "name": "InactivePool", + "type": "error" + }, + { + "inputs": [], + "name": "IncompatibleBorrowedAssets", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperationForCorePool", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "enum WeightFunction", + "name": "strategy", + "type": "uint8" + } + ], + "name": "InvalidWeightingStrategy", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "errorCode", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "shortfall", + "type": "uint256" + } + ], + "name": "LiquidityCheckFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "MarketAlreadyListed", + "type": "error" + }, + { + "inputs": [], + "name": "MarketConfigNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "MarketNotListedInCorePool", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + } + ], + "name": "PoolDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "PoolMarketNotFound", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "enum Action", + "name": "action", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bool", + "name": "pauseState", + "type": "bool" + } + ], + "name": "ActionPausedMarket", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bool", + "name": "state", + "type": "bool" + } + ], + "name": "ActionProtocolPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "oldStatus", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bool", + "name": "newStatus", + "type": "bool" + } + ], + "name": "BorrowAllowedUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "approver", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "DelegateUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "facetAddress", + "type": "address" + }, + { + "internalType": "enum IDiamondCut.FacetCutAction", + "name": "action", + "type": "uint8" + }, + { + "internalType": "bytes4[]", + "name": "functionSelectors", + "type": "bytes4[]" + } + ], + "indexed": false, + "internalType": "struct IDiamondCut.FacetCut[]", + "name": "_diamondCut", + "type": "tuple[]" + } + ], + "name": "DiamondCut", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "venusDelta", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "venusBorrowIndex", + "type": "uint256" + } + ], + "name": "DistributedBorrowerVenus", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "supplier", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "venusDelta", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "venusSupplyIndex", + "type": "uint256" + } + ], + "name": "DistributedSupplierVenus", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "DistributedVAIVaultVenus", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "error", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "info", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "detail", + "type": "uint256" + } + ], + "name": "Failure", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "enable", + "type": "bool" + } + ], + "name": "IsForcedLiquidationEnabledForUserUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "enable", + "type": "bool" + } + ], + "name": "IsForcedLiquidationEnabledUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "MarketEntered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "MarketExited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + } + ], + "name": "MarketListed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "MarketUnlisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldAccessControlAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAccessControlAddress", + "type": "address" + } + ], + "name": "NewAccessControl", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBorrowCap", + "type": "uint256" + } + ], + "name": "NewBorrowCap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldCloseFactorMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newCloseFactorMantissa", + "type": "uint256" + } + ], + "name": "NewCloseFactor", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldCollateralFactorMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newCollateralFactorMantissa", + "type": "uint256" + } + ], + "name": "NewCollateralFactor", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldComptrollerLens", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newComptrollerLens", + "type": "address" + } + ], + "name": "NewComptrollerLens", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "indexed": true, + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldLiquidationIncentiveMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newLiquidationIncentiveMantissa", + "type": "uint256" + } + ], + "name": "NewLiquidationIncentive", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldLiquidationThresholdMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newLiquidationThresholdMantissa", + "type": "uint256" + } + ], + "name": "NewLiquidationThreshold", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldLiquidatorContract", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newLiquidatorContract", + "type": "address" + } + ], + "name": "NewLiquidatorContract", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldPauseGuardian", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newPauseGuardian", + "type": "address" + } + ], + "name": "NewPauseGuardian", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract ResilientOracleInterface", + "name": "oldPriceOracle", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract ResilientOracleInterface", + "name": "newPriceOracle", + "type": "address" + } + ], + "name": "NewPriceOracle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract IPrime", + "name": "oldPrimeToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IPrime", + "name": "newPrimeToken", + "type": "address" + } + ], + "name": "NewPrimeToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newSupplyCap", + "type": "uint256" + } + ], + "name": "NewSupplyCap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldTreasuryAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newTreasuryAddress", + "type": "address" + } + ], + "name": "NewTreasuryAddress", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldTreasuryGuardian", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newTreasuryGuardian", + "type": "address" + } + ], + "name": "NewTreasuryGuardian", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldTreasuryPercent", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newTreasuryPercent", + "type": "uint256" + } + ], + "name": "NewTreasuryPercent", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract VAIControllerInterface", + "name": "oldVAIController", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract VAIControllerInterface", + "name": "newVAIController", + "type": "address" + } + ], + "name": "NewVAIController", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldVAIMintRate", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newVAIMintRate", + "type": "uint256" + } + ], + "name": "NewVAIMintRate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "vault_", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "releaseStartBlock_", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "releaseInterval_", + "type": "uint256" + } + ], + "name": "NewVAIVaultInfo", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldVenusVAIVaultRate", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newVenusVAIVaultRate", + "type": "uint256" + } + ], + "name": "NewVenusVAIVaultRate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldXVS", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newXVS", + "type": "address" + } + ], + "name": "NewXVSToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldXVSVToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newXVSVToken", + "type": "address" + } + ], + "name": "NewXVSVToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "indexed": false, + "internalType": "bool", + "name": "oldStatus", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bool", + "name": "newStatus", + "type": "bool" + } + ], + "name": "PoolActiveStatusUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "indexed": false, + "internalType": "string", + "name": "label", + "type": "string" + } + ], + "name": "PoolCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "indexed": false, + "internalType": "bool", + "name": "oldStatus", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bool", + "name": "newStatus", + "type": "bool" + } + ], + "name": "PoolFallbackStatusUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "indexed": false, + "internalType": "string", + "name": "oldLabel", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "newLabel", + "type": "string" + } + ], + "name": "PoolLabelUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "PoolMarketInitialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "indexed": true, + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "PoolMarketRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint96", + "name": "previousPoolId", + "type": "uint96" + }, + { + "indexed": true, + "internalType": "uint96", + "name": "newPoolId", + "type": "uint96" + } + ], + "name": "PoolSelected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newSpeed", + "type": "uint256" + } + ], + "name": "VenusBorrowSpeedUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "VenusGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "holder", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "VenusSeized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newSpeed", + "type": "uint256" + } + ], + "name": "VenusSupplySpeedUpdated", + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "contract Unitroller", + "name": "unitroller", + "type": "address" + } + ], + "name": "_become", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "_grantXVS", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newAccessControlAddress", + "type": "address" + } + ], + "name": "_setAccessControl", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "markets_", + "type": "address[]" + }, + { + "internalType": "enum Action[]", + "name": "actions_", + "type": "uint8[]" + }, + { + "internalType": "bool", + "name": "paused_", + "type": "bool" + } + ], + "name": "_setActionsPaused", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newCloseFactorMantissa", + "type": "uint256" + } + ], + "name": "_setCloseFactor", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ComptrollerLensInterface", + "name": "comptrollerLens_", + "type": "address" + } + ], + "name": "_setComptrollerLens", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "bool", + "name": "enable", + "type": "bool" + } + ], + "name": "_setForcedLiquidation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "bool", + "name": "enable", + "type": "bool" + } + ], + "name": "_setForcedLiquidationForUser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newLiquidatorContract_", + "type": "address" + } + ], + "name": "_setLiquidatorContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken[]", + "name": "vTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "newBorrowCaps", + "type": "uint256[]" + } + ], + "name": "_setMarketBorrowCaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken[]", + "name": "vTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "newSupplyCaps", + "type": "uint256[]" + } + ], + "name": "_setMarketSupplyCaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newPauseGuardian", + "type": "address" + } + ], + "name": "_setPauseGuardian", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ResilientOracleInterface", + "name": "newOracle", + "type": "address" + } + ], + "name": "_setPriceOracle", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPrime", + "name": "_prime", + "type": "address" + } + ], + "name": "_setPrimeToken", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "state", + "type": "bool" + } + ], + "name": "_setProtocolPaused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newTreasuryGuardian", + "type": "address" + }, + { + "internalType": "address", + "name": "newTreasuryAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "newTreasuryPercent", + "type": "uint256" + } + ], + "name": "_setTreasuryData", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VAIControllerInterface", + "name": "vaiController_", + "type": "address" + } + ], + "name": "_setVAIController", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newVAIMintRate", + "type": "uint256" + } + ], + "name": "_setVAIMintRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vault_", + "type": "address" + }, + { + "internalType": "uint256", + "name": "releaseStartBlock_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minReleaseAmount_", + "type": "uint256" + } + ], + "name": "_setVAIVaultInfo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken[]", + "name": "vTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "supplySpeeds", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "borrowSpeeds", + "type": "uint256[]" + } + ], + "name": "_setVenusSpeeds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "venusVAIVaultRate_", + "type": "uint256" + } + ], + "name": "_setVenusVAIVaultRate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "xvs_", + "type": "address" + } + ], + "name": "_setXVSToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "xvsVToken_", + "type": "address" + } + ], + "name": "_setXVSVToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + } + ], + "name": "_supportMarket", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "accountAssets", + "outputs": [ + { + "internalType": "contract VToken", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "enum Action", + "name": "action", + "type": "uint8" + } + ], + "name": "actionPaused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96[]", + "name": "poolIds", + "type": "uint96[]" + }, + { + "internalType": "address[]", + "name": "vTokens", + "type": "address[]" + } + ], + "name": "addPoolMarkets", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allMarkets", + "outputs": [ + { + "internalType": "contract VToken", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "approvedDelegates", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + } + ], + "name": "borrowAllowed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "borrowCapGuardian", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "borrowCaps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + } + ], + "name": "borrowVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + } + ], + "name": "checkMembership", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "holders", + "type": "address[]" + }, + { + "internalType": "contract VToken[]", + "name": "vTokens", + "type": "address[]" + }, + { + "internalType": "bool", + "name": "borrowers", + "type": "bool" + }, + { + "internalType": "bool", + "name": "suppliers", + "type": "bool" + }, + { + "internalType": "bool", + "name": "collateral", + "type": "bool" + } + ], + "name": "claimVenus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "holder", + "type": "address" + }, + { + "internalType": "contract VToken[]", + "name": "vTokens", + "type": "address[]" + } + ], + "name": "claimVenus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "holder", + "type": "address" + } + ], + "name": "claimVenus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "holders", + "type": "address[]" + }, + { + "internalType": "contract VToken[]", + "name": "vTokens", + "type": "address[]" + }, + { + "internalType": "bool", + "name": "borrowers", + "type": "bool" + }, + { + "internalType": "bool", + "name": "suppliers", + "type": "bool" + } + ], + "name": "claimVenus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "holder", + "type": "address" + } + ], + "name": "claimVenusAsCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "closeFactorMantissa", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "comptrollerImplementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "comptrollerLens", + "outputs": [ + { + "internalType": "contract ComptrollerLensInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "corePoolId", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + } + ], + "name": "createPool", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "facetAddress", + "type": "address" + }, + { + "internalType": "enum IDiamondCut.FacetCutAction", + "name": "action", + "type": "uint8" + }, + { + "internalType": "bytes4[]", + "name": "functionSelectors", + "type": "bytes4[]" + } + ], + "internalType": "struct IDiamondCut.FacetCut[]", + "name": "diamondCut_", + "type": "tuple[]" + } + ], + "name": "diamondCut", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "vTokens", + "type": "address[]" + } + ], + "name": "enterMarkets", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + } + ], + "name": "enterPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenAddress", + "type": "address" + } + ], + "name": "exitMarket", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "functionSelector", + "type": "bytes4" + } + ], + "name": "facetAddress", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "facetAddress", + "type": "address" + }, + { + "internalType": "uint96", + "name": "functionSelectorPosition", + "type": "uint96" + } + ], + "internalType": "struct ComptrollerV13Storage.FacetAddressAndPosition", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "facetAddresses", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "facet", + "type": "address" + } + ], + "name": "facetFunctionSelectors", + "outputs": [ + { + "internalType": "bytes4[]", + "name": "", + "type": "bytes4[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "facet", + "type": "address" + } + ], + "name": "facetPosition", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "facets", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "facetAddress", + "type": "address" + }, + { + "internalType": "bytes4[]", + "name": "functionSelectors", + "type": "bytes4[]" + } + ], + "internalType": "struct Diamond.Facet[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAccountLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAllMarkets", + "outputs": [ + { + "internalType": "contract VToken[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAssetsIn", + "outputs": [ + { + "internalType": "contract VToken[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getBorrowingPower", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "getCollateralFactor", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "getEffectiveLiquidationIncentive", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "enum WeightFunction", + "name": "weightingStrategy", + "type": "uint8" + } + ], + "name": "getEffectiveLtvFactor", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenModify", + "type": "address" + }, + { + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + } + ], + "name": "getHypotheticalAccountLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "getLiquidationIncentive", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "getLiquidationThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "getPoolMarketIndex", + "outputs": [ + { + "internalType": "PoolMarketId", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + } + ], + "name": "getPoolVTokens", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getXVSAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getXVSVTokenAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint96", + "name": "targetPoolId", + "type": "uint96" + } + ], + "name": "hasValidPoolBorrows", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isComptroller", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isForcedLiquidationEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "isForcedLiquidationEnabledForUser", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + } + ], + "name": "isMarketListed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastPoolId", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + } + ], + "name": "liquidateBorrowAllowed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "actualRepayAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "seizeTokens", + "type": "uint256" + } + ], + "name": "liquidateBorrowVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "uint256", + "name": "actualRepayAmount", + "type": "uint256" + } + ], + "name": "liquidateCalculateSeizeTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "uint256", + "name": "actualRepayAmount", + "type": "uint256" + } + ], + "name": "liquidateCalculateSeizeTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "uint256", + "name": "actualRepayAmount", + "type": "uint256" + } + ], + "name": "liquidateVAICalculateSeizeTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "liquidatorContract", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "markets", + "outputs": [ + { + "internalType": "bool", + "name": "isListed", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "collateralFactorMantissa", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isVenus", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "liquidationThresholdMantissa", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidationIncentiveMantissa", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "marketPoolId", + "type": "uint96" + }, + { + "internalType": "bool", + "name": "isBorrowAllowed", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxAssets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minReleaseAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "internalType": "uint256", + "name": "mintAmount", + "type": "uint256" + } + ], + "name": "mintAllowed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "mintVAIGuardianPaused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "internalType": "uint256", + "name": "actualMintAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "mintTokens", + "type": "uint256" + } + ], + "name": "mintVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "mintedVAIs", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "oracle", + "outputs": [ + { + "internalType": "contract ResilientOracleInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pauseGuardian", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingAdmin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingComptrollerImplementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "poolMarkets", + "outputs": [ + { + "internalType": "bool", + "name": "isListed", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "collateralFactorMantissa", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isVenus", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "liquidationThresholdMantissa", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "liquidationIncentiveMantissa", + "type": "uint256" + }, + { + "internalType": "uint96", + "name": "marketPoolId", + "type": "uint96" + }, + { + "internalType": "bool", + "name": "isBorrowAllowed", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "name": "pools", + "outputs": [ + { + "internalType": "string", + "name": "label", + "type": "string" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + }, + { + "internalType": "bool", + "name": "allowCorePoolFallback", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "prime", + "outputs": [ + { + "internalType": "contract IPrime", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolPaused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + } + ], + "name": "redeemAllowed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "redeemAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + } + ], + "name": "redeemVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "releaseStartBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "removePoolMarket", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + } + ], + "name": "repayBorrowAllowed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "actualRepayAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowerIndex", + "type": "uint256" + } + ], + "name": "repayBorrowVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repayVAIGuardianPaused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "seizeTokens", + "type": "uint256" + } + ], + "name": "seizeAllowed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "holders", + "type": "address[]" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "seizeVenus", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "seizeTokens", + "type": "uint256" + } + ], + "name": "seizeVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "markets_", + "type": "address[]" + }, + { + "internalType": "enum Action[]", + "name": "actions_", + "type": "uint8[]" + }, + { + "internalType": "bool", + "name": "paused_", + "type": "bool" + } + ], + "name": "setActionsPaused", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "bool", + "name": "allowFallback", + "type": "bool" + } + ], + "name": "setAllowCorePoolFallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newCloseFactorMantissa", + "type": "uint256" + } + ], + "name": "setCloseFactor", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "newCollateralFactorMantissa", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newLiquidationThresholdMantissa", + "type": "uint256" + } + ], + "name": "setCollateralFactor", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "newCollateralFactorMantissa", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newLiquidationThresholdMantissa", + "type": "uint256" + } + ], + "name": "setCollateralFactor", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vTokenBorrowed", + "type": "address" + }, + { + "internalType": "bool", + "name": "enable", + "type": "bool" + } + ], + "name": "setForcedLiquidation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "bool", + "name": "borrowAllowed", + "type": "bool" + } + ], + "name": "setIsBorrowAllowed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "newLiquidationIncentiveMantissa", + "type": "uint256" + } + ], + "name": "setLiquidationIncentive", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "newLiquidationIncentiveMantissa", + "type": "uint256" + } + ], + "name": "setLiquidationIncentive", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken[]", + "name": "vTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "newBorrowCaps", + "type": "uint256[]" + } + ], + "name": "setMarketBorrowCaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken[]", + "name": "vTokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "newSupplyCaps", + "type": "uint256[]" + } + ], + "name": "setMarketSupplyCaps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "setMintedVAIOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + } + ], + "name": "setPoolActive", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint96", + "name": "poolId", + "type": "uint96" + }, + { + "internalType": "string", + "name": "newLabel", + "type": "string" + } + ], + "name": "setPoolLabel", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ResilientOracleInterface", + "name": "newOracle", + "type": "address" + } + ], + "name": "setPriceOracle", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPrime", + "name": "_prime", + "type": "address" + } + ], + "name": "setPrimeToken", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "supplyCaps", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract VToken", + "name": "vToken", + "type": "address" + } + ], + "name": "supportMarket", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "transferTokens", + "type": "uint256" + } + ], + "name": "transferAllowed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "transferTokens", + "type": "uint256" + } + ], + "name": "transferVerify", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "treasuryAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "treasuryGuardian", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "treasuryPercent", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "unlistMarket", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "updateDelegate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "userPoolId", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vaiController", + "outputs": [ + { + "internalType": "contract VAIControllerInterface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vaiMintRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vaiVaultAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "venusAccrued", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "venusBorrowSpeeds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "venusBorrowState", + "outputs": [ + { + "internalType": "uint224", + "name": "index", + "type": "uint224" + }, + { + "internalType": "uint32", + "name": "block", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "venusBorrowerIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "venusInitialIndex", + "outputs": [ + { + "internalType": "uint224", + "name": "", + "type": "uint224" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "venusSupplierIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "venusSupplySpeeds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "venusSupplyState", + "outputs": [ + { + "internalType": "uint224", + "name": "index", + "type": "uint224" + }, + { + "internalType": "uint32", + "name": "block", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "venusVAIVaultRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-600/abi/ERC20.json b/simulations/vip-600/abi/ERC20.json new file mode 100644 index 000000000..3a509c9c4 --- /dev/null +++ b/simulations/vip-600/abi/ERC20.json @@ -0,0 +1,134 @@ +[ + { + "inputs": [ + { "internalType": "string", "name": "name_", "type": "string" }, + { "internalType": "string", "name": "symbol_", "type": "string" }, + { "internalType": "uint8", "name": "decimals_", "type": "uint8" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "subtractedValue", "type": "uint256" } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], + "name": "faucet", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "addedValue", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/simulations/vip-600/abi/VToken.json b/simulations/vip-600/abi/VToken.json new file mode 100644 index 000000000..61a618463 --- /dev/null +++ b/simulations/vip-600/abi/VToken.json @@ -0,0 +1,1747 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "cashPrior", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "interestAccumulated", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "borrowIndex", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalBorrows", + "type": "uint256" + } + ], + "name": "AccrueInterest", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accountBorrows", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalBorrows", + "type": "uint256" + } + ], + "name": "Borrow", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "error", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "info", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "detail", + "type": "uint256" + } + ], + "name": "Failure", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "vTokenCollateral", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "seizeTokens", + "type": "uint256" + } + ], + "name": "LiquidateBorrow", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "minter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintTokens", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mintTokens", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "name": "MintBehalf", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldAccessControlAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAccessControlAddress", + "type": "address" + } + ], + "name": "NewAccessControlManager", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdmin", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract ComptrollerInterface", + "name": "oldComptroller", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract ComptrollerInterface", + "name": "newComptroller", + "type": "address" + } + ], + "name": "NewComptroller", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "contract InterestRateModel", + "name": "oldInterestRateModel", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract InterestRateModel", + "name": "newInterestRateModel", + "type": "address" + } + ], + "name": "NewMarketInterestRateModel", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldPendingAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newPendingAdmin", + "type": "address" + } + ], + "name": "NewPendingAdmin", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldProtocolShareReserve", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newProtocolShareReserve", + "type": "address" + } + ], + "name": "NewProtocolShareReserve", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldReduceReservesBlockDelta", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newReduceReservesBlockDelta", + "type": "uint256" + } + ], + "name": "NewReduceReservesBlockDelta", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldReserveFactorMantissa", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newReserveFactorMantissa", + "type": "uint256" + } + ], + "name": "NewReserveFactor", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "redeemAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "name": "Redeem", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + } + ], + "name": "RedeemFee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accountBorrows", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalBorrows", + "type": "uint256" + } + ], + "name": "RepayBorrow", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "benefactor", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "addAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newTotalReserves", + "type": "uint256" + } + ], + "name": "ReservesAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "protocolShareReserve", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reduceAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newTotalReserves", + "type": "uint256" + } + ], + "name": "ReservesReduced", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "constant": false, + "inputs": [], + "name": "_acceptAdmin", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "addAmount", + "type": "uint256" + } + ], + "name": "_addReserves", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "reduceAmount_", + "type": "uint256" + } + ], + "name": "_reduceReserves", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "contract ComptrollerInterface", + "name": "newComptroller", + "type": "address" + } + ], + "name": "_setComptroller", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "contract InterestRateModel", + "name": "newInterestRateModel_", + "type": "address" + } + ], + "name": "_setInterestRateModel", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address payable", + "name": "newPendingAdmin", + "type": "address" + } + ], + "name": "_setPendingAdmin", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "newReserveFactorMantissa_", + "type": "uint256" + } + ], + "name": "_setReserveFactor", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "accrualBlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "accrueInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOfUnderlying", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + } + ], + "name": "borrow", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "borrowBalanceCurrent", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "borrowBalanceStored", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "borrowAmount", + "type": "uint256" + } + ], + "name": "borrowBehalf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "borrowIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "borrowRatePerBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "comptroller", + "outputs": [ + { + "internalType": "contract ComptrollerInterface", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "exchangeRateCurrent", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "exchangeRateStored", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getAccountSnapshot", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getCash", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "implementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "underlying_", + "type": "address" + }, + { + "internalType": "contract ComptrollerInterface", + "name": "comptroller_", + "type": "address" + }, + { + "internalType": "contract InterestRateModel", + "name": "interestRateModel_", + "type": "address" + }, + { + "internalType": "uint256", + "name": "initialExchangeRateMantissa_", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name_", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol_", + "type": "string" + }, + { + "internalType": "uint8", + "name": "decimals_", + "type": "uint8" + } + ], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "contract ComptrollerInterface", + "name": "comptroller_", + "type": "address" + }, + { + "internalType": "contract InterestRateModel", + "name": "interestRateModel_", + "type": "address" + }, + { + "internalType": "uint256", + "name": "initialExchangeRateMantissa_", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name_", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol_", + "type": "string" + }, + { + "internalType": "uint8", + "name": "decimals_", + "type": "uint8" + } + ], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "interestRateModel", + "outputs": [ + { + "internalType": "contract InterestRateModel", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "isVToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + }, + { + "internalType": "contract VTokenInterface", + "name": "vTokenCollateral", + "type": "address" + } + ], + "name": "liquidateBorrow", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "mintAmount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "mintAmount", + "type": "uint256" + } + ], + "name": "mintBehalf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "pendingAdmin", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "protocolShareReserve", + "outputs": [ + { + "internalType": "address payable", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + } + ], + "name": "redeem", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "redeemTokens", + "type": "uint256" + } + ], + "name": "redeemBehalf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "redeemAmount", + "type": "uint256" + } + ], + "name": "redeemUnderlying", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "redeemer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "redeemAmount", + "type": "uint256" + } + ], + "name": "redeemUnderlyingBehalf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "reduceReservesBlockDelta", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "reduceReservesBlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + } + ], + "name": "repayBorrow", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "repayAmount", + "type": "uint256" + } + ], + "name": "repayBorrowBehalf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "reserveFactorMantissa", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "seizeTokens", + "type": "uint256" + } + ], + "name": "seize", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "newAccessControlManagerAddress", + "type": "address" + } + ], + "name": "setAccessControlManager", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address payable", + "name": "protcolShareReserve_", + "type": "address" + } + ], + "name": "setProtocolShareReserve", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "newReduceReservesBlockDelta_", + "type": "uint256" + } + ], + "name": "setReduceReservesBlockDelta", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "supplyRatePerBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalBorrows", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "totalBorrowsCurrent", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalReserves", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "underlying", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-600/bscmainnet.ts b/simulations/vip-600/bscmainnet.ts index 82dea5108..a632ebe9a 100644 --- a/simulations/vip-600/bscmainnet.ts +++ b/simulations/vip-600/bscmainnet.ts @@ -1,46 +1,739 @@ import { expect } from "chai"; -import { Contract } from "ethers"; +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 { SWAP_HELPER, SWAP_ROUTER, UNITROLLER, vip600 } from "../../vips/vip-600/bscmainnet"; +import COMPTROLLER_ABI from "./abi/Comptroller.json"; +import ERC20_ABI from "./abi/ERC20.json"; import SWAP_ROUTER_ABI from "./abi/SwapRouter.json"; +import VTOKEN_ABI from "./abi/VToken.json"; const { bscmainnet } = NETWORK_ADDRESSES; -forking(76556273, async () => { +// ============================================================================= +// Constants +// ============================================================================= + +const vUSDT = "0xfD5840Cd36d94D7229439859C0112a4185BC0255"; +const vUSDC = "0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8"; +const vBNB = "0xA07c5b74C9B40447a954e1466938b865b6BBea36"; + +const USDT = "0x55d398326f99059fF775485246999027B3197955"; +const USDC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"; +const WBNB = "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"; + +const USDT_HOLDER = "0xF977814e90dA44bFA03b6295A0616a897441aceC"; +const USDC_HOLDER = "0x8894E0a0c962CB723c1976a4421c95949bE2D4E3"; + +const NATIVE_TOKEN_ADDR = "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"; +const FORK_BLOCK = 76556273; + +// ============================================================================= +// Swap Data Helper +// ============================================================================= + +async function getSwapData( + tokenInAddress: string, + tokenOutAddress: string, + exactAmountInMantissa: string, + recipientAddress: string, + slippagePercentage: string = "0.005", +): Promise<{ swapData: string; minAmountOut: BigNumber; amountOut: BigNumber }> { + const deadlineTimestamp = Math.floor(Date.now() / 1000) + 3600; + const url = `https://api.venus.io/find-swap?chainId=56&tokenInAddress=${tokenInAddress}&tokenOutAddress=${tokenOutAddress}&slippagePercentage=${slippagePercentage}&recipientAddress=${recipientAddress}&deadlineTimestampSecs=${deadlineTimestamp}&type=exact-in&shouldTransferToReceiver=true&exactAmountInMantissa=${exactAmountInMantissa}`; + + try { + const response = await fetch(url); + const data: unknown = await response.json(); + + if ( + typeof data === "object" && + data !== null && + "quotes" in data && + Array.isArray((data as any).quotes) && + (data as any).quotes.length > 0 + ) { + const quote = (data as any).quotes[0]; + const amountOut = BigNumber.from(quote.amountOut); + return { + swapData: quote.swapHelperMulticall.calldata.encodedCall, + minAmountOut: amountOut.mul(99).div(100), + amountOut, + }; + } + throw new Error("No quotes returned from Venus API"); + } catch (error) { + console.log(" [WARN] Failed to fetch swap data from Venus API:", error); + return { swapData: "0x", minAmountOut: BigNumber.from(0), amountOut: BigNumber.from(0) }; + } +} + +async function getNativeSwapData( + tokenOutAddress: string, + exactAmountInMantissa: string, + recipientAddress: string, + slippagePercentage: string = "0.005", +): Promise<{ swapData: string; minAmountOut: BigNumber; amountOut: BigNumber }> { + return getSwapData(NATIVE_TOKEN_ADDR, tokenOutAddress, exactAmountInMantissa, recipientAddress, slippagePercentage); +} + +// ============================================================================= +// Helpers: Event Parsing +// ============================================================================= + +function parseEventFromReceipt(receipt: any, eventName: string): any[] { + const iface = new ethers.utils.Interface(SWAP_ROUTER_ABI); + return receipt.logs + .map((log: { topics: string[]; data: string }) => { + try { + return iface.parseLog(log); + } catch { + return null; + } + }) + .filter((e: { name: string } | null) => e && e.name === eventName); +} + +// Checks if an error is a known swap failure (stale quote, route unavailable, etc.) +function isSwapError(error: unknown): boolean { + const msg = error instanceof Error ? error.message : String(error); + return ( + msg.includes("SwapFailed") || + msg.includes("InsufficientAmountOut") || + msg.includes("0x81ceff30") || // SwapFailed() selector + msg.includes("NoTokensReceived") + ); +} + +// ============================================================================= +// Test Suite +// ============================================================================= + +forking(FORK_BLOCK, async () => { let swapRouter: Contract; + let comptroller: Contract; + let usdt: Contract; + let usdc: Contract; + let vUSDTContract: Contract; + let vUSDCContract: Contract; + let impersonatedTimelock: Signer; + let testUser: Signer; + let testUserAddress: string; before(async () => { swapRouter = await ethers.getContractAt(SWAP_ROUTER_ABI, SWAP_ROUTER); + comptroller = new ethers.Contract(UNITROLLER, COMPTROLLER_ABI, ethers.provider); + usdt = new ethers.Contract(USDT, ERC20_ABI, ethers.provider); + usdc = new ethers.Contract(USDC, ERC20_ABI, ethers.provider); + vUSDTContract = new ethers.Contract(vUSDT, VTOKEN_ABI, ethers.provider); + vUSDCContract = new ethers.Contract(vUSDC, VTOKEN_ABI, ethers.provider); + + impersonatedTimelock = await initMainnetUser(bscmainnet.NORMAL_TIMELOCK, ethers.utils.parseEther("10")); + + const signers = await ethers.getSigners(); + testUser = signers[0]; + testUserAddress = await testUser.getAddress(); + + // Fund test user + const usdtHolder = await initMainnetUser(USDT_HOLDER, ethers.utils.parseEther("10")); + const usdcHolder = await initMainnetUser(USDC_HOLDER, ethers.utils.parseEther("10")); + await usdt.connect(usdtHolder).transfer(testUserAddress, parseUnits("10000", 18)); + await usdc.connect(usdcHolder).transfer(testUserAddress, parseUnits("10000", 18)); + + // Set stale periods for oracles + await setMaxStalePeriodInChainlinkOracle( + bscmainnet.CHAINLINK_ORACLE, + USDC, + ethers.constants.AddressZero, + bscmainnet.NORMAL_TIMELOCK, + 315360000, + ); + await setMaxStalePeriodInChainlinkOracle( + bscmainnet.CHAINLINK_ORACLE, + USDT, + ethers.constants.AddressZero, + bscmainnet.NORMAL_TIMELOCK, + 315360000, + ); + await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, "USDC", 315360000); + await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, "USDT", 315360000); }); + // =========================================================================== + // Pre-VIP + // =========================================================================== + describe("Pre-VIP behavior", () => { - it("SwapRouter should have NORMAL_TIMELOCK as pending owner", async () => { - expect(await swapRouter.pendingOwner()).to.equal(bscmainnet.NORMAL_TIMELOCK); + it("should have correct ownership state", async () => { + const pendingOwner = await swapRouter.pendingOwner(); + const owner = await swapRouter.owner(); + const isValidState = pendingOwner === bscmainnet.NORMAL_TIMELOCK || owner === bscmainnet.NORMAL_TIMELOCK; + expect(isValidState).to.be.true; + }); + + it("should have correct COMPTROLLER", async () => { + expect(await swapRouter.COMPTROLLER()).to.equal(UNITROLLER); + }); + + it("should have correct SWAP_HELPER", async () => { + expect(await swapRouter.SWAP_HELPER()).to.equal(SWAP_HELPER); + }); + + it("should have correct WRAPPED_NATIVE (WBNB)", async () => { + expect(await swapRouter.WRAPPED_NATIVE()).to.equal(WBNB); }); - describe("SwapRouter configuration", () => { - it("should have correct COMPTROLLER", async () => { - expect(await swapRouter.COMPTROLLER()).to.equal(UNITROLLER); - }); + it("should have correct NATIVE_VTOKEN (vBNB)", async () => { + expect(await swapRouter.NATIVE_VTOKEN()).to.equal(vBNB); + }); - it("should have correct swapHelper", async () => { - expect(await swapRouter.SWAP_HELPER()).to.equal(SWAP_HELPER); - }); + it("should have correct NATIVE_TOKEN_ADDR", async () => { + expect(await swapRouter.NATIVE_TOKEN_ADDR()).to.equal(NATIVE_TOKEN_ADDR); }); }); - testVip("VIP-600", await vip600(), {}); + // =========================================================================== + // VIP Execution + // =========================================================================== + + testVip("VIP-600", await vip600(), { + callbackAfterExecution: async txResponse => { + const receipt = await txResponse.wait(); + const events = parseEventFromReceipt(receipt, "OwnershipTransferred"); + expect(events.length).to.be.lte(1); + }, + }); + + // =========================================================================== + // Post-VIP + // =========================================================================== describe("Post-VIP behavior", () => { - it("SwapRouter should have NORMAL_TIMELOCK as owner", async () => { + it("should have NORMAL_TIMELOCK as owner", async () => { expect(await swapRouter.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); }); - it("SwapRouter should have zero address as pending owner", async () => { + it("should have zero address as pending owner", async () => { expect(await swapRouter.pendingOwner()).to.equal(ethers.constants.AddressZero); }); }); + + // =========================================================================== + // swapAndSupply + // =========================================================================== + + describe("swapAndSupply", () => { + it("should swap USDT -> USDC and supply to vUSDC", async () => { + const amountIn = parseUnits("100", 18); + const { swapData, minAmountOut } = await getSwapData(USDT, USDC, amountIn.toString(), SWAP_ROUTER, "0.01"); + + if (swapData === "0x") { + console.log(" [SKIP] Venus API unavailable"); + return; + } + + const usdtBefore = await usdt.balanceOf(testUserAddress); + const vUSDCBefore = await vUSDCContract.balanceOf(testUserAddress); + + await usdt.connect(testUser).approve(SWAP_ROUTER, amountIn); + + try { + const tx = await swapRouter + .connect(testUser) + .swapAndSupply(vUSDC, USDT, amountIn, minAmountOut, swapData); + const receipt = await tx.wait(); + + const events = parseEventFromReceipt(receipt, "SwapAndSupply"); + expect(events.length).to.equal(1); + expect(events[0].args.user.toLowerCase()).to.equal(testUserAddress.toLowerCase()); + expect(events[0].args.amountIn).to.equal(amountIn); + expect(events[0].args.amountOut).to.be.gte(minAmountOut); + + expect((await usdt.balanceOf(testUserAddress)).lt(usdtBefore)).to.be.true; + expect((await vUSDCContract.balanceOf(testUserAddress)).gt(vUSDCBefore)).to.be.true; + } catch (error: unknown) { + if (isSwapError(error)) { + console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); + return; + } + throw error; + } + }); + + it("should revert with MarketNotListed for unlisted vToken", async () => { + const fakeVToken = "0x0000000000000000000000000000000000000001"; + await usdt.connect(testUser).approve(SWAP_ROUTER, parseUnits("100", 18)); + + await expect( + swapRouter.connect(testUser).swapAndSupply(fakeVToken, USDT, parseUnits("100", 18), 0, "0x"), + ).to.be.revertedWithCustomError(swapRouter, "MarketNotListed"); + }); + + it("should revert with ZeroAmount when amountIn is zero", async () => { + await expect( + swapRouter.connect(testUser).swapAndSupply(vUSDC, USDT, 0, 0, "0x"), + ).to.be.revertedWithCustomError(swapRouter, "ZeroAmount"); + }); + + it("should revert with ZeroAddress when vToken is zero", async () => { + await expect( + swapRouter + .connect(testUser) + .swapAndSupply(ethers.constants.AddressZero, USDT, parseUnits("100", 18), 0, "0x"), + ).to.be.revertedWithCustomError(swapRouter, "ZeroAddress"); + }); + }); + + // =========================================================================== + // swapAndRepay + // =========================================================================== + + describe("swapAndRepay", () => { + before(async () => { + // Setup: supply USDC as collateral and borrow USDT + const supplyAmount = parseUnits("5000", 18); + await usdc.connect(testUser).approve(vUSDC, supplyAmount); + await vUSDCContract.connect(testUser).mint(supplyAmount); + await comptroller.connect(testUser).enterMarkets([vUSDC]); + await vUSDTContract.connect(testUser).borrow(parseUnits("1000", 18)); + }); + + it("should swap USDC -> USDT and repay vUSDT borrow", async () => { + const amountIn = parseUnits("500", 18); + const borrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + expect(borrowBefore).to.be.gt(0); + + const { swapData, minAmountOut } = await getSwapData(USDC, USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); + + if (swapData === "0x") { + console.log(" [SKIP] Venus API unavailable"); + return; + } + + await usdc.connect(testUser).approve(SWAP_ROUTER, amountIn); + + try { + const tx = await swapRouter + .connect(testUser) + .swapAndRepay(vUSDT, USDC, amountIn, minAmountOut, swapData); + const receipt = await tx.wait(); + + const events = parseEventFromReceipt(receipt, "SwapAndRepay"); + expect(events.length).to.equal(1); + expect(events[0].args.amountRepaid).to.be.gt(0); + + const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + expect(borrowAfter).to.be.lt(borrowBefore); + } catch (error: unknown) { + if (isSwapError(error)) { + console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); + return; + } + throw error; + } + }); + + it("should revert with ZeroAddress when vToken is zero", async () => { + await expect( + swapRouter + .connect(testUser) + .swapAndRepay(ethers.constants.AddressZero, USDT, parseUnits("100", 18), 0, "0x"), + ).to.be.revertedWithCustomError(swapRouter, "ZeroAddress"); + }); + }); + + // =========================================================================== + // swapAndRepayFull + // =========================================================================== + + describe("swapAndRepayFull", () => { + before(async () => { + // Ensure a borrow position exists + const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + if (borrowBalance.eq(0)) { + const supplyAmount = parseUnits("2000", 18); + await usdc.connect(testUser).approve(vUSDC, supplyAmount); + await vUSDCContract.connect(testUser).mint(supplyAmount); + await vUSDTContract.connect(testUser).borrow(parseUnits("500", 18)); + } + }); + + it("should swap USDC -> USDT and fully repay vUSDT borrow", async () => { + const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + if (borrowBalance.eq(0)) { + console.log(" [SKIP] No borrow balance"); + return; + } + + const maxAmountIn = borrowBalance.mul(110).div(100); + const { swapData } = await getSwapData(USDC, USDT, maxAmountIn.toString(), SWAP_ROUTER, "0.01"); + + if (swapData === "0x") { + console.log(" [SKIP] Venus API unavailable"); + return; + } + + await usdc.connect(testUser).approve(SWAP_ROUTER, maxAmountIn); + + try { + const tx = await swapRouter.connect(testUser).swapAndRepayFull(vUSDT, USDC, maxAmountIn, swapData); + const receipt = await tx.wait(); + + const events = parseEventFromReceipt(receipt, "SwapAndRepay"); + expect(events.length).to.equal(1); + + const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + expect(borrowAfter).to.equal(0); + } catch (error: unknown) { + if (isSwapError(error)) { + console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); + return; + } + throw error; + } + }); + }); + + // =========================================================================== + // swapNativeAndSupply + // =========================================================================== + + describe("swapNativeAndSupply", () => { + it("should swap BNB -> USDT and supply to vUSDT", async () => { + const amountIn = parseUnits("1", 18); + const { swapData, minAmountOut } = await getNativeSwapData(USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); + + if (swapData === "0x") { + console.log(" [SKIP] Venus API unavailable"); + return; + } + + const vUSDTBefore = await vUSDTContract.balanceOf(testUserAddress); + + try { + const tx = await swapRouter + .connect(testUser) + .swapNativeAndSupply(vUSDT, minAmountOut, swapData, { value: amountIn }); + const receipt = await tx.wait(); + + const events = parseEventFromReceipt(receipt, "SwapAndSupply"); + expect(events.length).to.equal(1); + expect(events[0].args.tokenIn.toLowerCase()).to.equal(NATIVE_TOKEN_ADDR.toLowerCase()); + + expect((await vUSDTContract.balanceOf(testUserAddress)).gt(vUSDTBefore)).to.be.true; + } catch (error: unknown) { + if (isSwapError(error)) { + console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); + return; + } + throw error; + } + }); + }); + + // =========================================================================== + // swapNativeAndRepay + // =========================================================================== + + describe("swapNativeAndRepay", () => { + before(async () => { + const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + if (borrowBalance.eq(0)) { + const supplyAmount = parseUnits("2000", 18); + await usdc.connect(testUser).approve(vUSDC, supplyAmount); + await vUSDCContract.connect(testUser).mint(supplyAmount); + await vUSDTContract.connect(testUser).borrow(parseUnits("500", 18)); + } + }); + + it("should swap BNB -> USDT and repay vUSDT borrow", async () => { + const amountIn = parseUnits("0.5", 18); + const borrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + + if (borrowBefore.eq(0)) { + console.log(" [SKIP] No borrow balance"); + return; + } + + const { swapData, minAmountOut } = await getNativeSwapData(USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); + + if (swapData === "0x") { + console.log(" [SKIP] Venus API unavailable"); + return; + } + + try { + const tx = await swapRouter + .connect(testUser) + .swapNativeAndRepay(vUSDT, minAmountOut, swapData, { value: amountIn }); + const receipt = await tx.wait(); + + const events = parseEventFromReceipt(receipt, "SwapAndRepay"); + expect(events.length).to.equal(1); + + const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + expect(borrowAfter).to.be.lt(borrowBefore); + } catch (error: unknown) { + if (isSwapError(error)) { + console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); + return; + } + throw error; + } + }); + }); + + // =========================================================================== + // swapNativeAndRepayFull + // =========================================================================== + + describe("swapNativeAndRepayFull", () => { + before(async () => { + const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + if (borrowBalance.eq(0)) { + const supplyAmount = parseUnits("1000", 18); + await usdc.connect(testUser).approve(vUSDC, supplyAmount); + await vUSDCContract.connect(testUser).mint(supplyAmount); + await vUSDTContract.connect(testUser).borrow(parseUnits("100", 18)); + } + }); + + it("should swap BNB -> USDT and fully repay vUSDT borrow", async () => { + const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + if (borrowBalance.eq(0)) { + console.log(" [SKIP] No borrow balance"); + return; + } + + const amountIn = parseUnits("1", 18); + const { swapData } = await getNativeSwapData(USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); + + if (swapData === "0x") { + console.log(" [SKIP] Venus API unavailable"); + return; + } + + try { + const tx = await swapRouter + .connect(testUser) + .swapNativeAndRepayFull(vUSDT, swapData, { value: amountIn }); + const receipt = await tx.wait(); + + const events = parseEventFromReceipt(receipt, "SwapAndRepay"); + expect(events.length).to.equal(1); + + const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + expect(borrowAfter).to.equal(0); + } catch (error: unknown) { + if (isSwapError(error)) { + console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); + return; + } + throw error; + } + }); + }); + + // =========================================================================== + // sweepToken + // =========================================================================== + + describe("sweepToken", () => { + it("should allow owner to sweep ERC20 tokens stuck in the router", async () => { + // Send tokens to the router (simulating accidental transfer) + const accidentalAmount = parseUnits("10", 18); + const usdtHolder = await initMainnetUser(USDT_HOLDER, ethers.utils.parseEther("1")); + await usdt.connect(usdtHolder).transfer(SWAP_ROUTER, accidentalAmount); + + const routerBalanceBefore = await usdt.balanceOf(SWAP_ROUTER); + expect(routerBalanceBefore).to.be.gte(accidentalAmount); + + // sweepToken sends to owner() which is NORMAL_TIMELOCK + const ownerAddress = await swapRouter.owner(); + const ownerBalanceBefore = await usdt.balanceOf(ownerAddress); + + const tx = await swapRouter.connect(impersonatedTimelock).sweepToken(USDT); + const receipt = await tx.wait(); + + const events = parseEventFromReceipt(receipt, "SweepToken"); + expect(events.length).to.equal(1); + expect(events[0].args.token.toLowerCase()).to.equal(USDT.toLowerCase()); + expect(events[0].args.amount).to.equal(routerBalanceBefore); + + // Router balance should be zero after sweep + expect(await usdt.balanceOf(SWAP_ROUTER)).to.equal(0); + + // Owner should have received the tokens + const ownerBalanceAfter = await usdt.balanceOf(ownerAddress); + expect(ownerBalanceAfter.sub(ownerBalanceBefore)).to.equal(routerBalanceBefore); + }); + + it("should revert when called by non-owner", async () => { + await expect(swapRouter.connect(testUser).sweepToken(USDT)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + }); + + // =========================================================================== + // sweepNative + // =========================================================================== + + describe("sweepNative", () => { + it("should allow owner to sweep native BNB stuck in the router", async () => { + // The receive() only accepts BNB from WBNB, so use hardhat_setBalance + // to simulate BNB being stuck in the contract (e.g. from swap leftovers) + const forcedAmount = parseUnits("0.1", 18); + await ethers.provider.send("hardhat_setBalance", [ + SWAP_ROUTER, + ethers.utils.hexStripZeros(forcedAmount.toHexString()), + ]); + + const routerBalance = await ethers.provider.getBalance(SWAP_ROUTER); + expect(routerBalance).to.be.gt(0); + + const tx = await swapRouter.connect(impersonatedTimelock).sweepNative(); + const receipt = await tx.wait(); + + const events = parseEventFromReceipt(receipt, "SweepNative"); + expect(events.length).to.equal(1); + expect(events[0].args.amount).to.equal(routerBalance); + + // Router should have zero BNB after sweep + expect(await ethers.provider.getBalance(SWAP_ROUTER)).to.equal(0); + }); + + it("should revert when called by non-owner", async () => { + await expect(swapRouter.connect(testUser).sweepNative()).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("should revert when non-WBNB address sends BNB directly", async () => { + await expect( + testUser.sendTransaction({ to: SWAP_ROUTER, value: parseUnits("0.1", 18) }), + ).to.be.revertedWithCustomError(swapRouter, "UnauthorizedNativeSender"); + }); + }); + + // =========================================================================== + // Error Cases + // =========================================================================== + + describe("Error cases", () => { + it("should revert swapAndSupply with MarketNotListed for unlisted vToken", async () => { + await expect( + swapRouter + .connect(testUser) + .swapAndSupply("0x0000000000000000000000000000000000000001", USDT, parseUnits("100", 18), 0, "0x"), + ).to.be.revertedWithCustomError(swapRouter, "MarketNotListed"); + }); + + it("should revert swapAndSupply with SwapFailed for invalid swap data", async () => { + await usdt.connect(testUser).approve(SWAP_ROUTER, parseUnits("100", 18)); + + await expect( + swapRouter.connect(testUser).swapAndSupply(vUSDC, USDT, parseUnits("100", 18), 0, "0xdeadbeef"), + ).to.be.revertedWithCustomError(swapRouter, "SwapFailed"); + }); + }); + + // =========================================================================== + // Ownership + // =========================================================================== + + describe("Ownership", () => { + it("should have correct owner after VIP execution", async () => { + expect(await swapRouter.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); + }); + + it("should support two-step ownership transfer", async () => { + const newOwner = testUserAddress; + + await swapRouter.connect(impersonatedTimelock).transferOwnership(newOwner); + expect(await swapRouter.pendingOwner()).to.equal(newOwner); + + await swapRouter.connect(testUser).acceptOwnership(); + expect(await swapRouter.owner()).to.equal(newOwner); + + // Transfer back + await swapRouter.connect(testUser).transferOwnership(bscmainnet.NORMAL_TIMELOCK); + await swapRouter.connect(impersonatedTimelock).acceptOwnership(); + expect(await swapRouter.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); + }); + }); + + // =========================================================================== + // Integration: Full Supply -> Borrow -> Repay Cycle + // =========================================================================== + + describe("Integration: full supply -> borrow -> repay cycle", () => { + it("should complete entire user journey via swap router", async () => { + const signers = await ethers.getSigners(); + const integrationUser = signers[5]; + const integrationUserAddress = await integrationUser.getAddress(); + + await initMainnetUser(integrationUserAddress, parseUnits("10", 18)); + + // Step 1: Swap BNB -> USDC and supply + const bnbToSupply = parseUnits("2", 18); + const { swapData: supplySwapData, minAmountOut: supplyMinOut } = await getNativeSwapData( + USDC, + bnbToSupply.toString(), + SWAP_ROUTER, + "0.02", + ); + + if (supplySwapData === "0x") { + console.log(" [SKIP] Venus API unavailable"); + return; + } + + try { + await swapRouter + .connect(integrationUser) + .swapNativeAndSupply(vUSDC, supplyMinOut, supplySwapData, { value: bnbToSupply }); + + const vUSDCBalance = await vUSDCContract.balanceOf(integrationUserAddress); + expect(vUSDCBalance).to.be.gt(0); + + // Step 2: Enter market and borrow + await comptroller.connect(integrationUser).enterMarkets([vUSDC]); + await vUSDTContract.connect(integrationUser).borrow(parseUnits("100", 18)); + + const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(integrationUserAddress); + expect(borrowBalance).to.be.gt(0); + + // Step 3: Swap BNB -> USDT and repay + const bnbToRepay = parseUnits("0.5", 18); + const { swapData: repaySwapData, minAmountOut: repayMinOut } = await getNativeSwapData( + USDT, + bnbToRepay.toString(), + SWAP_ROUTER, + "0.02", + ); + + await swapRouter + .connect(integrationUser) + .swapNativeAndRepay(vUSDT, repayMinOut, repaySwapData, { value: bnbToRepay }); + + const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(integrationUserAddress); + expect(borrowAfter).to.be.lt(borrowBalance); + } catch (error: unknown) { + if (isSwapError(error)) { + console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); + return; + } + throw error; + } + }); + }); }); From f757c6d2bba949f7739bea9bca9a5ed9cda47d84 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 27 Jan 2026 13:51:05 +0530 Subject: [PATCH 4/7] test: Builds the full signed SwapHelper multicall calldata for a PancakeSwap V2 token swap --- simulations/vip-600/abi/SwapHelper.json | 444 +++++++++++++++++ simulations/vip-600/bscmainnet.ts | 613 ++++++++++++++---------- 2 files changed, 809 insertions(+), 248 deletions(-) create mode 100644 simulations/vip-600/abi/SwapHelper.json diff --git a/simulations/vip-600/abi/SwapHelper.json b/simulations/vip-600/abi/SwapHelper.json new file mode 100644 index 000000000..fb92e0573 --- /dev/null +++ b/simulations/vip-600/abi/SwapHelper.json @@ -0,0 +1,444 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "backendSigner_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "CallerNotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "DeadlineReached", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidShortString", + "type": "error" + }, + { + "inputs": [], + "name": "MissingSignature", + "type": "error" + }, + { + "inputs": [], + "name": "NoCallsProvided", + "type": "error" + }, + { + "inputs": [], + "name": "SaltAlreadyUsed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "str", + "type": "string" + } + ], + "name": "StringTooLong", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ApprovedMax", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldSigner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newSigner", + "type": "address" + } + ], + "name": "BackendSignerUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "GenericCallExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "callsCount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "name": "MulticallExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Swept", + "type": "event" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20Upgradeable", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "approveMax", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "backendSigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "genericCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "calls", + "type": "bytes[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "multicall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newSigner", + "type": "address" + } + ], + "name": "setBackendSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20Upgradeable", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "sweep", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "usedSalts", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-600/bscmainnet.ts b/simulations/vip-600/bscmainnet.ts index a632ebe9a..13bba5d4b 100644 --- a/simulations/vip-600/bscmainnet.ts +++ b/simulations/vip-600/bscmainnet.ts @@ -1,18 +1,15 @@ import { expect } from "chai"; -import { BigNumber, Contract, Signer } from "ethers"; +import { BigNumber, Contract, Signer, Wallet } 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 { initMainnetUser, setMaxStalePeriodInBinanceOracle, setMaxStalePeriodInChainlinkOracle } from "src/utils"; import { forking, testVip } from "src/vip-framework"; import { SWAP_HELPER, SWAP_ROUTER, UNITROLLER, vip600 } from "../../vips/vip-600/bscmainnet"; import COMPTROLLER_ABI from "./abi/Comptroller.json"; import ERC20_ABI from "./abi/ERC20.json"; +import SWAP_HELPER_ABI from "./abi/SwapHelper.json"; import SWAP_ROUTER_ABI from "./abi/SwapRouter.json"; import VTOKEN_ABI from "./abi/VToken.json"; @@ -36,51 +33,202 @@ const USDC_HOLDER = "0x8894E0a0c962CB723c1976a4421c95949bE2D4E3"; const NATIVE_TOKEN_ADDR = "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"; const FORK_BLOCK = 76556273; +// PancakeSwap V2 Router on BSC +const PANCAKE_V2_ROUTER = "0x10ED43C718714eb63d5aA57B78B54704E256024E"; +const PANCAKE_V2_ROUTER_ABI = [ + "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)", + "function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)", +]; + // ============================================================================= -// Swap Data Helper +// Manual Swap Calldata Builder (builds against forked chain state) // ============================================================================= +// Shared signer wallet for EIP-712 signing — set up once in before() hook +let swapSignerWallet: Wallet; +let swapHelperContract: Contract; +let eip712Domain: { name: string; version: string; chainId: number; verifyingContract: string }; +let saltCounter = 0; + +/** + * Configures a deterministic EIP-712 signer for the SwapHelper contract on the forked chain. + * + * In production, the SwapHelper requires a backend signer to authorize multicall executions + * via EIP-712 signatures. In tests, we: + * 1. Create a deterministic Hardhat wallet (using Hardhat's default private key) + * 2. Impersonate the SwapHelper owner to register this wallet as the authorized backendSigner + * 3. Cache the EIP-712 domain parameters (name, version, chainId, verifyingContract) for later signing + * + * This must be called once in the before() hook before any swap tests that need signed calldata. + */ +async function setupSwapSigner() { + // Create a deterministic wallet for signing + swapSignerWallet = new Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + + swapHelperContract = new ethers.Contract(SWAP_HELPER, SWAP_HELPER_ABI, ethers.provider); + + // Impersonate SwapHelper owner and set our wallet as backendSigner + const swapHelperOwner = await swapHelperContract.owner(); + const impersonatedOwner = await initMainnetUser(swapHelperOwner, ethers.utils.parseEther("1")); + await swapHelperContract.connect(impersonatedOwner).setBackendSigner(swapSignerWallet.address); + + // Read EIP-712 domain — use the actual chainId from the network (hardhat fork uses 31337, not 56) + const domain = await swapHelperContract.eip712Domain(); + const network = await ethers.provider.getNetwork(); + eip712Domain = { + name: domain.name, + version: domain.version, + chainId: network.chainId, + verifyingContract: domain.verifyingContract, + }; +} + +/** + * Builds the full signed SwapHelper multicall calldata for a PancakeSwap V2 token swap. + * + * This function simulates what the backend swap service does in production: + * 1. Queries PancakeSwap V2 for the expected output amount (tries direct pair, falls back to WBNB route) + * 2. Applies slippage tolerance to compute minAmountOut + * 3. Encodes a 3-step SwapHelper multicall: + * a. approveMax — approve PancakeSwap router to spend tokenIn held by SwapHelper + * b. genericCall — execute swapExactTokensForTokens on PancakeSwap, receiving tokens to SwapHelper + * c. sweep — transfer the swapped tokens from SwapHelper to the recipient (SwapRouter) + * 4. Signs the multicall with EIP-712 using the test backend signer wallet + * 5. Returns the encoded multicall calldata ready to pass to SwapRouter functions + * + * @param tokenIn - Address of the input token (use NATIVE_TOKEN_ADDR for BNB) + * @param tokenOut - Address of the desired output token + * @param amountIn - Exact amount of tokenIn to swap (in wei/mantissa) + * @param recipient - Address to receive swapped tokens (typically SWAP_ROUTER) + * @param slippageBps - Slippage tolerance in basis points (default: 100 = 1%) + * @returns swapData (encoded multicall bytes), minAmountOut, and expected amountOut + */ +async function buildSwapCalldata( + tokenIn: string, + tokenOut: string, + amountIn: BigNumber, + recipient: string, + slippageBps: number = 100, // 1% default +): Promise<{ swapData: string; minAmountOut: BigNumber; amountOut: BigNumber }> { + const pancakeRouter = new ethers.Contract(PANCAKE_V2_ROUTER, PANCAKE_V2_ROUTER_ABI, ethers.provider); + + // For native swaps, the actual tokenIn on the DEX is WBNB + const actualTokenIn = tokenIn.toLowerCase() === NATIVE_TOKEN_ADDR.toLowerCase() ? WBNB : tokenIn; + + // Build path — direct pair or via WBNB + let path: string[]; + let amounts: BigNumber[]; + + try { + // Try direct path first + path = [actualTokenIn, tokenOut]; + amounts = await pancakeRouter.getAmountsOut(amountIn, path); + } catch { + // If direct path fails, route via WBNB + if (actualTokenIn !== WBNB && tokenOut !== WBNB) { + path = [actualTokenIn, WBNB, tokenOut]; + amounts = await pancakeRouter.getAmountsOut(amountIn, path); + } else { + throw new Error(`No route found for ${tokenIn} -> ${tokenOut}`); + } + } + + const amountOut = amounts[amounts.length - 1]; + const minAmountOut = amountOut.mul(10000 - slippageBps).div(10000); + + // Build the SwapHelper multicall calls array + const swapHelperIface = new ethers.utils.Interface(SWAP_HELPER_ABI); + + // 1. approveMax(tokenIn, PancakeRouter) — let SwapHelper approve the DEX + const approveCall = swapHelperIface.encodeFunctionData("approveMax", [actualTokenIn, PANCAKE_V2_ROUTER]); + + // 2. genericCall(PancakeRouter, swapExactTokensForTokens(...)) + const pancakeIface = new ethers.utils.Interface(PANCAKE_V2_ROUTER_ABI); + const deadline = Math.floor(Date.now() / 1000) + 3600; + const swapCalldata = pancakeIface.encodeFunctionData("swapExactTokensForTokens", [ + amountIn, + minAmountOut, + path, + SWAP_HELPER, // tokens go to SwapHelper first + deadline, + ]); + const genericCall = swapHelperIface.encodeFunctionData("genericCall", [PANCAKE_V2_ROUTER, swapCalldata]); + + // 3. sweep(tokenOut, recipient) — send swapped tokens to SwapRouter + const sweepCall = swapHelperIface.encodeFunctionData("sweep", [tokenOut, recipient]); + + const calls = [approveCall, genericCall, sweepCall]; + + // EIP-712 sign — type includes "address caller" which is msg.sender (SwapRouter) + const salt = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["uint256"], [++saltCounter])); + + const types = { + Multicall: [ + { name: "caller", type: "address" }, + { name: "calls", type: "bytes[]" }, + { name: "deadline", type: "uint256" }, + { name: "salt", type: "bytes32" }, + ], + }; + + const value = { + caller: SWAP_ROUTER, + calls, + deadline, + salt, + }; + + const signature = await swapSignerWallet._signTypedData(eip712Domain, types, value); + + // Encode the full multicall call + const multicallData = swapHelperIface.encodeFunctionData("multicall", [calls, deadline, salt, signature]); + + return { swapData: multicallData, minAmountOut, amountOut }; +} + +/** + * High-level wrapper around buildSwapCalldata that accepts string-based parameters + * matching the interface used by test cases. Converts the slippage percentage (e.g. "0.01" + * for 1%) to basis points and the amount string to BigNumber before delegating. + * + * @param tokenInAddress - Address of the input token + * @param tokenOutAddress - Address of the desired output token + * @param exactAmountInMantissa - Input amount as a string (in wei/mantissa) + * @param recipientAddress - Address to receive swapped tokens + * @param slippagePercentage - Slippage as a decimal string (e.g. "0.01" = 1%) + */ async function getSwapData( tokenInAddress: string, tokenOutAddress: string, exactAmountInMantissa: string, recipientAddress: string, - slippagePercentage: string = "0.005", + slippagePercentage: string = "0.01", ): Promise<{ swapData: string; minAmountOut: BigNumber; amountOut: BigNumber }> { - const deadlineTimestamp = Math.floor(Date.now() / 1000) + 3600; - const url = `https://api.venus.io/find-swap?chainId=56&tokenInAddress=${tokenInAddress}&tokenOutAddress=${tokenOutAddress}&slippagePercentage=${slippagePercentage}&recipientAddress=${recipientAddress}&deadlineTimestampSecs=${deadlineTimestamp}&type=exact-in&shouldTransferToReceiver=true&exactAmountInMantissa=${exactAmountInMantissa}`; - - try { - const response = await fetch(url); - const data: unknown = await response.json(); - - if ( - typeof data === "object" && - data !== null && - "quotes" in data && - Array.isArray((data as any).quotes) && - (data as any).quotes.length > 0 - ) { - const quote = (data as any).quotes[0]; - const amountOut = BigNumber.from(quote.amountOut); - return { - swapData: quote.swapHelperMulticall.calldata.encodedCall, - minAmountOut: amountOut.mul(99).div(100), - amountOut, - }; - } - throw new Error("No quotes returned from Venus API"); - } catch (error) { - console.log(" [WARN] Failed to fetch swap data from Venus API:", error); - return { swapData: "0x", minAmountOut: BigNumber.from(0), amountOut: BigNumber.from(0) }; - } + const slippageBps = Math.round(parseFloat(slippagePercentage) * 10000); + return buildSwapCalldata( + tokenInAddress, + tokenOutAddress, + BigNumber.from(exactAmountInMantissa), + recipientAddress, + slippageBps, + ); } +/** + * Convenience wrapper for native BNB swaps. Sets tokenIn to the sentinel NATIVE_TOKEN_ADDR + * (0xbBbB...bBbB) which buildSwapCalldata translates to WBNB for the actual DEX swap. + * Used by swapNativeAndSupply, swapNativeAndRepay, and swapNativeAndRepayFull tests. + * + * @param tokenOutAddress - Address of the desired output token + * @param exactAmountInMantissa - BNB amount as a string (in wei) + * @param recipientAddress - Address to receive swapped tokens + * @param slippagePercentage - Slippage as a decimal string (e.g. "0.01" = 1%) + */ async function getNativeSwapData( tokenOutAddress: string, exactAmountInMantissa: string, recipientAddress: string, - slippagePercentage: string = "0.005", + slippagePercentage: string = "0.01", ): Promise<{ swapData: string; minAmountOut: BigNumber; amountOut: BigNumber }> { return getSwapData(NATIVE_TOKEN_ADDR, tokenOutAddress, exactAmountInMantissa, recipientAddress, slippagePercentage); } @@ -102,21 +250,12 @@ function parseEventFromReceipt(receipt: any, eventName: string): any[] { .filter((e: { name: string } | null) => e && e.name === eventName); } -// Checks if an error is a known swap failure (stale quote, route unavailable, etc.) -function isSwapError(error: unknown): boolean { - const msg = error instanceof Error ? error.message : String(error); - return ( - msg.includes("SwapFailed") || - msg.includes("InsufficientAmountOut") || - msg.includes("0x81ceff30") || // SwapFailed() selector - msg.includes("NoTokensReceived") - ); -} - // ============================================================================= // Test Suite // ============================================================================= +// Fork BSC mainnet at a specific block to get deterministic test results. +// All tests run against this snapshot, with the VIP executed mid-suite via testVip(). forking(FORK_BLOCK, async () => { let swapRouter: Contract; let comptroller: Contract; @@ -129,6 +268,7 @@ forking(FORK_BLOCK, async () => { let testUserAddress: string; before(async () => { + // Initialize contract instances for the SwapRouter, Comptroller, tokens, and vTokens swapRouter = await ethers.getContractAt(SWAP_ROUTER_ABI, SWAP_ROUTER); comptroller = new ethers.Contract(UNITROLLER, COMPTROLLER_ABI, ethers.provider); usdt = new ethers.Contract(USDT, ERC20_ABI, ethers.provider); @@ -136,19 +276,22 @@ forking(FORK_BLOCK, async () => { vUSDTContract = new ethers.Contract(vUSDT, VTOKEN_ABI, ethers.provider); vUSDCContract = new ethers.Contract(vUSDC, VTOKEN_ABI, ethers.provider); + // Impersonate the timelock to execute owner-only functions (sweepToken, transferOwnership, etc.) impersonatedTimelock = await initMainnetUser(bscmainnet.NORMAL_TIMELOCK, ethers.utils.parseEther("10")); const signers = await ethers.getSigners(); testUser = signers[0]; testUserAddress = await testUser.getAddress(); - // Fund test user + // Fund the test user with 10,000 USDT and 10,000 USDC from whale accounts + // so they have enough tokens for all swap and supply/repay tests const usdtHolder = await initMainnetUser(USDT_HOLDER, ethers.utils.parseEther("10")); const usdcHolder = await initMainnetUser(USDC_HOLDER, ethers.utils.parseEther("10")); await usdt.connect(usdtHolder).transfer(testUserAddress, parseUnits("10000", 18)); await usdc.connect(usdcHolder).transfer(testUserAddress, parseUnits("10000", 18)); - // Set stale periods for oracles + // Set oracle stale periods to ~10 years so price feeds don't revert as "stale" + // on the forked chain (block timestamps are in the past relative to real time) await setMaxStalePeriodInChainlinkOracle( bscmainnet.CHAINLINK_ORACLE, USDC, @@ -165,6 +308,10 @@ forking(FORK_BLOCK, async () => { ); await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, "USDC", 315360000); await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, "USDT", 315360000); + + // Setup the EIP-712 swap signer so we can build signed multicall calldata + // for PancakeSwap swaps routed through the SwapHelper contract + await setupSwapSigner(); }); // =========================================================================== @@ -172,6 +319,8 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("Pre-VIP behavior", () => { + // Verify that before the VIP is executed, the SwapRouter either already has + // NORMAL_TIMELOCK as owner or has it set as pendingOwner (awaiting acceptOwnership). it("should have correct ownership state", async () => { const pendingOwner = await swapRouter.pendingOwner(); const owner = await swapRouter.owner(); @@ -179,6 +328,8 @@ forking(FORK_BLOCK, async () => { expect(isValidState).to.be.true; }); + // Validate all immutable constructor parameters were set correctly during deployment. + // These cannot be changed after deployment, so we confirm them before VIP execution. it("should have correct COMPTROLLER", async () => { expect(await swapRouter.COMPTROLLER()).to.equal(UNITROLLER); }); @@ -204,6 +355,8 @@ forking(FORK_BLOCK, async () => { // VIP Execution // =========================================================================== + // Execute the VIP proposal on the forked chain. After execution, verify that + // at most one OwnershipTransferred event was emitted (the acceptOwnership call). testVip("VIP-600", await vip600(), { callbackAfterExecution: async txResponse => { const receipt = await txResponse.wait(); @@ -217,6 +370,8 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("Post-VIP behavior", () => { + // After VIP execution, ownership should be fully transferred to NORMAL_TIMELOCK + // and pendingOwner should be cleared (zero address), confirming acceptOwnership succeeded. it("should have NORMAL_TIMELOCK as owner", async () => { expect(await swapRouter.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); }); @@ -231,43 +386,36 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("swapAndSupply", () => { + // Happy path: User swaps 100 USDT for USDC via PancakeSwap, then the router + // automatically supplies the received USDC into the vUSDC market on behalf of the user. + // Verifies: SwapAndSupply event emitted, USDT balance decreased, vUSDC balance increased. it("should swap USDT -> USDC and supply to vUSDC", async () => { const amountIn = parseUnits("100", 18); + // Build swap calldata: USDT -> USDC via PancakeSwap V2, with 1% slippage tolerance const { swapData, minAmountOut } = await getSwapData(USDT, USDC, amountIn.toString(), SWAP_ROUTER, "0.01"); - if (swapData === "0x") { - console.log(" [SKIP] Venus API unavailable"); - return; - } - const usdtBefore = await usdt.balanceOf(testUserAddress); const vUSDCBefore = await vUSDCContract.balanceOf(testUserAddress); + // Approve the SwapRouter to pull USDT from the user await usdt.connect(testUser).approve(SWAP_ROUTER, amountIn); - try { - const tx = await swapRouter - .connect(testUser) - .swapAndSupply(vUSDC, USDT, amountIn, minAmountOut, swapData); - const receipt = await tx.wait(); - - const events = parseEventFromReceipt(receipt, "SwapAndSupply"); - expect(events.length).to.equal(1); - expect(events[0].args.user.toLowerCase()).to.equal(testUserAddress.toLowerCase()); - expect(events[0].args.amountIn).to.equal(amountIn); - expect(events[0].args.amountOut).to.be.gte(minAmountOut); - - expect((await usdt.balanceOf(testUserAddress)).lt(usdtBefore)).to.be.true; - expect((await vUSDCContract.balanceOf(testUserAddress)).gt(vUSDCBefore)).to.be.true; - } catch (error: unknown) { - if (isSwapError(error)) { - console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); - return; - } - throw error; - } + const tx = await swapRouter.connect(testUser).swapAndSupply(vUSDC, USDT, amountIn, minAmountOut, swapData); + const receipt = await tx.wait(); + + // Verify the SwapAndSupply event contains the correct user, input amount, and output >= minAmountOut + const events = parseEventFromReceipt(receipt, "SwapAndSupply"); + expect(events.length).to.equal(1); + expect(events[0].args.user.toLowerCase()).to.equal(testUserAddress.toLowerCase()); + expect(events[0].args.amountIn).to.equal(amountIn); + expect(events[0].args.amountOut).to.be.gte(minAmountOut); + + // Confirm token balances changed: USDT spent, vUSDC (supply receipt tokens) received + expect((await usdt.balanceOf(testUserAddress)).lt(usdtBefore)).to.be.true; + expect((await vUSDCContract.balanceOf(testUserAddress)).gt(vUSDCBefore)).to.be.true; }); + // Supplying to a vToken that isn't listed in the Comptroller should revert it("should revert with MarketNotListed for unlisted vToken", async () => { const fakeVToken = "0x0000000000000000000000000000000000000001"; await usdt.connect(testUser).approve(SWAP_ROUTER, parseUnits("100", 18)); @@ -277,17 +425,18 @@ forking(FORK_BLOCK, async () => { ).to.be.revertedWithCustomError(swapRouter, "MarketNotListed"); }); + // Zero input amount is rejected to prevent no-op transactions it("should revert with ZeroAmount when amountIn is zero", async () => { - await expect( - swapRouter.connect(testUser).swapAndSupply(vUSDC, USDT, 0, 0, "0x"), - ).to.be.revertedWithCustomError(swapRouter, "ZeroAmount"); + await expect(swapRouter.connect(testUser).swapAndSupply(vUSDC, USDT, 0, 0, "0x")).to.be.revertedWithCustomError( + swapRouter, + "ZeroAmount", + ); }); + // Zero address for vToken is rejected as an input validation check it("should revert with ZeroAddress when vToken is zero", async () => { await expect( - swapRouter - .connect(testUser) - .swapAndSupply(ethers.constants.AddressZero, USDT, parseUnits("100", 18), 0, "0x"), + swapRouter.connect(testUser).swapAndSupply(ethers.constants.AddressZero, USDT, parseUnits("100", 18), 0, "0x"), ).to.be.revertedWithCustomError(swapRouter, "ZeroAddress"); }); }); @@ -298,7 +447,8 @@ forking(FORK_BLOCK, async () => { describe("swapAndRepay", () => { before(async () => { - // Setup: supply USDC as collateral and borrow USDT + // Create a borrow position: supply 5000 USDC as collateral, enter the market, + // then borrow 1000 USDT against it. This sets up the state needed for repay tests. const supplyAmount = parseUnits("5000", 18); await usdc.connect(testUser).approve(vUSDC, supplyAmount); await vUSDCContract.connect(testUser).mint(supplyAmount); @@ -306,6 +456,8 @@ forking(FORK_BLOCK, async () => { await vUSDTContract.connect(testUser).borrow(parseUnits("1000", 18)); }); + // Happy path: User swaps 500 USDC for USDT, and the router uses the received USDT + // to repay part of the user's outstanding vUSDT borrow. Confirms borrow balance decreased. it("should swap USDC -> USDT and repay vUSDT borrow", async () => { const amountIn = parseUnits("500", 18); const borrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); @@ -313,39 +465,24 @@ forking(FORK_BLOCK, async () => { const { swapData, minAmountOut } = await getSwapData(USDC, USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); - if (swapData === "0x") { - console.log(" [SKIP] Venus API unavailable"); - return; - } - await usdc.connect(testUser).approve(SWAP_ROUTER, amountIn); - try { - const tx = await swapRouter - .connect(testUser) - .swapAndRepay(vUSDT, USDC, amountIn, minAmountOut, swapData); - const receipt = await tx.wait(); - - const events = parseEventFromReceipt(receipt, "SwapAndRepay"); - expect(events.length).to.equal(1); - expect(events[0].args.amountRepaid).to.be.gt(0); - - const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); - expect(borrowAfter).to.be.lt(borrowBefore); - } catch (error: unknown) { - if (isSwapError(error)) { - console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); - return; - } - throw error; - } + const tx = await swapRouter.connect(testUser).swapAndRepay(vUSDT, USDC, amountIn, minAmountOut, swapData); + const receipt = await tx.wait(); + + // Verify SwapAndRepay event was emitted with a non-zero repaid amount + const events = parseEventFromReceipt(receipt, "SwapAndRepay"); + expect(events.length).to.equal(1); + expect(events[0].args.amountRepaid).to.be.gt(0); + + // Borrow balance should have decreased after partial repayment + const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + expect(borrowAfter).to.be.lt(borrowBefore); }); it("should revert with ZeroAddress when vToken is zero", async () => { await expect( - swapRouter - .connect(testUser) - .swapAndRepay(ethers.constants.AddressZero, USDT, parseUnits("100", 18), 0, "0x"), + swapRouter.connect(testUser).swapAndRepay(ethers.constants.AddressZero, USDT, parseUnits("100", 18), 0, "0x"), ).to.be.revertedWithCustomError(swapRouter, "ZeroAddress"); }); }); @@ -356,7 +493,7 @@ forking(FORK_BLOCK, async () => { describe("swapAndRepayFull", () => { before(async () => { - // Ensure a borrow position exists + // Ensure a borrow position exists; if previous tests fully repaid, re-borrow const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBalance.eq(0)) { const supplyAmount = parseUnits("2000", 18); @@ -366,6 +503,10 @@ forking(FORK_BLOCK, async () => { } }); + // Full repayment: swaps enough USDC -> USDT to cover the entire outstanding borrow. + // Uses 110% of borrow balance as maxAmountIn to account for swap slippage and accrued interest. + // The router repays only what's owed and refunds any excess tokens back to the user. + // After execution, borrow balance should be exactly zero. it("should swap USDC -> USDT and fully repay vUSDT borrow", async () => { const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBalance.eq(0)) { @@ -373,32 +514,21 @@ forking(FORK_BLOCK, async () => { return; } + // Provide 10% buffer over borrow balance to cover slippage and accrued interest const maxAmountIn = borrowBalance.mul(110).div(100); const { swapData } = await getSwapData(USDC, USDT, maxAmountIn.toString(), SWAP_ROUTER, "0.01"); - if (swapData === "0x") { - console.log(" [SKIP] Venus API unavailable"); - return; - } - await usdc.connect(testUser).approve(SWAP_ROUTER, maxAmountIn); - try { - const tx = await swapRouter.connect(testUser).swapAndRepayFull(vUSDT, USDC, maxAmountIn, swapData); - const receipt = await tx.wait(); - - const events = parseEventFromReceipt(receipt, "SwapAndRepay"); - expect(events.length).to.equal(1); - - const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); - expect(borrowAfter).to.equal(0); - } catch (error: unknown) { - if (isSwapError(error)) { - console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); - return; - } - throw error; - } + const tx = await swapRouter.connect(testUser).swapAndRepayFull(vUSDT, USDC, maxAmountIn, swapData); + const receipt = await tx.wait(); + + const events = parseEventFromReceipt(receipt, "SwapAndRepay"); + expect(events.length).to.equal(1); + + // Borrow balance should be fully cleared + const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + expect(borrowAfter).to.equal(0); }); }); @@ -407,35 +537,28 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("swapNativeAndSupply", () => { + // Happy path: User sends 1 BNB which the router wraps to WBNB, swaps to USDT + // via PancakeSwap, then supplies the USDT into the vUSDT market for the user. + // The event reports WBNB as tokenIn since the router wraps before swapping. it("should swap BNB -> USDT and supply to vUSDT", async () => { const amountIn = parseUnits("1", 18); const { swapData, minAmountOut } = await getNativeSwapData(USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); - if (swapData === "0x") { - console.log(" [SKIP] Venus API unavailable"); - return; - } - const vUSDTBefore = await vUSDTContract.balanceOf(testUserAddress); - try { - const tx = await swapRouter - .connect(testUser) - .swapNativeAndSupply(vUSDT, minAmountOut, swapData, { value: amountIn }); - const receipt = await tx.wait(); - - const events = parseEventFromReceipt(receipt, "SwapAndSupply"); - expect(events.length).to.equal(1); - expect(events[0].args.tokenIn.toLowerCase()).to.equal(NATIVE_TOKEN_ADDR.toLowerCase()); - - expect((await vUSDTContract.balanceOf(testUserAddress)).gt(vUSDTBefore)).to.be.true; - } catch (error: unknown) { - if (isSwapError(error)) { - console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); - return; - } - throw error; - } + // Send BNB as msg.value; the router handles wrapping to WBNB internally + const tx = await swapRouter + .connect(testUser) + .swapNativeAndSupply(vUSDT, minAmountOut, swapData, { value: amountIn }); + const receipt = await tx.wait(); + + const events = parseEventFromReceipt(receipt, "SwapAndSupply"); + expect(events.length).to.equal(1); + // SwapRouter wraps BNB to WBNB before swapping, so tokenIn in the event is WBNB + expect(events[0].args.tokenIn.toLowerCase()).to.equal(WBNB.toLowerCase()); + + // User should have received vUSDT tokens representing their supply position + expect((await vUSDTContract.balanceOf(testUserAddress)).gt(vUSDTBefore)).to.be.true; }); }); @@ -445,6 +568,7 @@ forking(FORK_BLOCK, async () => { describe("swapNativeAndRepay", () => { before(async () => { + // Ensure a borrow position exists for the repay test const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBalance.eq(0)) { const supplyAmount = parseUnits("2000", 18); @@ -454,6 +578,8 @@ forking(FORK_BLOCK, async () => { } }); + // Partial repayment using native BNB: sends 0.5 BNB which is wrapped to WBNB, + // swapped to USDT, and used to partially repay the user's vUSDT borrow position. it("should swap BNB -> USDT and repay vUSDT borrow", async () => { const amountIn = parseUnits("0.5", 18); const borrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); @@ -465,29 +591,17 @@ forking(FORK_BLOCK, async () => { const { swapData, minAmountOut } = await getNativeSwapData(USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); - if (swapData === "0x") { - console.log(" [SKIP] Venus API unavailable"); - return; - } + const tx = await swapRouter + .connect(testUser) + .swapNativeAndRepay(vUSDT, minAmountOut, swapData, { value: amountIn }); + const receipt = await tx.wait(); - try { - const tx = await swapRouter - .connect(testUser) - .swapNativeAndRepay(vUSDT, minAmountOut, swapData, { value: amountIn }); - const receipt = await tx.wait(); - - const events = parseEventFromReceipt(receipt, "SwapAndRepay"); - expect(events.length).to.equal(1); - - const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); - expect(borrowAfter).to.be.lt(borrowBefore); - } catch (error: unknown) { - if (isSwapError(error)) { - console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); - return; - } - throw error; - } + const events = parseEventFromReceipt(receipt, "SwapAndRepay"); + expect(events.length).to.equal(1); + + // Borrow balance should have decreased after partial repayment with native BNB + const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + expect(borrowAfter).to.be.lt(borrowBefore); }); }); @@ -497,6 +611,7 @@ forking(FORK_BLOCK, async () => { describe("swapNativeAndRepayFull", () => { before(async () => { + // Ensure a borrow position exists for the full repay test const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBalance.eq(0)) { const supplyAmount = parseUnits("1000", 18); @@ -506,6 +621,9 @@ forking(FORK_BLOCK, async () => { } }); + // Full repayment using native BNB: sends 1 BNB (more than enough to cover the ~100 USDT borrow). + // The router wraps BNB -> WBNB, swaps to USDT, repays the exact borrow amount, + // and refunds any excess tokens. Borrow balance should be zero after execution. it("should swap BNB -> USDT and fully repay vUSDT borrow", async () => { const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBalance.eq(0)) { @@ -513,32 +631,19 @@ forking(FORK_BLOCK, async () => { return; } + // Send more BNB than needed; the router handles exact repayment and refunds excess const amountIn = parseUnits("1", 18); const { swapData } = await getNativeSwapData(USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); - if (swapData === "0x") { - console.log(" [SKIP] Venus API unavailable"); - return; - } + const tx = await swapRouter.connect(testUser).swapNativeAndRepayFull(vUSDT, swapData, { value: amountIn }); + const receipt = await tx.wait(); - try { - const tx = await swapRouter - .connect(testUser) - .swapNativeAndRepayFull(vUSDT, swapData, { value: amountIn }); - const receipt = await tx.wait(); - - const events = parseEventFromReceipt(receipt, "SwapAndRepay"); - expect(events.length).to.equal(1); - - const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); - expect(borrowAfter).to.equal(0); - } catch (error: unknown) { - if (isSwapError(error)) { - console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); - return; - } - throw error; - } + const events = parseEventFromReceipt(receipt, "SwapAndRepay"); + expect(events.length).to.equal(1); + + // Borrow should be fully cleared + const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + expect(borrowAfter).to.equal(0); }); }); @@ -547,8 +652,11 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("sweepToken", () => { + // Recovery mechanism: if ERC20 tokens are accidentally sent directly to the router + // (not via a swap), the owner can sweep them out. This simulates an accidental + // transfer of 10 USDT to the router, then the owner recovers them. it("should allow owner to sweep ERC20 tokens stuck in the router", async () => { - // Send tokens to the router (simulating accidental transfer) + // Simulate accidental token transfer to the router contract const accidentalAmount = parseUnits("10", 18); const usdtHolder = await initMainnetUser(USDT_HOLDER, ethers.utils.parseEther("1")); await usdt.connect(usdtHolder).transfer(SWAP_ROUTER, accidentalAmount); @@ -556,13 +664,14 @@ forking(FORK_BLOCK, async () => { const routerBalanceBefore = await usdt.balanceOf(SWAP_ROUTER); expect(routerBalanceBefore).to.be.gte(accidentalAmount); - // sweepToken sends to owner() which is NORMAL_TIMELOCK + // sweepToken transfers the entire token balance to the owner (NORMAL_TIMELOCK) const ownerAddress = await swapRouter.owner(); const ownerBalanceBefore = await usdt.balanceOf(ownerAddress); const tx = await swapRouter.connect(impersonatedTimelock).sweepToken(USDT); const receipt = await tx.wait(); + // Verify SweepToken event reports the correct token and full amount swept const events = parseEventFromReceipt(receipt, "SweepToken"); expect(events.length).to.equal(1); expect(events[0].args.token.toLowerCase()).to.equal(USDT.toLowerCase()); @@ -576,6 +685,7 @@ forking(FORK_BLOCK, async () => { expect(ownerBalanceAfter.sub(ownerBalanceBefore)).to.equal(routerBalanceBefore); }); + // Only the owner (NORMAL_TIMELOCK) can sweep; regular users cannot drain the router it("should revert when called by non-owner", async () => { await expect(swapRouter.connect(testUser).sweepToken(USDT)).to.be.revertedWith( "Ownable: caller is not the owner", @@ -588,9 +698,10 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("sweepNative", () => { + // Recovery mechanism for native BNB stuck in the router (e.g. leftover from WBNB unwrapping). + // Since receive() only accepts BNB from WBNB, we use hardhat_setBalance to force BNB into the contract. it("should allow owner to sweep native BNB stuck in the router", async () => { - // The receive() only accepts BNB from WBNB, so use hardhat_setBalance - // to simulate BNB being stuck in the contract (e.g. from swap leftovers) + // Force 0.1 BNB into the router via hardhat cheatcode (can't send directly due to receive() guard) const forcedAmount = parseUnits("0.1", 18); await ethers.provider.send("hardhat_setBalance", [ SWAP_ROUTER, @@ -603,6 +714,7 @@ forking(FORK_BLOCK, async () => { const tx = await swapRouter.connect(impersonatedTimelock).sweepNative(); const receipt = await tx.wait(); + // Verify SweepNative event reports the full amount swept to the owner const events = parseEventFromReceipt(receipt, "SweepNative"); expect(events.length).to.equal(1); expect(events[0].args.amount).to.equal(routerBalance); @@ -611,12 +723,13 @@ forking(FORK_BLOCK, async () => { expect(await ethers.provider.getBalance(SWAP_ROUTER)).to.equal(0); }); + // Only the owner can sweep native BNB it("should revert when called by non-owner", async () => { - await expect(swapRouter.connect(testUser).sweepNative()).to.be.revertedWith( - "Ownable: caller is not the owner", - ); + await expect(swapRouter.connect(testUser).sweepNative()).to.be.revertedWith("Ownable: caller is not the owner"); }); + // The router's receive() function only accepts BNB from WBNB (during unwrap). + // Direct BNB transfers from any other address are rejected to prevent accidental sends. it("should revert when non-WBNB address sends BNB directly", async () => { await expect( testUser.sendTransaction({ to: SWAP_ROUTER, value: parseUnits("0.1", 18) }), @@ -629,6 +742,7 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("Error cases", () => { + // Attempting to supply into a market that isn't registered in the Comptroller should fail it("should revert swapAndSupply with MarketNotListed for unlisted vToken", async () => { await expect( swapRouter @@ -637,6 +751,8 @@ forking(FORK_BLOCK, async () => { ).to.be.revertedWithCustomError(swapRouter, "MarketNotListed"); }); + // Passing garbage swap calldata (0xdeadbeef) causes the SwapHelper multicall to fail, + // which the router surfaces as a SwapFailed error it("should revert swapAndSupply with SwapFailed for invalid swap data", async () => { await usdt.connect(testUser).approve(SWAP_ROUTER, parseUnits("100", 18)); @@ -655,16 +771,23 @@ forking(FORK_BLOCK, async () => { expect(await swapRouter.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); }); + // Two-step ownership transfer (Ownable2Step pattern): + // 1. Current owner calls transferOwnership(newOwner) -> sets pendingOwner + // 2. New owner calls acceptOwnership() -> becomes owner, pendingOwner cleared + // This prevents accidental transfers to wrong addresses (new owner must actively accept). + // After verifying the flow, ownership is transferred back to NORMAL_TIMELOCK for subsequent tests. it("should support two-step ownership transfer", async () => { const newOwner = testUserAddress; + // Step 1: Current owner proposes new owner await swapRouter.connect(impersonatedTimelock).transferOwnership(newOwner); expect(await swapRouter.pendingOwner()).to.equal(newOwner); + // Step 2: New owner accepts await swapRouter.connect(testUser).acceptOwnership(); expect(await swapRouter.owner()).to.equal(newOwner); - // Transfer back + // Cleanup: Transfer ownership back to NORMAL_TIMELOCK so other tests aren't affected await swapRouter.connect(testUser).transferOwnership(bscmainnet.NORMAL_TIMELOCK); await swapRouter.connect(impersonatedTimelock).acceptOwnership(); expect(await swapRouter.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); @@ -675,15 +798,21 @@ forking(FORK_BLOCK, async () => { // Integration: Full Supply -> Borrow -> Repay Cycle // =========================================================================== + // End-to-end integration test simulating a realistic user journey through the swap router: + // 1. A fresh user swaps BNB -> USDC and supplies it as collateral via swapNativeAndSupply + // 2. The user enters the USDC market and borrows USDT against their collateral + // 3. The user swaps BNB -> USDT and partially repays the borrow via swapNativeAndRepay + // This validates that all swap router functions work together in a real lending workflow. describe("Integration: full supply -> borrow -> repay cycle", () => { it("should complete entire user journey via swap router", async () => { + // Use a separate signer (signers[5]) to start with a clean slate — no prior positions const signers = await ethers.getSigners(); const integrationUser = signers[5]; const integrationUserAddress = await integrationUser.getAddress(); await initMainnetUser(integrationUserAddress, parseUnits("10", 18)); - // Step 1: Swap BNB -> USDC and supply + // Step 1: Swap 2 BNB -> USDC and supply to vUSDC (creates collateral position) const bnbToSupply = parseUnits("2", 18); const { swapData: supplySwapData, minAmountOut: supplyMinOut } = await getNativeSwapData( USDC, @@ -692,48 +821,36 @@ forking(FORK_BLOCK, async () => { "0.02", ); - if (supplySwapData === "0x") { - console.log(" [SKIP] Venus API unavailable"); - return; - } + await swapRouter + .connect(integrationUser) + .swapNativeAndSupply(vUSDC, supplyMinOut, supplySwapData, { value: bnbToSupply }); - try { - await swapRouter - .connect(integrationUser) - .swapNativeAndSupply(vUSDC, supplyMinOut, supplySwapData, { value: bnbToSupply }); - - const vUSDCBalance = await vUSDCContract.balanceOf(integrationUserAddress); - expect(vUSDCBalance).to.be.gt(0); - - // Step 2: Enter market and borrow - await comptroller.connect(integrationUser).enterMarkets([vUSDC]); - await vUSDTContract.connect(integrationUser).borrow(parseUnits("100", 18)); - - const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(integrationUserAddress); - expect(borrowBalance).to.be.gt(0); - - // Step 3: Swap BNB -> USDT and repay - const bnbToRepay = parseUnits("0.5", 18); - const { swapData: repaySwapData, minAmountOut: repayMinOut } = await getNativeSwapData( - USDT, - bnbToRepay.toString(), - SWAP_ROUTER, - "0.02", - ); - - await swapRouter - .connect(integrationUser) - .swapNativeAndRepay(vUSDT, repayMinOut, repaySwapData, { value: bnbToRepay }); - - const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(integrationUserAddress); - expect(borrowAfter).to.be.lt(borrowBalance); - } catch (error: unknown) { - if (isSwapError(error)) { - console.log(" [SKIP] Swap quote expired or route unavailable on forked chain"); - return; - } - throw error; - } + const vUSDCBalance = await vUSDCContract.balanceOf(integrationUserAddress); + expect(vUSDCBalance).to.be.gt(0); + + // Step 2: Enter the USDC market as collateral and borrow 100 USDT against it + await comptroller.connect(integrationUser).enterMarkets([vUSDC]); + await vUSDTContract.connect(integrationUser).borrow(parseUnits("100", 18)); + + const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(integrationUserAddress); + expect(borrowBalance).to.be.gt(0); + + // Step 3: Swap 0.5 BNB -> USDT and use it to partially repay the borrow + const bnbToRepay = parseUnits("0.5", 18); + const { swapData: repaySwapData, minAmountOut: repayMinOut } = await getNativeSwapData( + USDT, + bnbToRepay.toString(), + SWAP_ROUTER, + "0.02", + ); + + await swapRouter + .connect(integrationUser) + .swapNativeAndRepay(vUSDT, repayMinOut, repaySwapData, { value: bnbToRepay }); + + // Confirm borrow balance decreased after partial repayment + const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(integrationUserAddress); + expect(borrowAfter).to.be.lt(borrowBalance); }); }); }); From 1a6aed5543a9a7cf95e1b1311aa25b9dbb9274ac Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 27 Jan 2026 14:39:13 +0530 Subject: [PATCH 5/7] test: replace Venus API swap quotes with local EIP-712 signed calldata in vip-581 addendum2 Use deterministic EIP-712 signer for SwapHelper multicall instead of fetching live swap quotes from the Venus API, making tests reproducible on forked chains without external dependencies. --- simulations/vip-581/addendum2.ts | 182 +++++++++++++++++++++++++------ 1 file changed, 147 insertions(+), 35 deletions(-) diff --git a/simulations/vip-581/addendum2.ts b/simulations/vip-581/addendum2.ts index c2bb991ab..1d079564e 100644 --- a/simulations/vip-581/addendum2.ts +++ b/simulations/vip-581/addendum2.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { BigNumber, Contract, Signer } from "ethers"; +import { BigNumber, Contract, Signer, Wallet } from "ethers"; import { parseUnits } from "ethers/lib/utils"; import { ethers } from "hardhat"; import { NETWORK_ADDRESSES } from "src/networkAddresses"; @@ -22,6 +22,7 @@ import { } from "../../vips/vip-581/bscmainnet"; import VTOKEN_ABI from "../vip-567/abi/VToken.json"; import LEVERAGE_STRATEGIES_MANAGER_ABI from "../vip-576/abi/LeverageStrategiesManager.json"; +import SWAP_HELPER_ABI from "../vip-600/abi/SwapHelper.json"; import COMPTROLLER_ABI from "./abi/Comptroller.json"; import ERC20_ABI from "./abi/ERC20.json"; import RESILIENT_ORACLE_ABI from "./abi/ResilientOracle.json"; @@ -31,12 +32,21 @@ const { bscmainnet } = NETWORK_ADDRESSES; // Contract addresses const LEVERAGE_STRATEGIES_MANAGER = "0x03F079E809185a669Ca188676D0ADb09cbAd6dC1"; +const SWAP_HELPER = "0xD79be25aEe798Aa34A9Ba1230003d7499be29A24"; // Core Pool markets with flash loans enabled (from VIP-567) const vUSDC = "0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8"; const vUSDT = "0xfD5840Cd36d94D7229439859C0112a4185BC0255"; const USDC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"; const USDT = "0x55d398326f99059fF775485246999027B3197955"; +const WBNB = "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"; + +// PancakeSwap V2 Router on BSC +const PANCAKE_V2_ROUTER = "0x10ED43C718714eb63d5aA57B78B54704E256024E"; +const PANCAKE_V2_ROUTER_ABI = [ + "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)", + "function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)", +]; // User with U tokens to fund test user const U_HOLDER = "0x95282779ee2f3d4cf383041f7361c741cf8cc00e"; @@ -48,47 +58,145 @@ const USDT_HOLDER = "0xF977814e90dA44bFA03b6295A0616a897441aceC"; // Fork block number - UPDATE THIS when tests fail due to stale oracle data const FORK_BLOCK = 75075100; +// ============================================================================= +// EIP-712 Swap Signer & Calldata Builder +// ============================================================================= + +let swapSignerWallet: Wallet; +let swapHelperContract: Contract; +let eip712Domain: { name: string; version: string; chainId: number; verifyingContract: string }; +let saltCounter = 0; + /** - * Fetches swap data from Venus API for swapping tokens - * Uses swapHelperMulticall format required by LeverageStrategiesManager + * Configures a deterministic EIP-712 signer for the SwapHelper contract on the forked chain. + * + * In production, the SwapHelper requires a backend signer to authorize multicall executions + * via EIP-712 signatures. In tests, we: + * 1. Create a deterministic Hardhat wallet (using Hardhat's default private key) + * 2. Impersonate the SwapHelper owner to register this wallet as the authorized backendSigner + * 3. Cache the EIP-712 domain parameters (name, version, chainId, verifyingContract) for later signing */ -async function getSwapData( - tokenInAddress: string, - tokenOutAddress: string, - exactAmountInMantissa: string, - slippagePercentage: string = "0.01", +async function setupSwapSigner() { + swapSignerWallet = new Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); + + swapHelperContract = new ethers.Contract(SWAP_HELPER, SWAP_HELPER_ABI, ethers.provider); + + const swapHelperOwner = await swapHelperContract.owner(); + const impersonatedOwner = await initMainnetUser(swapHelperOwner, ethers.utils.parseEther("1")); + await swapHelperContract.connect(impersonatedOwner).setBackendSigner(swapSignerWallet.address); + + const domain = await swapHelperContract.eip712Domain(); + const network = await ethers.provider.getNetwork(); + eip712Domain = { + name: domain.name, + version: domain.version, + chainId: network.chainId, + verifyingContract: domain.verifyingContract, + }; +} + +/** + * Builds the full signed SwapHelper multicall calldata for a PancakeSwap V2 token swap. + * + * Steps: + * 1. Queries PancakeSwap V2 for the expected output amount + * 2. Applies slippage tolerance to compute minAmountOut + * 3. Encodes a 3-step SwapHelper multicall (approveMax, genericCall, sweep) + * 4. Signs the multicall with EIP-712 + * 5. Returns the encoded multicall calldata + */ +async function buildSwapCalldata( + tokenIn: string, + tokenOut: string, + amountIn: BigNumber, + recipient: string, + slippageBps: number = 100, ): Promise<{ swapData: string; minAmountOut: BigNumber }> { - const deadlineTimestamp = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now + const pancakeRouter = new ethers.Contract(PANCAKE_V2_ROUTER, PANCAKE_V2_ROUTER_ABI, ethers.provider); - const url = `https://api.venus.io/find-swap?chainId=56&tokenInAddress=${tokenInAddress}&tokenOutAddress=${tokenOutAddress}&slippagePercentage=${slippagePercentage}&recipientAddress=${LEVERAGE_STRATEGIES_MANAGER}&deadlineTimestampSecs=${deadlineTimestamp}&type=exact-in&shouldTransferToReceiver=true&exactAmountInMantissa=${exactAmountInMantissa}`; + let path: string[]; + let amounts: BigNumber[]; try { - const response = await fetch(url); - const data: unknown = await response.json(); - - // Type guard for expected Venus API response - if ( - typeof data === "object" && - data !== null && - "quotes" in data && - Array.isArray((data as any).quotes) && - (data as any).quotes.length > 0 - ) { - const quote = (data as any).quotes[0]; - return { - swapData: quote.swapHelperMulticall.calldata.encodedCall, - minAmountOut: BigNumber.from(quote.amountOut).mul(99).div(100), // 1% slippage buffer - }; + path = [tokenIn, tokenOut]; + amounts = await pancakeRouter.getAmountsOut(amountIn, path); + } catch { + if (tokenIn !== WBNB && tokenOut !== WBNB) { + path = [tokenIn, WBNB, tokenOut]; + amounts = await pancakeRouter.getAmountsOut(amountIn, path); + } else { + throw new Error(`No route found for ${tokenIn} -> ${tokenOut}`); } - throw new Error("No quotes returned from Venus API"); - } catch (error) { - console.log("Failed to fetch swap data from Venus API, using fallback"); - // Return empty swap data - tests will skip cross-asset tests - return { - swapData: "0x", - minAmountOut: BigNumber.from(0), - }; } + + const amountOut = amounts[amounts.length - 1]; + const minAmountOut = amountOut.mul(10000 - slippageBps).div(10000); + + const swapHelperIface = new ethers.utils.Interface(SWAP_HELPER_ABI); + + // 1. approveMax(tokenIn, PancakeRouter) + const approveCall = swapHelperIface.encodeFunctionData("approveMax", [tokenIn, PANCAKE_V2_ROUTER]); + + // 2. genericCall(PancakeRouter, swapExactTokensForTokens(...)) + const pancakeIface = new ethers.utils.Interface(PANCAKE_V2_ROUTER_ABI); + const deadline = Math.floor(Date.now() / 1000) + 3600; + const swapCalldata = pancakeIface.encodeFunctionData("swapExactTokensForTokens", [ + amountIn, + minAmountOut, + path, + SWAP_HELPER, // tokens go to SwapHelper first + deadline, + ]); + const genericCall = swapHelperIface.encodeFunctionData("genericCall", [PANCAKE_V2_ROUTER, swapCalldata]); + + // 3. sweep(tokenOut, recipient) — send swapped tokens to LeverageStrategiesManager + const sweepCall = swapHelperIface.encodeFunctionData("sweep", [tokenOut, recipient]); + + const calls = [approveCall, genericCall, sweepCall]; + + // EIP-712 sign — caller is LeverageStrategiesManager (it calls SwapHelper.multicall) + const salt = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["uint256"], [++saltCounter])); + + const types = { + Multicall: [ + { name: "caller", type: "address" }, + { name: "calls", type: "bytes[]" }, + { name: "deadline", type: "uint256" }, + { name: "salt", type: "bytes32" }, + ], + }; + + const value = { + caller: LEVERAGE_STRATEGIES_MANAGER, + calls, + deadline, + salt, + }; + + const signature = await swapSignerWallet._signTypedData(eip712Domain, types, value); + + const multicallData = swapHelperIface.encodeFunctionData("multicall", [calls, deadline, salt, signature]); + + return { swapData: multicallData, minAmountOut }; +} + +/** + * High-level wrapper matching the interface used by test cases. + */ +async function getSwapData( + tokenInAddress: string, + tokenOutAddress: string, + exactAmountInMantissa: string, + slippagePercentage: string = "0.01", +): Promise<{ swapData: string; minAmountOut: BigNumber }> { + const slippageBps = Math.round(parseFloat(slippagePercentage) * 10000); + return buildSwapCalldata( + tokenInAddress, + tokenOutAddress, + BigNumber.from(exactAmountInMantissa), + LEVERAGE_STRATEGIES_MANAGER, + slippageBps, + ); } forking(FORK_BLOCK, async () => { @@ -172,6 +280,9 @@ forking(FORK_BLOCK, async () => { ); await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, "USDC", 315360000); await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, "USDT", 315360000); + + // Setup the EIP-712 swap signer so we can build signed multicall calldata + await setupSwapSigner(); }); /** @@ -433,7 +544,8 @@ forking(FORK_BLOCK, async () => { } // For exitLeverage: flash loan USDT to repay, redeem USDC, swap USDC to USDT - const collateralAmountToRedeem = parseUnits("50", 18); + // Redeem more than borrow to cover swap slippage and flash loan fees + const collateralAmountToRedeem = parseUnits("55", 18); // Get swap data from Venus API (USDC -> USDT) const { swapData, minAmountOut } = await getSwapData(USDC, USDT, collateralAmountToRedeem.toString(), "0.01"); From e866156cb7dcafed1e403f878d508175aec616bc Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 27 Jan 2026 18:23:36 +0530 Subject: [PATCH 6/7] refactor: use U token for cross-asset leverage tests in vip-581 addendum2 Replace USDC with U token as collateral in cross-asset leverage tests (enterLeverage, exitLeverage, enterLeverageFromBorrow) since the VIP enables flash loans on vU. Update U_HOLDER to a whale with sufficient balance at the fork block and remove unused USDC references. --- simulations/vip-581/addendum2.ts | 144 ++++++++++++++----------------- 1 file changed, 66 insertions(+), 78 deletions(-) diff --git a/simulations/vip-581/addendum2.ts b/simulations/vip-581/addendum2.ts index 1d079564e..4caa1133f 100644 --- a/simulations/vip-581/addendum2.ts +++ b/simulations/vip-581/addendum2.ts @@ -34,8 +34,7 @@ const { bscmainnet } = NETWORK_ADDRESSES; const LEVERAGE_STRATEGIES_MANAGER = "0x03F079E809185a669Ca188676D0ADb09cbAd6dC1"; const SWAP_HELPER = "0xD79be25aEe798Aa34A9Ba1230003d7499be29A24"; -// Core Pool markets with flash loans enabled (from VIP-567) -const vUSDC = "0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8"; +// Core Pool markets const vUSDT = "0xfD5840Cd36d94D7229439859C0112a4185BC0255"; const USDC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"; const USDT = "0x55d398326f99059fF775485246999027B3197955"; @@ -49,9 +48,7 @@ const PANCAKE_V2_ROUTER_ABI = [ ]; // User with U tokens to fund test user -const U_HOLDER = "0x95282779ee2f3d4cf383041f7361c741cf8cc00e"; -// User with USDC tokens -const USDC_HOLDER = "0x8894E0a0c962CB723c1976a4421c95949bE2D4E3"; +const U_HOLDER = "0x8894E0a0c962CB723c1976a4421c95949bE2D4E3"; // User with USDT tokens const USDT_HOLDER = "0xF977814e90dA44bFA03b6295A0616a897441aceC"; @@ -203,9 +200,7 @@ forking(FORK_BLOCK, async () => { let vUContract: Contract; let leverageStrategiesManager: Contract; let u: Contract; - let usdc: Contract; let usdt: Contract; - let vUSDCContract: Contract; let vUSDTContract: Contract; let comptroller: Contract; let resilientOracle: Contract; @@ -223,9 +218,7 @@ forking(FORK_BLOCK, async () => { ethers.provider, ); u = new ethers.Contract(U, ERC20_ABI, ethers.provider); - usdc = new ethers.Contract(USDC, ERC20_ABI, ethers.provider); usdt = new ethers.Contract(USDT, ERC20_ABI, ethers.provider); - vUSDCContract = new ethers.Contract(vUSDC, VTOKEN_ABI, ethers.provider); vUSDTContract = new ethers.Contract(vUSDT, VTOKEN_ABI, ethers.provider); comptroller = new ethers.Contract(bscmainnet.UNITROLLER, COMPTROLLER_ABI, ethers.provider); resilientOracle = new ethers.Contract(RESILIENT_ORACLE, RESILIENT_ORACLE_ABI, ethers.provider); @@ -240,11 +233,10 @@ forking(FORK_BLOCK, async () => { // Fund test users with tokens const uHolder = await initMainnetUser(U_HOLDER, ethers.utils.parseEther("10")); - const usdcHolder = await initMainnetUser(USDC_HOLDER, ethers.utils.parseEther("10")); const usdtHolder = await initMainnetUser(USDT_HOLDER, ethers.utils.parseEther("10")); await u.connect(uHolder).transfer(await testUser.getAddress(), parseUnits("100", 18)); - await usdc.connect(usdcHolder).transfer(await leverageTestUser.getAddress(), parseUnits("1000", 18)); + await u.connect(uHolder).transfer(await leverageTestUser.getAddress(), parseUnits("1000", 18)); await usdt.connect(usdtHolder).transfer(await leverageTestUser.getAddress(), parseUnits("1000", 18)); // Set direct prices and extend stale periods for oracle compatibility @@ -404,27 +396,24 @@ forking(FORK_BLOCK, async () => { * ===================================================================== * * This test demonstrates a cross-asset leverage operation using the - * LeverageStrategiesManager contract. The user supplies USDC as collateral, - * borrows USDT via a flash loan, and swaps the borrowed USDT for more USDC - * to increase their leveraged position. The swap data is fetched live from - * the Venus API, which provides a signed quote for the swap. + * LeverageStrategiesManager contract. The user supplies U as collateral, + * borrows USDT via a flash loan, and swaps the borrowed USDT for more U + * to increase their leveraged position. * * Key steps: - * 1. User enters both USDC and USDT markets and delegates to the manager. - * 2. User approves the manager to spend their USDC. - * 3. The test fetches swap data for USDT->USDC from the Venus API. + * 1. User enters both vU and vUSDT markets and delegates to the manager. + * 2. User approves the manager to spend their U. + * 3. The test fetches swap data for USDT->U. * 4. The manager's enterLeverage is called, which: - * - Supplies USDC as seed collateral + * - Supplies U as seed collateral * - Borrows USDT via flash loan - * - Swaps USDT for more USDC (increasing collateral) + * - Swaps USDT for more U (increasing collateral) * - Leaves the user with a leveraged position * 5. The test verifies: * - The LeverageEntered event is emitted with correct parameters - * - The user's vUSDC balance increases + * - The user's vU balance increases * - The user's USDT borrow balance increases - * - The user's USDC wallet balance decreases by the seed amount - * - * This test will gracefully skip if the Venus API is unavailable or the swap fails. + * - The user's U wallet balance decreases by the seed amount */ describe("LeverageStrategiesManager: enterLeverage (cross-asset)", () => { let leverageUserAddress: string; @@ -432,51 +421,50 @@ forking(FORK_BLOCK, async () => { before(async () => { leverageUserAddress = await leverageTestUser.getAddress(); - // Enter markets for both USDC and USDT so the user can supply collateral and borrow - await comptroller.connect(leverageTestUser).enterMarkets([vUSDC, vUSDT]); + // Enter markets for both vU and vUSDT so the user can supply collateral and borrow + await comptroller.connect(leverageTestUser).enterMarkets([vU, vUSDT]); // Approve leverage manager to act on behalf of user (required for leverage operations) await comptroller.connect(leverageTestUser).updateDelegate(LEVERAGE_STRATEGIES_MANAGER, true); }); - it("should enter cross-asset leverage position (USDC collateral, USDT borrow) with value checks", async () => { - // User will supply 100 USDC as seed collateral + it("should enter cross-asset leverage position (U collateral, USDT borrow) with value checks", async () => { + // User will supply 100 U as seed collateral const collateralAmountSeed = parseUnits("100", 18); // User will borrow 50 USDT via flash loan const borrowedAmountToFlashLoan = parseUnits("50", 18); - // Fetch swap data from Venus API for swapping borrowed USDT to USDC - // This is required for the leverage manager to perform the swap on-chain - const { swapData, minAmountOut } = await getSwapData(USDT, USDC, borrowedAmountToFlashLoan.toString(), "0.01"); + // Fetch swap data for swapping borrowed USDT to U + const { swapData, minAmountOut } = await getSwapData(USDT, U, borrowedAmountToFlashLoan.toString(), "0.01"); // If swap data is unavailable, skip the test if (swapData === "0x") { - console.log("Skipping cross-asset enterLeverage test - Venus API unavailable"); + console.log("Skipping cross-asset enterLeverage test - swap data unavailable"); return; } // Record balances before leverage - const vUSDCBalanceBefore = await vUSDCContract.balanceOf(leverageUserAddress); + const vUBalanceBefore = await vUContract.balanceOf(leverageUserAddress); const usdtBorrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(leverageUserAddress); - const usdcBalanceBefore = await usdc.balanceOf(leverageUserAddress); + const uBalanceBefore = await u.balanceOf(leverageUserAddress); - // Approve the leverage manager to spend user's USDC for seed collateral - await usdc.connect(leverageTestUser).approve(LEVERAGE_STRATEGIES_MANAGER, collateralAmountSeed); + // Approve the leverage manager to spend user's U for seed collateral + await u.connect(leverageTestUser).approve(LEVERAGE_STRATEGIES_MANAGER, collateralAmountSeed); try { // Call enterLeverage on the manager contract // This will: - // - Supply USDC as collateral + // - Supply U as collateral // - Borrow USDT via flash loan - // - Swap USDT for more USDC (increasing collateral) + // - Swap USDT for more U (increasing collateral) // - Leave the user with a leveraged position const tx = await leverageStrategiesManager.connect(leverageTestUser).enterLeverage( - vUSDC, // collateralMarket + vU, // collateralMarket collateralAmountSeed, // collateralAmountSeed vUSDT, // borrowedMarket borrowedAmountToFlashLoan, // borrowedAmountToFlashLoan minAmountOut, // minAmountOutAfterSwap - swapData, // swapData from Venus API + swapData, // swapData ); const receipt = await tx.wait(); @@ -498,27 +486,27 @@ forking(FORK_BLOCK, async () => { // Verify event parameters match input expect(event.args.user.toLowerCase()).to.equal(leverageUserAddress.toLowerCase()); - expect(event.args.collateralMarket.toLowerCase()).to.equal(vUSDC.toLowerCase()); + expect(event.args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); expect(event.args.collateralAmountSeed).to.equal(collateralAmountSeed); expect(event.args.borrowedMarket.toLowerCase()).to.equal(vUSDT.toLowerCase()); expect(event.args.borrowedAmountToFlashLoan).to.equal(borrowedAmountToFlashLoan); // Record balances after leverage - const vUSDCBalanceAfter = await vUSDCContract.balanceOf(leverageUserAddress); + const vUBalanceAfter = await vUContract.balanceOf(leverageUserAddress); const usdtBorrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(leverageUserAddress); - const usdcBalanceAfter = await usdc.balanceOf(leverageUserAddress); + const uBalanceAfter = await u.balanceOf(leverageUserAddress); - // The user's vUSDC balance should increase (more collateral) - expect(vUSDCBalanceAfter).to.be.gt(vUSDCBalanceBefore); + // The user's vU balance should increase (more collateral) + expect(vUBalanceAfter).to.be.gt(vUBalanceBefore); // The user's USDT borrow balance should increase (from flash loan) expect(usdtBorrowAfter).to.be.gt(usdtBorrowBefore); expect(usdtBorrowAfter).to.be.gte(borrowedAmountToFlashLoan); - // The user's USDC wallet balance should decrease by the seed amount - expect(usdcBalanceAfter).to.equal(usdcBalanceBefore.sub(collateralAmountSeed)); + // The user's U wallet balance should decrease by the seed amount + expect(uBalanceAfter).to.equal(uBalanceBefore.sub(collateralAmountSeed)); // Log the results for manual inspection console.log(`Cross-asset leverage entered:`); - console.log(` vUSDC balance: ${vUSDCBalanceBefore.toString()} -> ${vUSDCBalanceAfter.toString()}`); + console.log(` vU balance: ${vUBalanceBefore.toString()} -> ${vUBalanceAfter.toString()}`); console.log(` USDT borrow: ${usdtBorrowBefore.toString()} -> ${usdtBorrowAfter.toString()}`); } catch (error: unknown) { // TokenSwapCallFailed (0x428c0cc7) or similar swap errors - skip gracefully @@ -543,21 +531,21 @@ forking(FORK_BLOCK, async () => { return; } - // For exitLeverage: flash loan USDT to repay, redeem USDC, swap USDC to USDT + // For exitLeverage: flash loan USDT to repay, redeem U, swap U to USDT // Redeem more than borrow to cover swap slippage and flash loan fees const collateralAmountToRedeem = parseUnits("55", 18); - // Get swap data from Venus API (USDC -> USDT) - const { swapData, minAmountOut } = await getSwapData(USDC, USDT, collateralAmountToRedeem.toString(), "0.01"); + // Get swap data (U -> USDT) + const { swapData, minAmountOut } = await getSwapData(U, USDT, collateralAmountToRedeem.toString(), "0.01"); if (swapData === "0x") { - console.log("Skipping exitLeverage test - Venus API unavailable"); + console.log("Skipping exitLeverage test - swap data unavailable"); return; } // Get balances before exit - const vUSDCBalanceBefore = await vUSDCContract.balanceOf(leverageUserAddress); - const usdcBalanceBefore = await usdc.balanceOf(leverageUserAddress); + const vUBalanceBefore = await vUContract.balanceOf(leverageUserAddress); + const uBalanceBefore = await u.balanceOf(leverageUserAddress); // Flash loan amount: borrow balance + 1% buffer for interest const flashLoanAmount = usdtBorrowBefore.mul(101).div(100); @@ -565,12 +553,12 @@ forking(FORK_BLOCK, async () => { try { // Call exitLeverage const tx = await leverageStrategiesManager.connect(leverageTestUser).exitLeverage( - vUSDC, // collateralMarket + vU, // collateralMarket collateralAmountToRedeem, // collateralAmountToRedeemForSwap vUSDT, // borrowedMarket flashLoanAmount, // borrowedAmountToFlashLoan minAmountOut, // minAmountOutAfterSwap - swapData, // swapData from Venus API + swapData, // swapData ); const receipt = await tx.wait(); @@ -591,22 +579,22 @@ forking(FORK_BLOCK, async () => { // Verify event parameters expect(event.args.user.toLowerCase()).to.equal(leverageUserAddress.toLowerCase()); - expect(event.args.collateralMarket.toLowerCase()).to.equal(vUSDC.toLowerCase()); + expect(event.args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); expect(event.args.collateralAmountToRedeemForSwap).to.equal(collateralAmountToRedeem); expect(event.args.borrowedMarket.toLowerCase()).to.equal(vUSDT.toLowerCase()); expect(event.args.borrowedAmountToFlashLoan).to.equal(flashLoanAmount); // Get balances after exit - const vUSDCBalanceAfter = await vUSDCContract.balanceOf(leverageUserAddress); + const vUBalanceAfter = await vUContract.balanceOf(leverageUserAddress); const usdtBorrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(leverageUserAddress); - const usdcBalanceAfter = await usdc.balanceOf(leverageUserAddress); + const uBalanceAfter = await u.balanceOf(leverageUserAddress); - expect(vUSDCBalanceAfter).to.be.lt(vUSDCBalanceBefore); + expect(vUBalanceAfter).to.be.lt(vUBalanceBefore); expect(usdtBorrowAfter).to.equal(0); - expect(usdcBalanceAfter).to.be.gte(usdcBalanceBefore); + expect(uBalanceAfter).to.be.gte(uBalanceBefore); console.log(`Cross-asset leverage exited:`); - console.log(` vUSDC balance: ${vUSDCBalanceBefore.toString()} -> ${vUSDCBalanceAfter.toString()}`); + console.log(` vU balance: ${vUBalanceBefore.toString()} -> ${vUBalanceAfter.toString()}`); console.log(` USDT borrow: ${usdtBorrowBefore.toString()} -> ${usdtBorrowAfter.toString()}`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -623,19 +611,19 @@ forking(FORK_BLOCK, async () => { it("should enter leverage from existing borrow position with value checks", async () => { const userAddress = await leverageTestUser.getAddress(); - // Setup: mint vUSDC and borrow some USDT first + // Setup: mint vU and borrow some USDT first const mintAmount = parseUnits("200", 18); const initialBorrow = parseUnits("20", 18); - const usdcBalance = await usdc.balanceOf(userAddress); - if (usdcBalance.lt(mintAmount)) { - console.log("Skipping enterLeverageFromBorrow test - insufficient USDC balance"); + const uBalance = await u.balanceOf(userAddress); + if (uBalance.lt(mintAmount)) { + console.log("Skipping enterLeverageFromBorrow test - insufficient U balance"); return; } - // Approve and mint vUSDC - await usdc.connect(leverageTestUser).approve(vUSDC, mintAmount); - await vUSDCContract.connect(leverageTestUser).mint(mintAmount); + // Approve and mint vU + await u.connect(leverageTestUser).approve(vU, mintAmount); + await vUContract.connect(leverageTestUser).mint(mintAmount); // Borrow some USDT to create initial position await vUSDTContract.connect(leverageTestUser).borrow(initialBorrow); @@ -644,23 +632,23 @@ forking(FORK_BLOCK, async () => { const additionalBorrowSeed = parseUnits("10", 18); const additionalFlashLoan = parseUnits("20", 18); - // Get swap data (USDT -> USDC) + // Get swap data (USDT -> U) const totalUSDT = additionalBorrowSeed.add(additionalFlashLoan); - const { swapData, minAmountOut } = await getSwapData(USDT, USDC, totalUSDT.toString(), "0.01"); + const { swapData, minAmountOut } = await getSwapData(USDT, U, totalUSDT.toString(), "0.01"); if (swapData === "0x") { - console.log("Skipping enterLeverageFromBorrow test - Venus API unavailable"); + console.log("Skipping enterLeverageFromBorrow test - swap data unavailable"); return; } // Get balances before - const vUSDCBalanceBefore = await vUSDCContract.balanceOf(userAddress); + const vUBalanceBefore = await vUContract.balanceOf(userAddress); const usdtBorrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(userAddress); try { // Call enterLeverageFromBorrow const tx = await leverageStrategiesManager.connect(leverageTestUser).enterLeverageFromBorrow( - vUSDC, // collateralMarket + vU, // collateralMarket vUSDT, // borrowedMarket additionalBorrowSeed, // borrowedAmountSeed (additional borrow, not flash loaned) additionalFlashLoan, // borrowedAmountToFlashLoan @@ -686,20 +674,20 @@ forking(FORK_BLOCK, async () => { // Verify event parameters expect(event.args.user.toLowerCase()).to.equal(userAddress.toLowerCase()); - expect(event.args.collateralMarket.toLowerCase()).to.equal(vUSDC.toLowerCase()); + expect(event.args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); expect(event.args.borrowedMarket.toLowerCase()).to.equal(vUSDT.toLowerCase()); expect(event.args.borrowedAmountSeed).to.equal(additionalBorrowSeed); expect(event.args.borrowedAmountToFlashLoan).to.equal(additionalFlashLoan); // Get balances after - const vUSDCBalanceAfter = await vUSDCContract.balanceOf(userAddress); + const vUBalanceAfter = await vUContract.balanceOf(userAddress); const usdtBorrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(userAddress); - expect(vUSDCBalanceAfter).to.be.gt(vUSDCBalanceBefore); + expect(vUBalanceAfter).to.be.gt(vUBalanceBefore); expect(usdtBorrowAfter).to.be.gt(usdtBorrowBefore); console.log(`Leverage from borrow entered:`); - console.log(` vUSDC balance: ${vUSDCBalanceBefore.toString()} -> ${vUSDCBalanceAfter.toString()}`); + console.log(` vU balance: ${vUBalanceBefore.toString()} -> ${vUBalanceAfter.toString()}`); console.log(` USDT borrow: ${usdtBorrowBefore.toString()} -> ${usdtBorrowAfter.toString()}`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); From b4d5f0d172b820b85b83b6b8cbd005c169d70341 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 27 Jan 2026 23:51:26 +0530 Subject: [PATCH 7/7] refactor: optimize vip-581 and vip-600 simulations with API-first swap strategy and parallel execution - Add Venus swap API integration (20s timeout, single retry on quick failures) with PancakeSwap V2 fallback for swap calldata building - Parallelize independent RPC calls using Promise.all for balance reads, signer initialization, and view function queries - Fix nonce conflicts by keeping same-signer transactions sequential (oracle configs, revert assertions, token transfers) - Add results tracker with summary table printed after all tests - Extract reusable parseEvents helper to reduce event parsing boilerplate - Fix enterLeverageFromBorrow: add missing USDT approval for LeverageStrategiesManager before entering leverage from borrow - Remove verbose JSDoc comments in favor of self-documenting code --- simulations/vip-581/addendum2.ts | 812 ++++++++++++++++-------------- simulations/vip-600/bscmainnet.ts | 620 +++++++++++++---------- 2 files changed, 783 insertions(+), 649 deletions(-) diff --git a/simulations/vip-581/addendum2.ts b/simulations/vip-581/addendum2.ts index 4caa1133f..5da33a296 100644 --- a/simulations/vip-581/addendum2.ts +++ b/simulations/vip-581/addendum2.ts @@ -30,33 +30,31 @@ import CHAINLINK_ORACLE_ABI from "./abi/chainlinkOracle.json"; const { bscmainnet } = NETWORK_ADDRESSES; -// Contract addresses +// ============================================================================= +// Constants +// ============================================================================= + const LEVERAGE_STRATEGIES_MANAGER = "0x03F079E809185a669Ca188676D0ADb09cbAd6dC1"; const SWAP_HELPER = "0xD79be25aEe798Aa34A9Ba1230003d7499be29A24"; -// Core Pool markets const vUSDT = "0xfD5840Cd36d94D7229439859C0112a4185BC0255"; const USDC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"; const USDT = "0x55d398326f99059fF775485246999027B3197955"; const WBNB = "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"; -// PancakeSwap V2 Router on BSC const PANCAKE_V2_ROUTER = "0x10ED43C718714eb63d5aA57B78B54704E256024E"; const PANCAKE_V2_ROUTER_ABI = [ "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)", "function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)", ]; -// User with U tokens to fund test user const U_HOLDER = "0x8894E0a0c962CB723c1976a4421c95949bE2D4E3"; -// User with USDT tokens const USDT_HOLDER = "0xF977814e90dA44bFA03b6295A0616a897441aceC"; -// Fork block number - UPDATE THIS when tests fail due to stale oracle data const FORK_BLOCK = 75075100; // ============================================================================= -// EIP-712 Swap Signer & Calldata Builder +// EIP-712 Swap Signer // ============================================================================= let swapSignerWallet: Wallet; @@ -64,15 +62,6 @@ let swapHelperContract: Contract; let eip712Domain: { name: string; version: string; chainId: number; verifyingContract: string }; let saltCounter = 0; -/** - * Configures a deterministic EIP-712 signer for the SwapHelper contract on the forked chain. - * - * In production, the SwapHelper requires a backend signer to authorize multicall executions - * via EIP-712 signatures. In tests, we: - * 1. Create a deterministic Hardhat wallet (using Hardhat's default private key) - * 2. Impersonate the SwapHelper owner to register this wallet as the authorized backendSigner - * 3. Cache the EIP-712 domain parameters (name, version, chainId, verifyingContract) for later signing - */ async function setupSwapSigner() { swapSignerWallet = new Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); @@ -82,8 +71,7 @@ async function setupSwapSigner() { const impersonatedOwner = await initMainnetUser(swapHelperOwner, ethers.utils.parseEther("1")); await swapHelperContract.connect(impersonatedOwner).setBackendSigner(swapSignerWallet.address); - const domain = await swapHelperContract.eip712Domain(); - const network = await ethers.provider.getNetwork(); + const [domain, network] = await Promise.all([swapHelperContract.eip712Domain(), ethers.provider.getNetwork()]); eip712Domain = { name: domain.name, version: domain.version, @@ -92,15 +80,12 @@ async function setupSwapSigner() { }; } +// ============================================================================= +// Swap Calldata Builders (API-first with PancakeSwap V2 fallback) +// ============================================================================= + /** - * Builds the full signed SwapHelper multicall calldata for a PancakeSwap V2 token swap. - * - * Steps: - * 1. Queries PancakeSwap V2 for the expected output amount - * 2. Applies slippage tolerance to compute minAmountOut - * 3. Encodes a 3-step SwapHelper multicall (approveMax, genericCall, sweep) - * 4. Signs the multicall with EIP-712 - * 5. Returns the encoded multicall calldata + * Tries the Venus swap API first (20s timeout), falls back to PancakeSwap V2 on-chain. */ async function buildSwapCalldata( tokenIn: string, @@ -108,6 +93,86 @@ async function buildSwapCalldata( amountIn: BigNumber, recipient: string, slippageBps: number = 100, +): Promise<{ swapData: string; minAmountOut: BigNumber }> { + try { + return await buildSwapCalldataFromAPI(tokenIn, tokenOut, amountIn, recipient); + } catch (apiError) { + console.log( + ` Swap API unavailable (${ + apiError instanceof Error ? apiError.message : apiError + }), falling back to PancakeSwap V2`, + ); + } + return buildSwapCalldataFromPancakeV2(tokenIn, tokenOut, amountIn, recipient, slippageBps); +} + +async function buildSwapCalldataFromAPI( + tokenIn: string, + tokenOut: string, + amountIn: BigNumber, + recipient: string, +): Promise<{ swapData: string; minAmountOut: BigNumber }> { + const TEN_YEARS_SECS = 10 * 365 * 24 * 60 * 60; + const deadline = Math.floor(Date.now() / 1000) + TEN_YEARS_SECS; + + const params = new URLSearchParams({ + chainId: "56", + tokenInAddress: tokenIn, + tokenOutAddress: tokenOut, + slippagePercentage: "0.5", + recipientAddress: SWAP_HELPER, + deadlineTimestampSecs: deadline.toString(), + type: "exact-in", + shouldTransferToReceiver: "false", + exactAmountInMantissa: amountIn.toString(), + }); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 20000); + + try { + const res = await fetch(`https://api.venus.io/find-swap?${params}`, { signal: controller.signal }); + if (!res.ok) throw new Error(`Swap API error: ${res.status}`); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = (await res.json()) as any; + if (!json.quotes?.length) throw new Error(`No API route found for ${tokenIn} -> ${tokenOut}`); + + const quote = json.quotes[0]; + const swapHelperIface = new ethers.utils.Interface(SWAP_HELPER_ABI); + const calls: string[] = []; + + for (const tx of quote.txs) { + calls.push(swapHelperIface.encodeFunctionData("approveMax", [tokenIn, tx.target])); + calls.push(swapHelperIface.encodeFunctionData("genericCall", [tx.target, tx.data])); + } + calls.push(swapHelperIface.encodeFunctionData("sweep", [tokenOut, recipient])); + + const salt = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["uint256"], [++saltCounter])); + const types = { + Multicall: [ + { name: "caller", type: "address" }, + { name: "calls", type: "bytes[]" }, + { name: "deadline", type: "uint256" }, + { name: "salt", type: "bytes32" }, + ], + }; + const value = { caller: LEVERAGE_STRATEGIES_MANAGER, calls, deadline, salt }; + const signature = await swapSignerWallet._signTypedData(eip712Domain, types, value); + const multicallData = swapHelperIface.encodeFunctionData("multicall", [calls, deadline, salt, signature]); + + return { swapData: multicallData, minAmountOut: BigNumber.from(1) }; + } finally { + clearTimeout(timeoutId); + } +} + +async function buildSwapCalldataFromPancakeV2( + tokenIn: string, + tokenOut: string, + amountIn: BigNumber, + recipient: string, + slippageBps: number, ): Promise<{ swapData: string; minAmountOut: BigNumber }> { const pancakeRouter = new ethers.Contract(PANCAKE_V2_ROUTER, PANCAKE_V2_ROUTER_ABI, ethers.provider); @@ -130,30 +195,25 @@ async function buildSwapCalldata( const minAmountOut = amountOut.mul(10000 - slippageBps).div(10000); const swapHelperIface = new ethers.utils.Interface(SWAP_HELPER_ABI); - - // 1. approveMax(tokenIn, PancakeRouter) - const approveCall = swapHelperIface.encodeFunctionData("approveMax", [tokenIn, PANCAKE_V2_ROUTER]); - - // 2. genericCall(PancakeRouter, swapExactTokensForTokens(...)) const pancakeIface = new ethers.utils.Interface(PANCAKE_V2_ROUTER_ABI); const deadline = Math.floor(Date.now() / 1000) + 3600; - const swapCalldata = pancakeIface.encodeFunctionData("swapExactTokensForTokens", [ - amountIn, - minAmountOut, - path, - SWAP_HELPER, // tokens go to SwapHelper first - deadline, - ]); - const genericCall = swapHelperIface.encodeFunctionData("genericCall", [PANCAKE_V2_ROUTER, swapCalldata]); - // 3. sweep(tokenOut, recipient) — send swapped tokens to LeverageStrategiesManager - const sweepCall = swapHelperIface.encodeFunctionData("sweep", [tokenOut, recipient]); + const calls = [ + swapHelperIface.encodeFunctionData("approveMax", [tokenIn, PANCAKE_V2_ROUTER]), + swapHelperIface.encodeFunctionData("genericCall", [ + PANCAKE_V2_ROUTER, + pancakeIface.encodeFunctionData("swapExactTokensForTokens", [ + amountIn, + minAmountOut, + path, + SWAP_HELPER, + deadline, + ]), + ]), + swapHelperIface.encodeFunctionData("sweep", [tokenOut, recipient]), + ]; - const calls = [approveCall, genericCall, sweepCall]; - - // EIP-712 sign — caller is LeverageStrategiesManager (it calls SwapHelper.multicall) const salt = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["uint256"], [++saltCounter])); - const types = { Multicall: [ { name: "caller", type: "address" }, @@ -162,24 +222,17 @@ async function buildSwapCalldata( { name: "salt", type: "bytes32" }, ], }; - - const value = { - caller: LEVERAGE_STRATEGIES_MANAGER, - calls, - deadline, - salt, - }; - + const value = { caller: LEVERAGE_STRATEGIES_MANAGER, calls, deadline, salt }; const signature = await swapSignerWallet._signTypedData(eip712Domain, types, value); - const multicallData = swapHelperIface.encodeFunctionData("multicall", [calls, deadline, salt, signature]); return { swapData: multicallData, minAmountOut }; } -/** - * High-level wrapper matching the interface used by test cases. - */ +// ============================================================================= +// Swap Data Wrapper +// ============================================================================= + async function getSwapData( tokenInAddress: string, tokenOutAddress: string, @@ -196,6 +249,70 @@ async function getSwapData( ); } +// ============================================================================= +// Results Tracker +// ============================================================================= + +interface LeverageResult { + section: string; + test: string; + status: "PASSED" | "SKIPPED" | "FAILED"; + detail: string; +} +const leverageResults: LeverageResult[] = []; + +function printLeverageResultsSummary() { + const passed = leverageResults.filter(r => r.status === "PASSED"); + const failed = leverageResults.filter(r => r.status === "FAILED"); + const skipped = leverageResults.filter(r => r.status === "SKIPPED"); + + console.log("\n" + "=".repeat(120)); + console.log(" LEVERAGE STRATEGIES RESULTS SUMMARY"); + console.log("=".repeat(120)); + + for (const [label, list] of [ + ["PASSED", passed], + ["FAILED", failed], + ["SKIPPED", skipped], + ] as const) { + if (list.length === 0) continue; + console.log(`\n ${label} (${list.length})`); + console.log(" " + "-".repeat(116)); + console.log(` ${"Section".padEnd(35)} ${"Test".padEnd(50)} ${label === "PASSED" ? "Detail" : "Reason"}`); + console.log(" " + "-".repeat(116)); + for (const r of list) { + console.log(` ${r.section.padEnd(35)} ${r.test.padEnd(50)} ${r.detail}`); + } + } + + console.log("\n" + "=".repeat(120)); + console.log( + ` Total: ${leverageResults.length} | Passed: ${passed.length} | Failed: ${failed.length} | Skipped: ${skipped.length}`, + ); + console.log("=".repeat(120) + "\n"); +} + +// ============================================================================= +// Helpers +// ============================================================================= + +function parseEvents(receipt: any, abi: any, eventName: string): any[] { + const iface = new ethers.utils.Interface(abi); + return receipt.logs + .map((log: { topics: string[]; data: string }) => { + try { + return iface.parseLog(log); + } catch { + return null; + } + }) + .filter((e: { name: string } | null) => e && e.name === eventName); +} + +// ============================================================================= +// Test Suite +// ============================================================================= + forking(FORK_BLOCK, async () => { let vUContract: Contract; let leverageStrategiesManager: Contract; @@ -211,6 +328,7 @@ forking(FORK_BLOCK, async () => { let leverageTestUser: Signer; before(async () => { + // Instantiate contracts (pure local — no RPC) vUContract = new ethers.Contract(vU, VTOKEN_ABI, ethers.provider); leverageStrategiesManager = new ethers.Contract( LEVERAGE_STRATEGIES_MANAGER, @@ -224,25 +342,36 @@ forking(FORK_BLOCK, async () => { resilientOracle = new ethers.Contract(RESILIENT_ORACLE, RESILIENT_ORACLE_ABI, ethers.provider); chainlinkOracle = new ethers.Contract(CHAINLINK_ORACLE, CHAINLINK_ORACLE_ABI, ethers.provider); usdtChainlinkOracle = new ethers.Contract(USDT_CHAINLINK_ORACLE, CHAINLINK_ORACLE_ABI, ethers.provider); - impersonatedTimelock = await initMainnetUser(bscmainnet.NORMAL_TIMELOCK, ethers.utils.parseEther("2")); - // Use fresh Hardhat signers as test users to avoid oracle issues with other markets const signers = await ethers.getSigners(); testUser = signers[0]; leverageTestUser = signers[1]; - // Fund test users with tokens - const uHolder = await initMainnetUser(U_HOLDER, ethers.utils.parseEther("10")); - const usdtHolder = await initMainnetUser(USDT_HOLDER, ethers.utils.parseEther("10")); - - await u.connect(uHolder).transfer(await testUser.getAddress(), parseUnits("100", 18)); - await u.connect(uHolder).transfer(await leverageTestUser.getAddress(), parseUnits("1000", 18)); - await usdt.connect(usdtHolder).transfer(await leverageTestUser.getAddress(), parseUnits("1000", 18)); - - // Set direct prices and extend stale periods for oracle compatibility + // Step 1: Impersonate users and setup swap signer (different signers — safe in parallel) + const [, uHolder, usdtHolder, timelockSigner] = await Promise.all([ + setupSwapSigner(), + initMainnetUser(U_HOLDER, ethers.utils.parseEther("10")), + initMainnetUser(USDT_HOLDER, ethers.utils.parseEther("10")), + initMainnetUser(bscmainnet.NORMAL_TIMELOCK, ethers.utils.parseEther("2")), + ]); + impersonatedTimelock = timelockSigner; + + // Step 2: Fund test users + const [testUserAddress, leverageUserAddress] = await Promise.all([ + testUser.getAddress(), + leverageTestUser.getAddress(), + ]); + // uHolder sends two transfers — must be sequential (same signer, nonce conflict) + await u.connect(uHolder).transfer(testUserAddress, parseUnits("100", 18)); + await u.connect(uHolder).transfer(leverageUserAddress, parseUnits("1000", 18)); + // usdtHolder is a different signer but has no parallel partner now, so just await + await usdt.connect(usdtHolder).transfer(leverageUserAddress, parseUnits("1000", 18)); + + // Step 3: Set direct prices (same signer: impersonatedTimelock — must be sequential) await usdtChainlinkOracle.connect(impersonatedTimelock).setDirectPrice(U, parseUnits("1", 18)); await chainlinkOracle.connect(impersonatedTimelock).setDirectPrice(U, parseUnits("1", 18)); + // Step 4: Configure oracle stale periods (uses NORMAL_TIMELOCK internally — sequential) await setMaxStalePeriodInChainlinkOracle( USDT_CHAINLINK_ORACLE, U, @@ -250,12 +379,8 @@ forking(FORK_BLOCK, async () => { bscmainnet.NORMAL_TIMELOCK, 315360000, ); - await setMaxStalePeriodInChainlinkOracle(CHAINLINK_ORACLE, U, USD1_FEED, bscmainnet.NORMAL_TIMELOCK, 315360000); - await setMaxStalePeriod(resilientOracle, u); - - // Set stale periods for USDC and USDT for cross-asset leverage tests await setMaxStalePeriodInChainlinkOracle( bscmainnet.CHAINLINK_ORACLE, USDC, @@ -272,247 +397,161 @@ forking(FORK_BLOCK, async () => { ); await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, "USDC", 315360000); await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, "USDT", 315360000); - - // Setup the EIP-712 swap signer so we can build signed multicall calldata - await setupSwapSigner(); }); - /** - * VIP Execution Test - * - * Note: At recent blocks, this VIP may have already been executed on mainnet. - * We check for 0 or 1 FlashLoanStatusChanged events to handle both cases: - * - 0 events: VIP was already executed (no state change) - * - 1 event: VIP just executed (state changed from false to true) - */ + // =========================================================================== + // VIP Execution + // =========================================================================== + testVip("VIP-581 Addendum2", await vip581Addendum2(), { callbackAfterExecution: async txResponse => { const receipt = await txResponse.wait(); - const iface = new ethers.utils.Interface(VTOKEN_ABI); - const events = receipt.logs - .map((log: { topics: string[]; data: string }) => { - try { - return iface.parseLog(log); - } catch { - return null; - } - }) - .filter((e: { name: string } | null) => e && e.name === "FlashLoanStatusChanged"); - // Either 0 (already executed) or 1 (just executed) events is acceptable + const events = parseEvents(receipt, VTOKEN_ABI, "FlashLoanStatusChanged"); expect(events.length).to.be.lte(1); }, }); + // =========================================================================== + // Post-VIP checks + // =========================================================================== + describe("Post-VIP checks", () => { it("vU has flash loans enabled", async () => { - const isFlashLoanEnabled = await vUContract.isFlashLoanEnabled(); - expect(isFlashLoanEnabled).to.be.equal(true); + expect(await vUContract.isFlashLoanEnabled()).to.be.equal(true); + + leverageResults.push({ + section: "Post-VIP", + test: "Flash loans enabled on vU", + status: "PASSED", + detail: "isFlashLoanEnabled = true", + }); }); - it("LeverageStrategiesManager: enterSingleAssetLeverage with value checks", async () => { + it("enterSingleAssetLeverage with value checks", async () => { const userAddress = await testUser.getAddress(); const collateralAmountSeed = parseUnits("10", 18); const collateralAmountToFlashLoan = parseUnits("5", 18); - // Approve U tokens to vU for minting collateral first await u.connect(testUser).approve(vU, collateralAmountSeed); - - // Mint vU as collateral await vUContract.connect(testUser).mint(collateralAmountSeed); - - // Enter market so vU can be used as collateral await comptroller.connect(testUser).enterMarkets([vU]); - - // Approve leverage manager to act on behalf of user (updateDelegate) await comptroller.connect(testUser).updateDelegate(LEVERAGE_STRATEGIES_MANAGER, true); - // Get balances before - const vUBalanceBefore = await vUContract.balanceOf(userAddress); - const borrowBalanceBefore = await vUContract.callStatic.borrowBalanceCurrent(userAddress); + const [vUBalanceBefore, borrowBalanceBefore] = await Promise.all([ + vUContract.balanceOf(userAddress), + vUContract.callStatic.borrowBalanceCurrent(userAddress), + ]); - // Call enterSingleAssetLeverage const tx = await leverageStrategiesManager .connect(testUser) .enterSingleAssetLeverage(vU, 0, collateralAmountToFlashLoan); const receipt = await tx.wait(); - // Parse and verify SingleAssetLeverageEntered event - const iface = new ethers.utils.Interface(LEVERAGE_STRATEGIES_MANAGER_ABI); - const leverageEvents = receipt.logs - .map((log: { topics: string[]; data: string }) => { - try { - return iface.parseLog(log); - } catch { - return null; - } - }) - .filter((e: { name: string } | null) => e && e.name === "SingleAssetLeverageEntered"); - + const leverageEvents = parseEvents(receipt, LEVERAGE_STRATEGIES_MANAGER_ABI, "SingleAssetLeverageEntered"); expect(leverageEvents.length).to.equal(1); - const event = leverageEvents[0]; - - // Verify event parameters - expect(event.args.user.toLowerCase()).to.equal(userAddress.toLowerCase()); - expect(event.args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); - expect(event.args.collateralAmountSeed).to.equal(0); - expect(event.args.collateralAmountToFlashLoan).to.equal(collateralAmountToFlashLoan); + expect(leverageEvents[0].args.user.toLowerCase()).to.equal(userAddress.toLowerCase()); + expect(leverageEvents[0].args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); + expect(leverageEvents[0].args.collateralAmountSeed).to.equal(0); + expect(leverageEvents[0].args.collateralAmountToFlashLoan).to.equal(collateralAmountToFlashLoan); - // Verify balance changes - const vUBalanceAfter = await vUContract.balanceOf(userAddress); - const borrowBalanceAfter = await vUContract.callStatic.borrowBalanceCurrent(userAddress); + const [vUBalanceAfter, borrowBalanceAfter] = await Promise.all([ + vUContract.balanceOf(userAddress), + vUContract.callStatic.borrowBalanceCurrent(userAddress), + ]); - // User should have more vU (from flash loan collateral added) expect(vUBalanceAfter).to.be.gt(vUBalanceBefore); - - // User should now have a borrow balance equal to or greater than flash loan amount expect(borrowBalanceAfter).to.be.gt(borrowBalanceBefore); expect(borrowBalanceAfter).to.be.gte(collateralAmountToFlashLoan); - console.log(`SingleAssetLeverage entered:`); - console.log(` vU balance: ${vUBalanceBefore.toString()} -> ${vUBalanceAfter.toString()}`); - console.log(` Borrow: ${borrowBalanceBefore.toString()} -> ${borrowBalanceAfter.toString()}`); + leverageResults.push({ + section: "Post-VIP", + test: "enterSingleAssetLeverage", + status: "PASSED", + detail: `vU: ${vUBalanceBefore} -> ${vUBalanceAfter}, Borrow: ${borrowBalanceBefore} -> ${borrowBalanceAfter}`, + }); }); }); - /** - * Cross-asset leverage tests (enterLeverage, exitLeverage, enterLeverageFromBorrow) - * - * These tests require live swap quotes from the Venus API (api.venus.io/find-swap). - * The swap data includes a signed quote with a deadline and specific amounts. - * - * IMPORTANT: These tests may fail due to: - * 1. Swap quote expiration (deadline passed by the time tx executes) - * 2. Price movement causing slippage to exceed limits - * 3. DEX liquidity changes affecting swap routes - * 4. API unavailability - * - * The tests gracefully skip if API is unavailable or swap fails. - * For reliable testing of cross-asset leverage, use Tenderly virtual testnet - * with fresh swap quotes: https://api.venus.io/find-swap - */ - /** - * ===================================================================== - * LeverageStrategiesManager: enterLeverage (cross-asset) - * ===================================================================== - * - * This test demonstrates a cross-asset leverage operation using the - * LeverageStrategiesManager contract. The user supplies U as collateral, - * borrows USDT via a flash loan, and swaps the borrowed USDT for more U - * to increase their leveraged position. - * - * Key steps: - * 1. User enters both vU and vUSDT markets and delegates to the manager. - * 2. User approves the manager to spend their U. - * 3. The test fetches swap data for USDT->U. - * 4. The manager's enterLeverage is called, which: - * - Supplies U as seed collateral - * - Borrows USDT via flash loan - * - Swaps USDT for more U (increasing collateral) - * - Leaves the user with a leveraged position - * 5. The test verifies: - * - The LeverageEntered event is emitted with correct parameters - * - The user's vU balance increases - * - The user's USDT borrow balance increases - * - The user's U wallet balance decreases by the seed amount - */ - describe("LeverageStrategiesManager: enterLeverage (cross-asset)", () => { + // =========================================================================== + // Cross-Asset Leverage: enterLeverage + // =========================================================================== + + describe("enterLeverage (cross-asset)", () => { let leverageUserAddress: string; before(async () => { leverageUserAddress = await leverageTestUser.getAddress(); - - // Enter markets for both vU and vUSDT so the user can supply collateral and borrow + // Sequential — same signer (leverageTestUser) await comptroller.connect(leverageTestUser).enterMarkets([vU, vUSDT]); - - // Approve leverage manager to act on behalf of user (required for leverage operations) await comptroller.connect(leverageTestUser).updateDelegate(LEVERAGE_STRATEGIES_MANAGER, true); }); - it("should enter cross-asset leverage position (U collateral, USDT borrow) with value checks", async () => { - // User will supply 100 U as seed collateral + it("should enter cross-asset leverage (U collateral, USDT borrow)", async () => { const collateralAmountSeed = parseUnits("100", 18); - // User will borrow 50 USDT via flash loan const borrowedAmountToFlashLoan = parseUnits("50", 18); - // Fetch swap data for swapping borrowed USDT to U const { swapData, minAmountOut } = await getSwapData(USDT, U, borrowedAmountToFlashLoan.toString(), "0.01"); - // If swap data is unavailable, skip the test if (swapData === "0x") { - console.log("Skipping cross-asset enterLeverage test - swap data unavailable"); + leverageResults.push({ + section: "enterLeverage", + test: "U collateral / USDT borrow", + status: "SKIPPED", + detail: "Swap data unavailable", + }); + console.log(" [SKIP] Swap data unavailable"); return; } - // Record balances before leverage - const vUBalanceBefore = await vUContract.balanceOf(leverageUserAddress); - const usdtBorrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(leverageUserAddress); - const uBalanceBefore = await u.balanceOf(leverageUserAddress); + const [vUBalanceBefore, usdtBorrowBefore, uBalanceBefore] = await Promise.all([ + vUContract.balanceOf(leverageUserAddress), + vUSDTContract.callStatic.borrowBalanceCurrent(leverageUserAddress), + u.balanceOf(leverageUserAddress), + ]); - // Approve the leverage manager to spend user's U for seed collateral await u.connect(leverageTestUser).approve(LEVERAGE_STRATEGIES_MANAGER, collateralAmountSeed); try { - // Call enterLeverage on the manager contract - // This will: - // - Supply U as collateral - // - Borrow USDT via flash loan - // - Swap USDT for more U (increasing collateral) - // - Leave the user with a leveraged position - const tx = await leverageStrategiesManager.connect(leverageTestUser).enterLeverage( - vU, // collateralMarket - collateralAmountSeed, // collateralAmountSeed - vUSDT, // borrowedMarket - borrowedAmountToFlashLoan, // borrowedAmountToFlashLoan - minAmountOut, // minAmountOutAfterSwap - swapData, // swapData - ); + const tx = await leverageStrategiesManager + .connect(leverageTestUser) + .enterLeverage(vU, collateralAmountSeed, vUSDT, borrowedAmountToFlashLoan, minAmountOut, swapData); const receipt = await tx.wait(); - // Parse and verify LeverageEntered event - const iface = new ethers.utils.Interface(LEVERAGE_STRATEGIES_MANAGER_ABI); - const leverageEvents = receipt.logs - .map((log: { topics: string[]; data: string }) => { - try { - return iface.parseLog(log); - } catch { - return null; - } - }) - .filter((e: { name: string } | null) => e && e.name === "LeverageEntered"); - - // There should be exactly one LeverageEntered event + const leverageEvents = parseEvents(receipt, LEVERAGE_STRATEGIES_MANAGER_ABI, "LeverageEntered"); expect(leverageEvents.length).to.equal(1); - const event = leverageEvents[0]; - - // Verify event parameters match input - expect(event.args.user.toLowerCase()).to.equal(leverageUserAddress.toLowerCase()); - expect(event.args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); - expect(event.args.collateralAmountSeed).to.equal(collateralAmountSeed); - expect(event.args.borrowedMarket.toLowerCase()).to.equal(vUSDT.toLowerCase()); - expect(event.args.borrowedAmountToFlashLoan).to.equal(borrowedAmountToFlashLoan); - - // Record balances after leverage - const vUBalanceAfter = await vUContract.balanceOf(leverageUserAddress); - const usdtBorrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(leverageUserAddress); - const uBalanceAfter = await u.balanceOf(leverageUserAddress); + expect(leverageEvents[0].args.user.toLowerCase()).to.equal(leverageUserAddress.toLowerCase()); + expect(leverageEvents[0].args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); + expect(leverageEvents[0].args.collateralAmountSeed).to.equal(collateralAmountSeed); + expect(leverageEvents[0].args.borrowedMarket.toLowerCase()).to.equal(vUSDT.toLowerCase()); + expect(leverageEvents[0].args.borrowedAmountToFlashLoan).to.equal(borrowedAmountToFlashLoan); + + const [vUBalanceAfter, usdtBorrowAfter, uBalanceAfter] = await Promise.all([ + vUContract.balanceOf(leverageUserAddress), + vUSDTContract.callStatic.borrowBalanceCurrent(leverageUserAddress), + u.balanceOf(leverageUserAddress), + ]); - // The user's vU balance should increase (more collateral) expect(vUBalanceAfter).to.be.gt(vUBalanceBefore); - // The user's USDT borrow balance should increase (from flash loan) expect(usdtBorrowAfter).to.be.gt(usdtBorrowBefore); expect(usdtBorrowAfter).to.be.gte(borrowedAmountToFlashLoan); - // The user's U wallet balance should decrease by the seed amount expect(uBalanceAfter).to.equal(uBalanceBefore.sub(collateralAmountSeed)); - // Log the results for manual inspection - console.log(`Cross-asset leverage entered:`); - console.log(` vU balance: ${vUBalanceBefore.toString()} -> ${vUBalanceAfter.toString()}`); - console.log(` USDT borrow: ${usdtBorrowBefore.toString()} -> ${usdtBorrowAfter.toString()}`); + leverageResults.push({ + section: "enterLeverage", + test: "U collateral / USDT borrow", + status: "PASSED", + detail: `vU: ${vUBalanceBefore} -> ${vUBalanceAfter}, USDT borrow: ${usdtBorrowBefore} -> ${usdtBorrowAfter}`, + }); } catch (error: unknown) { - // TokenSwapCallFailed (0x428c0cc7) or similar swap errors - skip gracefully - const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes("0x428c0cc7") || errorMessage.includes("TokenSwapCallFailed")) { - console.log("Skipping cross-asset enterLeverage - swap quote expired or route unavailable"); + const msg = error instanceof Error ? error.message : String(error); + if (msg.includes("0x428c0cc7") || msg.includes("TokenSwapCallFailed") || msg.includes("Swap API error")) { + leverageResults.push({ + section: "enterLeverage", + test: "U collateral / USDT borrow", + status: "SKIPPED", + detail: "Swap failed or API unavailable", + }); + console.log(" [SKIP] Swap quote expired or route unavailable"); return; } throw error; @@ -520,86 +559,87 @@ forking(FORK_BLOCK, async () => { }); }); - describe("LeverageStrategiesManager: exitLeverage (cross-asset)", () => { - it("should exit cross-asset leverage position with value checks", async () => { + // =========================================================================== + // Cross-Asset Leverage: exitLeverage + // =========================================================================== + + describe("exitLeverage (cross-asset)", () => { + it("should exit cross-asset leverage position", async () => { const leverageUserAddress = await leverageTestUser.getAddress(); - // Check if user has a leverage position to exit const usdtBorrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(leverageUserAddress); if (usdtBorrowBefore.eq(0)) { - console.log("Skipping exitLeverage test - no position to exit"); + leverageResults.push({ + section: "exitLeverage", + test: "Exit U/USDT position", + status: "SKIPPED", + detail: "No position to exit", + }); + console.log(" [SKIP] No position to exit"); return; } - // For exitLeverage: flash loan USDT to repay, redeem U, swap U to USDT - // Redeem more than borrow to cover swap slippage and flash loan fees const collateralAmountToRedeem = parseUnits("55", 18); - - // Get swap data (U -> USDT) const { swapData, minAmountOut } = await getSwapData(U, USDT, collateralAmountToRedeem.toString(), "0.01"); if (swapData === "0x") { - console.log("Skipping exitLeverage test - swap data unavailable"); + leverageResults.push({ + section: "exitLeverage", + test: "Exit U/USDT position", + status: "SKIPPED", + detail: "Swap data unavailable", + }); + console.log(" [SKIP] Swap data unavailable"); return; } - // Get balances before exit - const vUBalanceBefore = await vUContract.balanceOf(leverageUserAddress); - const uBalanceBefore = await u.balanceOf(leverageUserAddress); + const [vUBalanceBefore, uBalanceBefore] = await Promise.all([ + vUContract.balanceOf(leverageUserAddress), + u.balanceOf(leverageUserAddress), + ]); - // Flash loan amount: borrow balance + 1% buffer for interest const flashLoanAmount = usdtBorrowBefore.mul(101).div(100); try { - // Call exitLeverage - const tx = await leverageStrategiesManager.connect(leverageTestUser).exitLeverage( - vU, // collateralMarket - collateralAmountToRedeem, // collateralAmountToRedeemForSwap - vUSDT, // borrowedMarket - flashLoanAmount, // borrowedAmountToFlashLoan - minAmountOut, // minAmountOutAfterSwap - swapData, // swapData - ); + const tx = await leverageStrategiesManager + .connect(leverageTestUser) + .exitLeverage(vU, collateralAmountToRedeem, vUSDT, flashLoanAmount, minAmountOut, swapData); const receipt = await tx.wait(); - // Parse and verify LeverageExited event - const iface = new ethers.utils.Interface(LEVERAGE_STRATEGIES_MANAGER_ABI); - const exitEvents = receipt.logs - .map((log: { topics: string[]; data: string }) => { - try { - return iface.parseLog(log); - } catch { - return null; - } - }) - .filter((e: { name: string } | null) => e && e.name === "LeverageExited"); - + const exitEvents = parseEvents(receipt, LEVERAGE_STRATEGIES_MANAGER_ABI, "LeverageExited"); expect(exitEvents.length).to.equal(1); - const event = exitEvents[0]; - - // Verify event parameters - expect(event.args.user.toLowerCase()).to.equal(leverageUserAddress.toLowerCase()); - expect(event.args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); - expect(event.args.collateralAmountToRedeemForSwap).to.equal(collateralAmountToRedeem); - expect(event.args.borrowedMarket.toLowerCase()).to.equal(vUSDT.toLowerCase()); - expect(event.args.borrowedAmountToFlashLoan).to.equal(flashLoanAmount); - - // Get balances after exit - const vUBalanceAfter = await vUContract.balanceOf(leverageUserAddress); - const usdtBorrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(leverageUserAddress); - const uBalanceAfter = await u.balanceOf(leverageUserAddress); + expect(exitEvents[0].args.user.toLowerCase()).to.equal(leverageUserAddress.toLowerCase()); + expect(exitEvents[0].args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); + expect(exitEvents[0].args.collateralAmountToRedeemForSwap).to.equal(collateralAmountToRedeem); + expect(exitEvents[0].args.borrowedMarket.toLowerCase()).to.equal(vUSDT.toLowerCase()); + expect(exitEvents[0].args.borrowedAmountToFlashLoan).to.equal(flashLoanAmount); + + const [vUBalanceAfter, usdtBorrowAfter, uBalanceAfter] = await Promise.all([ + vUContract.balanceOf(leverageUserAddress), + vUSDTContract.callStatic.borrowBalanceCurrent(leverageUserAddress), + u.balanceOf(leverageUserAddress), + ]); expect(vUBalanceAfter).to.be.lt(vUBalanceBefore); expect(usdtBorrowAfter).to.equal(0); expect(uBalanceAfter).to.be.gte(uBalanceBefore); - console.log(`Cross-asset leverage exited:`); - console.log(` vU balance: ${vUBalanceBefore.toString()} -> ${vUBalanceAfter.toString()}`); - console.log(` USDT borrow: ${usdtBorrowBefore.toString()} -> ${usdtBorrowAfter.toString()}`); + leverageResults.push({ + section: "exitLeverage", + test: "Exit U/USDT position", + status: "PASSED", + detail: `vU: ${vUBalanceBefore} -> ${vUBalanceAfter}, USDT borrow: ${usdtBorrowBefore} -> ${usdtBorrowAfter}`, + }); } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes("0x428c0cc7") || errorMessage.includes("TokenSwapCallFailed")) { - console.log("Skipping cross-asset exitLeverage - swap quote expired or route unavailable"); + const msg = error instanceof Error ? error.message : String(error); + if (msg.includes("0x428c0cc7") || msg.includes("TokenSwapCallFailed") || msg.includes("Swap API error")) { + leverageResults.push({ + section: "exitLeverage", + test: "Exit U/USDT position", + status: "SKIPPED", + detail: "Swap failed or API unavailable", + }); + console.log(" [SKIP] Swap quote expired or route unavailable"); return; } throw error; @@ -607,96 +647,99 @@ forking(FORK_BLOCK, async () => { }); }); - describe("LeverageStrategiesManager: enterLeverageFromBorrow", () => { - it("should enter leverage from existing borrow position with value checks", async () => { + // =========================================================================== + // Cross-Asset Leverage: enterLeverageFromBorrow + // =========================================================================== + + describe("enterLeverageFromBorrow", () => { + it("should enter leverage from existing borrow position", async () => { const userAddress = await leverageTestUser.getAddress(); - // Setup: mint vU and borrow some USDT first const mintAmount = parseUnits("200", 18); const initialBorrow = parseUnits("20", 18); const uBalance = await u.balanceOf(userAddress); if (uBalance.lt(mintAmount)) { - console.log("Skipping enterLeverageFromBorrow test - insufficient U balance"); + leverageResults.push({ + section: "enterLeverageFromBorrow", + test: "Leverage from existing borrow", + status: "SKIPPED", + detail: "Insufficient U balance", + }); + console.log(" [SKIP] Insufficient U balance"); return; } - // Approve and mint vU + // Sequential — same signer (leverageTestUser) await u.connect(leverageTestUser).approve(vU, mintAmount); await vUContract.connect(leverageTestUser).mint(mintAmount); - - // Borrow some USDT to create initial position await vUSDTContract.connect(leverageTestUser).borrow(initialBorrow); - // Now use enterLeverageFromBorrow to increase leverage const additionalBorrowSeed = parseUnits("10", 18); const additionalFlashLoan = parseUnits("20", 18); - - // Get swap data (USDT -> U) const totalUSDT = additionalBorrowSeed.add(additionalFlashLoan); + const { swapData, minAmountOut } = await getSwapData(USDT, U, totalUSDT.toString(), "0.01"); if (swapData === "0x") { - console.log("Skipping enterLeverageFromBorrow test - swap data unavailable"); + leverageResults.push({ + section: "enterLeverageFromBorrow", + test: "Leverage from existing borrow", + status: "SKIPPED", + detail: "Swap data unavailable", + }); + console.log(" [SKIP] Swap data unavailable"); return; } - // Get balances before - const vUBalanceBefore = await vUContract.balanceOf(userAddress); - const usdtBorrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(userAddress); + const [vUBalanceBefore, usdtBorrowBefore] = await Promise.all([ + vUContract.balanceOf(userAddress), + vUSDTContract.callStatic.borrowBalanceCurrent(userAddress), + ]); try { - // Call enterLeverageFromBorrow - const tx = await leverageStrategiesManager.connect(leverageTestUser).enterLeverageFromBorrow( - vU, // collateralMarket - vUSDT, // borrowedMarket - additionalBorrowSeed, // borrowedAmountSeed (additional borrow, not flash loaned) - additionalFlashLoan, // borrowedAmountToFlashLoan - minAmountOut, // minAmountOutAfterSwap - swapData, // swapData - ); + const tx = await leverageStrategiesManager + .connect(leverageTestUser) + .enterLeverageFromBorrow(vU, vUSDT, additionalBorrowSeed, additionalFlashLoan, minAmountOut, swapData); const receipt = await tx.wait(); - // Parse and verify LeverageEnteredFromBorrow event - const iface = new ethers.utils.Interface(LEVERAGE_STRATEGIES_MANAGER_ABI); - const events = receipt.logs - .map((log: { topics: string[]; data: string }) => { - try { - return iface.parseLog(log); - } catch { - return null; - } - }) - .filter((e: { name: string } | null) => e && e.name === "LeverageEnteredFromBorrow"); - + const events = parseEvents(receipt, LEVERAGE_STRATEGIES_MANAGER_ABI, "LeverageEnteredFromBorrow"); expect(events.length).to.equal(1); - const event = events[0]; + expect(events[0].args.user.toLowerCase()).to.equal(userAddress.toLowerCase()); + expect(events[0].args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); + expect(events[0].args.borrowedMarket.toLowerCase()).to.equal(vUSDT.toLowerCase()); + expect(events[0].args.borrowedAmountSeed).to.equal(additionalBorrowSeed); + expect(events[0].args.borrowedAmountToFlashLoan).to.equal(additionalFlashLoan); - // Verify event parameters - expect(event.args.user.toLowerCase()).to.equal(userAddress.toLowerCase()); - expect(event.args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); - expect(event.args.borrowedMarket.toLowerCase()).to.equal(vUSDT.toLowerCase()); - expect(event.args.borrowedAmountSeed).to.equal(additionalBorrowSeed); - expect(event.args.borrowedAmountToFlashLoan).to.equal(additionalFlashLoan); - - // Get balances after - const vUBalanceAfter = await vUContract.balanceOf(userAddress); - const usdtBorrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(userAddress); + const [vUBalanceAfter, usdtBorrowAfter] = await Promise.all([ + vUContract.balanceOf(userAddress), + vUSDTContract.callStatic.borrowBalanceCurrent(userAddress), + ]); expect(vUBalanceAfter).to.be.gt(vUBalanceBefore); expect(usdtBorrowAfter).to.be.gt(usdtBorrowBefore); - console.log(`Leverage from borrow entered:`); - console.log(` vU balance: ${vUBalanceBefore.toString()} -> ${vUBalanceAfter.toString()}`); - console.log(` USDT borrow: ${usdtBorrowBefore.toString()} -> ${usdtBorrowAfter.toString()}`); + leverageResults.push({ + section: "enterLeverageFromBorrow", + test: "Leverage from existing borrow", + status: "PASSED", + detail: `vU: ${vUBalanceBefore} -> ${vUBalanceAfter}, USDT borrow: ${usdtBorrowBefore} -> ${usdtBorrowAfter}`, + }); } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); + const msg = error instanceof Error ? error.message : String(error); if ( - errorMessage.includes("0x428c0cc7") || - errorMessage.includes("TokenSwapCallFailed") || - errorMessage.includes("transfer amount exceeds allowance") + msg.includes("0x428c0cc7") || + msg.includes("TokenSwapCallFailed") || + msg.includes("transfer amount exceeds allowance") || + msg.includes("Swap API error") ) { - console.log("Skipping enterLeverageFromBorrow - swap quote expired or route unavailable"); + leverageResults.push({ + section: "enterLeverageFromBorrow", + test: "Leverage from existing borrow", + status: "SKIPPED", + detail: "Swap failed or API unavailable", + }); + console.log(" [SKIP] Swap quote expired or route unavailable"); return; } throw error; @@ -704,61 +747,56 @@ forking(FORK_BLOCK, async () => { }); }); - describe("LeverageStrategiesManager: exitSingleAssetLeverage", () => { - it("should exit single asset leverage position with value checks", async () => { + // =========================================================================== + // exitSingleAssetLeverage + // =========================================================================== + + describe("exitSingleAssetLeverage", () => { + it("should exit single asset leverage position", async () => { const userAddress = await testUser.getAddress(); - // Get current borrow balance const borrowBalance = await vUContract.callStatic.borrowBalanceCurrent(userAddress); - if (borrowBalance.eq(0)) { - console.log("Skipping exitSingleAssetLeverage test - no position to exit"); + leverageResults.push({ + section: "exitSingleAssetLeverage", + test: "Exit single-asset position", + status: "SKIPPED", + detail: "No position to exit", + }); + console.log(" [SKIP] No position to exit"); return; } - // Get balances before const vUBalanceBefore = await vUContract.balanceOf(userAddress); - - // Flash loan amount: borrow balance + 1% buffer const flashLoanAmount = borrowBalance.mul(101).div(100); - // Call exitSingleAssetLeverage const tx = await leverageStrategiesManager.connect(testUser).exitSingleAssetLeverage(vU, flashLoanAmount); const receipt = await tx.wait(); - // Parse and verify SingleAssetLeverageExited event - const iface = new ethers.utils.Interface(LEVERAGE_STRATEGIES_MANAGER_ABI); - const exitEvents = receipt.logs - .map((log: { topics: string[]; data: string }) => { - try { - return iface.parseLog(log); - } catch { - return null; - } - }) - .filter((e: { name: string } | null) => e && e.name === "SingleAssetLeverageExited"); - + const exitEvents = parseEvents(receipt, LEVERAGE_STRATEGIES_MANAGER_ABI, "SingleAssetLeverageExited"); expect(exitEvents.length).to.equal(1); - const event = exitEvents[0]; - - // Verify event parameters - expect(event.args.user.toLowerCase()).to.equal(userAddress.toLowerCase()); - expect(event.args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); - expect(event.args.collateralAmountToFlashLoan).to.equal(flashLoanAmount); + expect(exitEvents[0].args.user.toLowerCase()).to.equal(userAddress.toLowerCase()); + expect(exitEvents[0].args.collateralMarket.toLowerCase()).to.equal(vU.toLowerCase()); + expect(exitEvents[0].args.collateralAmountToFlashLoan).to.equal(flashLoanAmount); - // Get balances after - const vUBalanceAfter = await vUContract.balanceOf(userAddress); - const borrowBalanceAfter = await vUContract.callStatic.borrowBalanceCurrent(userAddress); + const [vUBalanceAfter, borrowBalanceAfter] = await Promise.all([ + vUContract.balanceOf(userAddress), + vUContract.callStatic.borrowBalanceCurrent(userAddress), + ]); - // Borrow balance should be zero expect(borrowBalanceAfter).to.equal(0); - - // User should have less vU (used to repay flash loan) expect(vUBalanceAfter).to.be.lt(vUBalanceBefore); - console.log(`Single asset leverage exited:`); - console.log(` vU balance: ${vUBalanceBefore.toString()} -> ${vUBalanceAfter.toString()}`); - console.log(` U borrow: ${borrowBalance.toString()} -> ${borrowBalanceAfter.toString()}`); + leverageResults.push({ + section: "exitSingleAssetLeverage", + test: "Exit single-asset position", + status: "PASSED", + detail: `vU: ${vUBalanceBefore} -> ${vUBalanceAfter}, Borrow: ${borrowBalance} -> ${borrowBalanceAfter}`, + }); }); }); + + after(() => { + printLeverageResultsSummary(); + }); }); diff --git a/simulations/vip-600/bscmainnet.ts b/simulations/vip-600/bscmainnet.ts index 13bba5d4b..c7ff3721c 100644 --- a/simulations/vip-600/bscmainnet.ts +++ b/simulations/vip-600/bscmainnet.ts @@ -41,40 +41,24 @@ const PANCAKE_V2_ROUTER_ABI = [ ]; // ============================================================================= -// Manual Swap Calldata Builder (builds against forked chain state) +// EIP-712 Swap Signer // ============================================================================= -// Shared signer wallet for EIP-712 signing — set up once in before() hook let swapSignerWallet: Wallet; let swapHelperContract: Contract; let eip712Domain: { name: string; version: string; chainId: number; verifyingContract: string }; let saltCounter = 0; -/** - * Configures a deterministic EIP-712 signer for the SwapHelper contract on the forked chain. - * - * In production, the SwapHelper requires a backend signer to authorize multicall executions - * via EIP-712 signatures. In tests, we: - * 1. Create a deterministic Hardhat wallet (using Hardhat's default private key) - * 2. Impersonate the SwapHelper owner to register this wallet as the authorized backendSigner - * 3. Cache the EIP-712 domain parameters (name, version, chainId, verifyingContract) for later signing - * - * This must be called once in the before() hook before any swap tests that need signed calldata. - */ async function setupSwapSigner() { - // Create a deterministic wallet for signing swapSignerWallet = new Wallet("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ethers.provider); swapHelperContract = new ethers.Contract(SWAP_HELPER, SWAP_HELPER_ABI, ethers.provider); - // Impersonate SwapHelper owner and set our wallet as backendSigner const swapHelperOwner = await swapHelperContract.owner(); const impersonatedOwner = await initMainnetUser(swapHelperOwner, ethers.utils.parseEther("1")); await swapHelperContract.connect(impersonatedOwner).setBackendSigner(swapSignerWallet.address); - // Read EIP-712 domain — use the actual chainId from the network (hardhat fork uses 31337, not 56) - const domain = await swapHelperContract.eip712Domain(); - const network = await ethers.provider.getNetwork(); + const [domain, network] = await Promise.all([swapHelperContract.eip712Domain(), ethers.provider.getNetwork()]); eip712Domain = { name: domain.name, version: domain.version, @@ -83,48 +67,112 @@ async function setupSwapSigner() { }; } +// ============================================================================= +// Swap Calldata Builders (API-first with PancakeSwap V2 fallback) +// ============================================================================= + /** - * Builds the full signed SwapHelper multicall calldata for a PancakeSwap V2 token swap. - * - * This function simulates what the backend swap service does in production: - * 1. Queries PancakeSwap V2 for the expected output amount (tries direct pair, falls back to WBNB route) - * 2. Applies slippage tolerance to compute minAmountOut - * 3. Encodes a 3-step SwapHelper multicall: - * a. approveMax — approve PancakeSwap router to spend tokenIn held by SwapHelper - * b. genericCall — execute swapExactTokensForTokens on PancakeSwap, receiving tokens to SwapHelper - * c. sweep — transfer the swapped tokens from SwapHelper to the recipient (SwapRouter) - * 4. Signs the multicall with EIP-712 using the test backend signer wallet - * 5. Returns the encoded multicall calldata ready to pass to SwapRouter functions - * - * @param tokenIn - Address of the input token (use NATIVE_TOKEN_ADDR for BNB) - * @param tokenOut - Address of the desired output token - * @param amountIn - Exact amount of tokenIn to swap (in wei/mantissa) - * @param recipient - Address to receive swapped tokens (typically SWAP_ROUTER) - * @param slippageBps - Slippage tolerance in basis points (default: 100 = 1%) - * @returns swapData (encoded multicall bytes), minAmountOut, and expected amountOut + * Tries the Venus swap API first (5s timeout), falls back to PancakeSwap V2 on-chain. */ async function buildSwapCalldata( tokenIn: string, tokenOut: string, amountIn: BigNumber, recipient: string, - slippageBps: number = 100, // 1% default + slippageBps: number = 100, ): Promise<{ swapData: string; minAmountOut: BigNumber; amountOut: BigNumber }> { - const pancakeRouter = new ethers.Contract(PANCAKE_V2_ROUTER, PANCAKE_V2_ROUTER_ABI, ethers.provider); + try { + const result = await buildSwapCalldataFromAPI(tokenIn, tokenOut, amountIn, recipient); + return { ...result, amountOut: result.minAmountOut }; + } catch (apiError) { + console.log( + ` Swap API unavailable (${ + apiError instanceof Error ? apiError.message : apiError + }), falling back to PancakeSwap V2`, + ); + } + return buildSwapCalldataFromPancakeV2(tokenIn, tokenOut, amountIn, recipient, slippageBps); +} + +async function buildSwapCalldataFromAPI( + tokenIn: string, + tokenOut: string, + amountIn: BigNumber, + recipient: string, +): Promise<{ swapData: string; minAmountOut: BigNumber }> { + const TEN_YEARS_SECS = 10 * 365 * 24 * 60 * 60; + const deadline = Math.floor(Date.now() / 1000) + TEN_YEARS_SECS; + const actualTokenIn = tokenIn.toLowerCase() === NATIVE_TOKEN_ADDR.toLowerCase() ? WBNB : tokenIn; + + const params = new URLSearchParams({ + chainId: "56", + tokenInAddress: actualTokenIn, + tokenOutAddress: tokenOut, + slippagePercentage: "0.5", + recipientAddress: SWAP_HELPER, + deadlineTimestampSecs: deadline.toString(), + type: "exact-in", + shouldTransferToReceiver: "false", + exactAmountInMantissa: amountIn.toString(), + }); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 20000); + + try { + const res = await fetch(`https://api.venus.io/find-swap?${params}`, { signal: controller.signal }); + if (!res.ok) throw new Error(`Swap API error: ${res.status}`); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = (await res.json()) as any; + if (!json.quotes?.length) throw new Error(`No API route found for ${tokenIn} -> ${tokenOut}`); + + const quote = json.quotes[0]; + const swapHelperIface = new ethers.utils.Interface(SWAP_HELPER_ABI); + const calls: string[] = []; - // For native swaps, the actual tokenIn on the DEX is WBNB + for (const tx of quote.txs) { + calls.push(swapHelperIface.encodeFunctionData("approveMax", [actualTokenIn, tx.target])); + calls.push(swapHelperIface.encodeFunctionData("genericCall", [tx.target, tx.data])); + } + calls.push(swapHelperIface.encodeFunctionData("sweep", [tokenOut, recipient])); + + const salt = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["uint256"], [++saltCounter])); + const types = { + Multicall: [ + { name: "caller", type: "address" }, + { name: "calls", type: "bytes[]" }, + { name: "deadline", type: "uint256" }, + { name: "salt", type: "bytes32" }, + ], + }; + const value = { caller: SWAP_ROUTER, calls, deadline, salt }; + const signature = await swapSignerWallet._signTypedData(eip712Domain, types, value); + const multicallData = swapHelperIface.encodeFunctionData("multicall", [calls, deadline, salt, signature]); + + return { swapData: multicallData, minAmountOut: BigNumber.from(1) }; + } finally { + clearTimeout(timeoutId); + } +} + +async function buildSwapCalldataFromPancakeV2( + tokenIn: string, + tokenOut: string, + amountIn: BigNumber, + recipient: string, + slippageBps: number, +): Promise<{ swapData: string; minAmountOut: BigNumber; amountOut: BigNumber }> { + const pancakeRouter = new ethers.Contract(PANCAKE_V2_ROUTER, PANCAKE_V2_ROUTER_ABI, ethers.provider); const actualTokenIn = tokenIn.toLowerCase() === NATIVE_TOKEN_ADDR.toLowerCase() ? WBNB : tokenIn; - // Build path — direct pair or via WBNB let path: string[]; let amounts: BigNumber[]; try { - // Try direct path first path = [actualTokenIn, tokenOut]; amounts = await pancakeRouter.getAmountsOut(amountIn, path); } catch { - // If direct path fails, route via WBNB if (actualTokenIn !== WBNB && tokenOut !== WBNB) { path = [actualTokenIn, WBNB, tokenOut]; amounts = await pancakeRouter.getAmountsOut(amountIn, path); @@ -136,32 +184,26 @@ async function buildSwapCalldata( const amountOut = amounts[amounts.length - 1]; const minAmountOut = amountOut.mul(10000 - slippageBps).div(10000); - // Build the SwapHelper multicall calls array const swapHelperIface = new ethers.utils.Interface(SWAP_HELPER_ABI); - - // 1. approveMax(tokenIn, PancakeRouter) — let SwapHelper approve the DEX - const approveCall = swapHelperIface.encodeFunctionData("approveMax", [actualTokenIn, PANCAKE_V2_ROUTER]); - - // 2. genericCall(PancakeRouter, swapExactTokensForTokens(...)) const pancakeIface = new ethers.utils.Interface(PANCAKE_V2_ROUTER_ABI); const deadline = Math.floor(Date.now() / 1000) + 3600; - const swapCalldata = pancakeIface.encodeFunctionData("swapExactTokensForTokens", [ - amountIn, - minAmountOut, - path, - SWAP_HELPER, // tokens go to SwapHelper first - deadline, - ]); - const genericCall = swapHelperIface.encodeFunctionData("genericCall", [PANCAKE_V2_ROUTER, swapCalldata]); - - // 3. sweep(tokenOut, recipient) — send swapped tokens to SwapRouter - const sweepCall = swapHelperIface.encodeFunctionData("sweep", [tokenOut, recipient]); - const calls = [approveCall, genericCall, sweepCall]; + const calls = [ + swapHelperIface.encodeFunctionData("approveMax", [actualTokenIn, PANCAKE_V2_ROUTER]), + swapHelperIface.encodeFunctionData("genericCall", [ + PANCAKE_V2_ROUTER, + pancakeIface.encodeFunctionData("swapExactTokensForTokens", [ + amountIn, + minAmountOut, + path, + SWAP_HELPER, + deadline, + ]), + ]), + swapHelperIface.encodeFunctionData("sweep", [tokenOut, recipient]), + ]; - // EIP-712 sign — type includes "address caller" which is msg.sender (SwapRouter) const salt = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["uint256"], [++saltCounter])); - const types = { Multicall: [ { name: "caller", type: "address" }, @@ -170,33 +212,17 @@ async function buildSwapCalldata( { name: "salt", type: "bytes32" }, ], }; - - const value = { - caller: SWAP_ROUTER, - calls, - deadline, - salt, - }; - + const value = { caller: SWAP_ROUTER, calls, deadline, salt }; const signature = await swapSignerWallet._signTypedData(eip712Domain, types, value); - - // Encode the full multicall call const multicallData = swapHelperIface.encodeFunctionData("multicall", [calls, deadline, salt, signature]); return { swapData: multicallData, minAmountOut, amountOut }; } -/** - * High-level wrapper around buildSwapCalldata that accepts string-based parameters - * matching the interface used by test cases. Converts the slippage percentage (e.g. "0.01" - * for 1%) to basis points and the amount string to BigNumber before delegating. - * - * @param tokenInAddress - Address of the input token - * @param tokenOutAddress - Address of the desired output token - * @param exactAmountInMantissa - Input amount as a string (in wei/mantissa) - * @param recipientAddress - Address to receive swapped tokens - * @param slippagePercentage - Slippage as a decimal string (e.g. "0.01" = 1%) - */ +// ============================================================================= +// Swap Data Wrappers +// ============================================================================= + async function getSwapData( tokenInAddress: string, tokenOutAddress: string, @@ -214,16 +240,6 @@ async function getSwapData( ); } -/** - * Convenience wrapper for native BNB swaps. Sets tokenIn to the sentinel NATIVE_TOKEN_ADDR - * (0xbBbB...bBbB) which buildSwapCalldata translates to WBNB for the actual DEX swap. - * Used by swapNativeAndSupply, swapNativeAndRepay, and swapNativeAndRepayFull tests. - * - * @param tokenOutAddress - Address of the desired output token - * @param exactAmountInMantissa - BNB amount as a string (in wei) - * @param recipientAddress - Address to receive swapped tokens - * @param slippagePercentage - Slippage as a decimal string (e.g. "0.01" = 1%) - */ async function getNativeSwapData( tokenOutAddress: string, exactAmountInMantissa: string, @@ -234,7 +250,50 @@ async function getNativeSwapData( } // ============================================================================= -// Helpers: Event Parsing +// Results Tracker +// ============================================================================= + +interface SwapRouterResult { + section: string; + test: string; + status: "PASSED" | "SKIPPED" | "FAILED"; + detail: string; +} +const swapRouterResults: SwapRouterResult[] = []; + +function printSwapRouterResultsSummary() { + const passed = swapRouterResults.filter(r => r.status === "PASSED"); + const failed = swapRouterResults.filter(r => r.status === "FAILED"); + const skipped = swapRouterResults.filter(r => r.status === "SKIPPED"); + + console.log("\n" + "=".repeat(120)); + console.log(" SWAP ROUTER RESULTS SUMMARY"); + console.log("=".repeat(120)); + + for (const [label, list] of [ + ["PASSED", passed], + ["FAILED", failed], + ["SKIPPED", skipped], + ] as const) { + if (list.length === 0) continue; + console.log(`\n ${label} (${list.length})`); + console.log(" " + "-".repeat(116)); + console.log(` ${"Section".padEnd(30)} ${"Test".padEnd(55)} ${label === "PASSED" ? "Detail" : "Reason"}`); + console.log(" " + "-".repeat(116)); + for (const r of list) { + console.log(` ${r.section.padEnd(30)} ${r.test.padEnd(55)} ${r.detail}`); + } + } + + console.log("\n" + "=".repeat(120)); + console.log( + ` Total: ${swapRouterResults.length} | Passed: ${passed.length} | Failed: ${failed.length} | Skipped: ${skipped.length}`, + ); + console.log("=".repeat(120) + "\n"); +} + +// ============================================================================= +// Helpers // ============================================================================= function parseEventFromReceipt(receipt: any, eventName: string): any[] { @@ -254,8 +313,6 @@ function parseEventFromReceipt(receipt: any, eventName: string): any[] { // Test Suite // ============================================================================= -// Fork BSC mainnet at a specific block to get deterministic test results. -// All tests run against this snapshot, with the VIP executed mid-suite via testVip(). forking(FORK_BLOCK, async () => { let swapRouter: Contract; let comptroller: Contract; @@ -268,7 +325,7 @@ forking(FORK_BLOCK, async () => { let testUserAddress: string; before(async () => { - // Initialize contract instances for the SwapRouter, Comptroller, tokens, and vTokens + // Instantiate contracts (pure local — no RPC) swapRouter = await ethers.getContractAt(SWAP_ROUTER_ABI, SWAP_ROUTER); comptroller = new ethers.Contract(UNITROLLER, COMPTROLLER_ABI, ethers.provider); usdt = new ethers.Contract(USDT, ERC20_ABI, ethers.provider); @@ -276,22 +333,21 @@ forking(FORK_BLOCK, async () => { vUSDTContract = new ethers.Contract(vUSDT, VTOKEN_ABI, ethers.provider); vUSDCContract = new ethers.Contract(vUSDC, VTOKEN_ABI, ethers.provider); - // Impersonate the timelock to execute owner-only functions (sweepToken, transferOwnership, etc.) - impersonatedTimelock = await initMainnetUser(bscmainnet.NORMAL_TIMELOCK, ethers.utils.parseEther("10")); - const signers = await ethers.getSigners(); testUser = signers[0]; testUserAddress = await testUser.getAddress(); - // Fund the test user with 10,000 USDT and 10,000 USDC from whale accounts - // so they have enough tokens for all swap and supply/repay tests - const usdtHolder = await initMainnetUser(USDT_HOLDER, ethers.utils.parseEther("10")); - const usdcHolder = await initMainnetUser(USDC_HOLDER, ethers.utils.parseEther("10")); - await usdt.connect(usdtHolder).transfer(testUserAddress, parseUnits("10000", 18)); - await usdc.connect(usdcHolder).transfer(testUserAddress, parseUnits("10000", 18)); - - // Set oracle stale periods to ~10 years so price feeds don't revert as "stale" - // on the forked chain (block timestamps are in the past relative to real time) + // Step 1: Impersonate users and setup swap signer (no shared-signer conflicts) + const [, usdtHolder, usdcHolder, timelockSigner] = await Promise.all([ + setupSwapSigner(), + initMainnetUser(USDT_HOLDER, ethers.utils.parseEther("10")), + initMainnetUser(USDC_HOLDER, ethers.utils.parseEther("10")), + initMainnetUser(bscmainnet.NORMAL_TIMELOCK, ethers.utils.parseEther("10")), + ]); + impersonatedTimelock = timelockSigner; + + // Step 2: Configure oracles + // Chainlink calls use NORMAL_TIMELOCK as signer — must be sequential to avoid nonce conflicts await setMaxStalePeriodInChainlinkOracle( bscmainnet.CHAINLINK_ORACLE, USDC, @@ -306,12 +362,15 @@ forking(FORK_BLOCK, async () => { bscmainnet.NORMAL_TIMELOCK, 315360000, ); + // Binance oracle calls use oracle.owner() as signer — same owner so also sequential await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, "USDC", 315360000); await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, "USDT", 315360000); - // Setup the EIP-712 swap signer so we can build signed multicall calldata - // for PancakeSwap swaps routed through the SwapHelper contract - await setupSwapSigner(); + // Step 3: Fund test user (different signers — safe in parallel) + await Promise.all([ + usdt.connect(usdtHolder).transfer(testUserAddress, parseUnits("10000", 18)), + usdc.connect(usdcHolder).transfer(testUserAddress, parseUnits("10000", 18)), + ]); }); // =========================================================================== @@ -319,35 +378,25 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("Pre-VIP behavior", () => { - // Verify that before the VIP is executed, the SwapRouter either already has - // NORMAL_TIMELOCK as owner or has it set as pendingOwner (awaiting acceptOwnership). - it("should have correct ownership state", async () => { - const pendingOwner = await swapRouter.pendingOwner(); - const owner = await swapRouter.owner(); + it("should have correct ownership state and immutable parameters", async () => { + const [pendingOwner, owner, comptrollerAddr, swapHelperAddr, wrappedNative, nativeVToken, nativeTokenAddr] = + await Promise.all([ + swapRouter.pendingOwner(), + swapRouter.owner(), + swapRouter.COMPTROLLER(), + swapRouter.SWAP_HELPER(), + swapRouter.WRAPPED_NATIVE(), + swapRouter.NATIVE_VTOKEN(), + swapRouter.NATIVE_TOKEN_ADDR(), + ]); + const isValidState = pendingOwner === bscmainnet.NORMAL_TIMELOCK || owner === bscmainnet.NORMAL_TIMELOCK; expect(isValidState).to.be.true; - }); - - // Validate all immutable constructor parameters were set correctly during deployment. - // These cannot be changed after deployment, so we confirm them before VIP execution. - it("should have correct COMPTROLLER", async () => { - expect(await swapRouter.COMPTROLLER()).to.equal(UNITROLLER); - }); - - it("should have correct SWAP_HELPER", async () => { - expect(await swapRouter.SWAP_HELPER()).to.equal(SWAP_HELPER); - }); - - it("should have correct WRAPPED_NATIVE (WBNB)", async () => { - expect(await swapRouter.WRAPPED_NATIVE()).to.equal(WBNB); - }); - - it("should have correct NATIVE_VTOKEN (vBNB)", async () => { - expect(await swapRouter.NATIVE_VTOKEN()).to.equal(vBNB); - }); - - it("should have correct NATIVE_TOKEN_ADDR", async () => { - expect(await swapRouter.NATIVE_TOKEN_ADDR()).to.equal(NATIVE_TOKEN_ADDR); + expect(comptrollerAddr).to.equal(UNITROLLER); + expect(swapHelperAddr).to.equal(SWAP_HELPER); + expect(wrappedNative).to.equal(WBNB); + expect(nativeVToken).to.equal(vBNB); + expect(nativeTokenAddr).to.equal(NATIVE_TOKEN_ADDR); }); }); @@ -355,8 +404,6 @@ forking(FORK_BLOCK, async () => { // VIP Execution // =========================================================================== - // Execute the VIP proposal on the forked chain. After execution, verify that - // at most one OwnershipTransferred event was emitted (the acceptOwnership call). testVip("VIP-600", await vip600(), { callbackAfterExecution: async txResponse => { const receipt = await txResponse.wait(); @@ -370,14 +417,10 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("Post-VIP behavior", () => { - // After VIP execution, ownership should be fully transferred to NORMAL_TIMELOCK - // and pendingOwner should be cleared (zero address), confirming acceptOwnership succeeded. - it("should have NORMAL_TIMELOCK as owner", async () => { - expect(await swapRouter.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); - }); - - it("should have zero address as pending owner", async () => { - expect(await swapRouter.pendingOwner()).to.equal(ethers.constants.AddressZero); + it("should have NORMAL_TIMELOCK as owner with zero pending owner", async () => { + const [owner, pendingOwner] = await Promise.all([swapRouter.owner(), swapRouter.pendingOwner()]); + expect(owner).to.equal(bscmainnet.NORMAL_TIMELOCK); + expect(pendingOwner).to.equal(ethers.constants.AddressZero); }); }); @@ -386,58 +429,65 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("swapAndSupply", () => { - // Happy path: User swaps 100 USDT for USDC via PancakeSwap, then the router - // automatically supplies the received USDC into the vUSDC market on behalf of the user. - // Verifies: SwapAndSupply event emitted, USDT balance decreased, vUSDC balance increased. it("should swap USDT -> USDC and supply to vUSDC", async () => { const amountIn = parseUnits("100", 18); - // Build swap calldata: USDT -> USDC via PancakeSwap V2, with 1% slippage tolerance const { swapData, minAmountOut } = await getSwapData(USDT, USDC, amountIn.toString(), SWAP_ROUTER, "0.01"); - const usdtBefore = await usdt.balanceOf(testUserAddress); - const vUSDCBefore = await vUSDCContract.balanceOf(testUserAddress); + const [usdtBefore, vUSDCBefore] = await Promise.all([ + usdt.balanceOf(testUserAddress), + vUSDCContract.balanceOf(testUserAddress), + ]); - // Approve the SwapRouter to pull USDT from the user await usdt.connect(testUser).approve(SWAP_ROUTER, amountIn); const tx = await swapRouter.connect(testUser).swapAndSupply(vUSDC, USDT, amountIn, minAmountOut, swapData); const receipt = await tx.wait(); - // Verify the SwapAndSupply event contains the correct user, input amount, and output >= minAmountOut const events = parseEventFromReceipt(receipt, "SwapAndSupply"); expect(events.length).to.equal(1); expect(events[0].args.user.toLowerCase()).to.equal(testUserAddress.toLowerCase()); expect(events[0].args.amountIn).to.equal(amountIn); expect(events[0].args.amountOut).to.be.gte(minAmountOut); - // Confirm token balances changed: USDT spent, vUSDC (supply receipt tokens) received - expect((await usdt.balanceOf(testUserAddress)).lt(usdtBefore)).to.be.true; - expect((await vUSDCContract.balanceOf(testUserAddress)).gt(vUSDCBefore)).to.be.true; + const [usdtAfter, vUSDCAfter] = await Promise.all([ + usdt.balanceOf(testUserAddress), + vUSDCContract.balanceOf(testUserAddress), + ]); + expect(usdtAfter).to.be.lt(usdtBefore); + expect(vUSDCAfter).to.be.gt(vUSDCBefore); + + swapRouterResults.push({ + section: "swapAndSupply", + test: "USDT -> USDC supply to vUSDC", + status: "PASSED", + detail: `amountIn: ${amountIn}, amountOut: ${events[0].args.amountOut}`, + }); }); - // Supplying to a vToken that isn't listed in the Comptroller should revert - it("should revert with MarketNotListed for unlisted vToken", async () => { - const fakeVToken = "0x0000000000000000000000000000000000000001"; + it("should revert with MarketNotListed, ZeroAmount, and ZeroAddress", async () => { await usdt.connect(testUser).approve(SWAP_ROUTER, parseUnits("100", 18)); await expect( - swapRouter.connect(testUser).swapAndSupply(fakeVToken, USDT, parseUnits("100", 18), 0, "0x"), + swapRouter + .connect(testUser) + .swapAndSupply("0x0000000000000000000000000000000000000001", USDT, parseUnits("100", 18), 0, "0x"), ).to.be.revertedWithCustomError(swapRouter, "MarketNotListed"); - }); - - // Zero input amount is rejected to prevent no-op transactions - it("should revert with ZeroAmount when amountIn is zero", async () => { await expect(swapRouter.connect(testUser).swapAndSupply(vUSDC, USDT, 0, 0, "0x")).to.be.revertedWithCustomError( swapRouter, "ZeroAmount", ); - }); - - // Zero address for vToken is rejected as an input validation check - it("should revert with ZeroAddress when vToken is zero", async () => { await expect( swapRouter.connect(testUser).swapAndSupply(ethers.constants.AddressZero, USDT, parseUnits("100", 18), 0, "0x"), ).to.be.revertedWithCustomError(swapRouter, "ZeroAddress"); + + for (const name of ["MarketNotListed", "ZeroAmount", "ZeroAddress"]) { + swapRouterResults.push({ + section: "swapAndSupply", + test: `Revert ${name}`, + status: "PASSED", + detail: "Correctly reverted", + }); + } }); }); @@ -447,8 +497,6 @@ forking(FORK_BLOCK, async () => { describe("swapAndRepay", () => { before(async () => { - // Create a borrow position: supply 5000 USDC as collateral, enter the market, - // then borrow 1000 USDT against it. This sets up the state needed for repay tests. const supplyAmount = parseUnits("5000", 18); await usdc.connect(testUser).approve(vUSDC, supplyAmount); await vUSDCContract.connect(testUser).mint(supplyAmount); @@ -456,34 +504,45 @@ forking(FORK_BLOCK, async () => { await vUSDTContract.connect(testUser).borrow(parseUnits("1000", 18)); }); - // Happy path: User swaps 500 USDC for USDT, and the router uses the received USDT - // to repay part of the user's outstanding vUSDT borrow. Confirms borrow balance decreased. it("should swap USDC -> USDT and repay vUSDT borrow", async () => { const amountIn = parseUnits("500", 18); - const borrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); + const [borrowBefore, { swapData, minAmountOut }] = await Promise.all([ + vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress), + getSwapData(USDC, USDT, amountIn.toString(), SWAP_ROUTER, "0.01"), + ]); expect(borrowBefore).to.be.gt(0); - const { swapData, minAmountOut } = await getSwapData(USDC, USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); - await usdc.connect(testUser).approve(SWAP_ROUTER, amountIn); const tx = await swapRouter.connect(testUser).swapAndRepay(vUSDT, USDC, amountIn, minAmountOut, swapData); const receipt = await tx.wait(); - // Verify SwapAndRepay event was emitted with a non-zero repaid amount const events = parseEventFromReceipt(receipt, "SwapAndRepay"); expect(events.length).to.equal(1); expect(events[0].args.amountRepaid).to.be.gt(0); - // Borrow balance should have decreased after partial repayment const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); expect(borrowAfter).to.be.lt(borrowBefore); + + swapRouterResults.push({ + section: "swapAndRepay", + test: "USDC -> USDT repay vUSDT borrow", + status: "PASSED", + detail: `Borrow: ${borrowBefore} -> ${borrowAfter}`, + }); }); it("should revert with ZeroAddress when vToken is zero", async () => { await expect( swapRouter.connect(testUser).swapAndRepay(ethers.constants.AddressZero, USDT, parseUnits("100", 18), 0, "0x"), ).to.be.revertedWithCustomError(swapRouter, "ZeroAddress"); + + swapRouterResults.push({ + section: "swapAndRepay", + test: "Revert ZeroAddress", + status: "PASSED", + detail: "Correctly reverted", + }); }); }); @@ -493,7 +552,6 @@ forking(FORK_BLOCK, async () => { describe("swapAndRepayFull", () => { before(async () => { - // Ensure a borrow position exists; if previous tests fully repaid, re-borrow const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBalance.eq(0)) { const supplyAmount = parseUnits("2000", 18); @@ -503,18 +561,19 @@ forking(FORK_BLOCK, async () => { } }); - // Full repayment: swaps enough USDC -> USDT to cover the entire outstanding borrow. - // Uses 110% of borrow balance as maxAmountIn to account for swap slippage and accrued interest. - // The router repays only what's owed and refunds any excess tokens back to the user. - // After execution, borrow balance should be exactly zero. it("should swap USDC -> USDT and fully repay vUSDT borrow", async () => { const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBalance.eq(0)) { + swapRouterResults.push({ + section: "swapAndRepayFull", + test: "USDC -> USDT full repay vUSDT", + status: "SKIPPED", + detail: "No borrow balance", + }); console.log(" [SKIP] No borrow balance"); return; } - // Provide 10% buffer over borrow balance to cover slippage and accrued interest const maxAmountIn = borrowBalance.mul(110).div(100); const { swapData } = await getSwapData(USDC, USDT, maxAmountIn.toString(), SWAP_ROUTER, "0.01"); @@ -526,9 +585,15 @@ forking(FORK_BLOCK, async () => { const events = parseEventFromReceipt(receipt, "SwapAndRepay"); expect(events.length).to.equal(1); - // Borrow balance should be fully cleared const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); expect(borrowAfter).to.equal(0); + + swapRouterResults.push({ + section: "swapAndRepayFull", + test: "USDC -> USDT full repay vUSDT", + status: "PASSED", + detail: `Borrow: ${borrowBalance} -> 0`, + }); }); }); @@ -537,16 +602,13 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("swapNativeAndSupply", () => { - // Happy path: User sends 1 BNB which the router wraps to WBNB, swaps to USDT - // via PancakeSwap, then supplies the USDT into the vUSDT market for the user. - // The event reports WBNB as tokenIn since the router wraps before swapping. it("should swap BNB -> USDT and supply to vUSDT", async () => { const amountIn = parseUnits("1", 18); - const { swapData, minAmountOut } = await getNativeSwapData(USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); - - const vUSDTBefore = await vUSDTContract.balanceOf(testUserAddress); + const [{ swapData, minAmountOut }, vUSDTBefore] = await Promise.all([ + getNativeSwapData(USDT, amountIn.toString(), SWAP_ROUTER, "0.01"), + vUSDTContract.balanceOf(testUserAddress), + ]); - // Send BNB as msg.value; the router handles wrapping to WBNB internally const tx = await swapRouter .connect(testUser) .swapNativeAndSupply(vUSDT, minAmountOut, swapData, { value: amountIn }); @@ -554,11 +616,16 @@ forking(FORK_BLOCK, async () => { const events = parseEventFromReceipt(receipt, "SwapAndSupply"); expect(events.length).to.equal(1); - // SwapRouter wraps BNB to WBNB before swapping, so tokenIn in the event is WBNB expect(events[0].args.tokenIn.toLowerCase()).to.equal(WBNB.toLowerCase()); - // User should have received vUSDT tokens representing their supply position - expect((await vUSDTContract.balanceOf(testUserAddress)).gt(vUSDTBefore)).to.be.true; + expect(await vUSDTContract.balanceOf(testUserAddress)).to.be.gt(vUSDTBefore); + + swapRouterResults.push({ + section: "swapNativeAndSupply", + test: "BNB -> USDT supply to vUSDT", + status: "PASSED", + detail: `amountIn: ${amountIn}, amountOut: ${events[0].args.amountOut}`, + }); }); }); @@ -568,7 +635,6 @@ forking(FORK_BLOCK, async () => { describe("swapNativeAndRepay", () => { before(async () => { - // Ensure a borrow position exists for the repay test const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBalance.eq(0)) { const supplyAmount = parseUnits("2000", 18); @@ -578,13 +644,17 @@ forking(FORK_BLOCK, async () => { } }); - // Partial repayment using native BNB: sends 0.5 BNB which is wrapped to WBNB, - // swapped to USDT, and used to partially repay the user's vUSDT borrow position. it("should swap BNB -> USDT and repay vUSDT borrow", async () => { const amountIn = parseUnits("0.5", 18); const borrowBefore = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBefore.eq(0)) { + swapRouterResults.push({ + section: "swapNativeAndRepay", + test: "BNB -> USDT repay vUSDT", + status: "SKIPPED", + detail: "No borrow balance", + }); console.log(" [SKIP] No borrow balance"); return; } @@ -599,9 +669,15 @@ forking(FORK_BLOCK, async () => { const events = parseEventFromReceipt(receipt, "SwapAndRepay"); expect(events.length).to.equal(1); - // Borrow balance should have decreased after partial repayment with native BNB const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); expect(borrowAfter).to.be.lt(borrowBefore); + + swapRouterResults.push({ + section: "swapNativeAndRepay", + test: "BNB -> USDT repay vUSDT", + status: "PASSED", + detail: `Borrow: ${borrowBefore} -> ${borrowAfter}`, + }); }); }); @@ -611,7 +687,6 @@ forking(FORK_BLOCK, async () => { describe("swapNativeAndRepayFull", () => { before(async () => { - // Ensure a borrow position exists for the full repay test const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBalance.eq(0)) { const supplyAmount = parseUnits("1000", 18); @@ -621,17 +696,19 @@ forking(FORK_BLOCK, async () => { } }); - // Full repayment using native BNB: sends 1 BNB (more than enough to cover the ~100 USDT borrow). - // The router wraps BNB -> WBNB, swaps to USDT, repays the exact borrow amount, - // and refunds any excess tokens. Borrow balance should be zero after execution. it("should swap BNB -> USDT and fully repay vUSDT borrow", async () => { const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); if (borrowBalance.eq(0)) { + swapRouterResults.push({ + section: "swapNativeAndRepayFull", + test: "BNB -> USDT full repay vUSDT", + status: "SKIPPED", + detail: "No borrow balance", + }); console.log(" [SKIP] No borrow balance"); return; } - // Send more BNB than needed; the router handles exact repayment and refunds excess const amountIn = parseUnits("1", 18); const { swapData } = await getNativeSwapData(USDT, amountIn.toString(), SWAP_ROUTER, "0.01"); @@ -641,9 +718,15 @@ forking(FORK_BLOCK, async () => { const events = parseEventFromReceipt(receipt, "SwapAndRepay"); expect(events.length).to.equal(1); - // Borrow should be fully cleared const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(testUserAddress); expect(borrowAfter).to.equal(0); + + swapRouterResults.push({ + section: "swapNativeAndRepayFull", + test: "BNB -> USDT full repay vUSDT", + status: "PASSED", + detail: `Borrow: ${borrowBalance} -> 0`, + }); }); }); @@ -652,44 +735,50 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("sweepToken", () => { - // Recovery mechanism: if ERC20 tokens are accidentally sent directly to the router - // (not via a swap), the owner can sweep them out. This simulates an accidental - // transfer of 10 USDT to the router, then the owner recovers them. it("should allow owner to sweep ERC20 tokens stuck in the router", async () => { - // Simulate accidental token transfer to the router contract const accidentalAmount = parseUnits("10", 18); const usdtHolder = await initMainnetUser(USDT_HOLDER, ethers.utils.parseEther("1")); await usdt.connect(usdtHolder).transfer(SWAP_ROUTER, accidentalAmount); - const routerBalanceBefore = await usdt.balanceOf(SWAP_ROUTER); + const [routerBalanceBefore, ownerAddress] = await Promise.all([usdt.balanceOf(SWAP_ROUTER), swapRouter.owner()]); expect(routerBalanceBefore).to.be.gte(accidentalAmount); - // sweepToken transfers the entire token balance to the owner (NORMAL_TIMELOCK) - const ownerAddress = await swapRouter.owner(); const ownerBalanceBefore = await usdt.balanceOf(ownerAddress); const tx = await swapRouter.connect(impersonatedTimelock).sweepToken(USDT); const receipt = await tx.wait(); - // Verify SweepToken event reports the correct token and full amount swept const events = parseEventFromReceipt(receipt, "SweepToken"); expect(events.length).to.equal(1); expect(events[0].args.token.toLowerCase()).to.equal(USDT.toLowerCase()); expect(events[0].args.amount).to.equal(routerBalanceBefore); - // Router balance should be zero after sweep - expect(await usdt.balanceOf(SWAP_ROUTER)).to.equal(0); - - // Owner should have received the tokens - const ownerBalanceAfter = await usdt.balanceOf(ownerAddress); + const [routerBalanceAfter, ownerBalanceAfter] = await Promise.all([ + usdt.balanceOf(SWAP_ROUTER), + usdt.balanceOf(ownerAddress), + ]); + expect(routerBalanceAfter).to.equal(0); expect(ownerBalanceAfter.sub(ownerBalanceBefore)).to.equal(routerBalanceBefore); + + swapRouterResults.push({ + section: "sweepToken", + test: "Owner sweeps ERC20 tokens", + status: "PASSED", + detail: `Swept ${routerBalanceBefore} USDT`, + }); }); - // Only the owner (NORMAL_TIMELOCK) can sweep; regular users cannot drain the router it("should revert when called by non-owner", async () => { await expect(swapRouter.connect(testUser).sweepToken(USDT)).to.be.revertedWith( "Ownable: caller is not the owner", ); + + swapRouterResults.push({ + section: "sweepToken", + test: "Revert when called by non-owner", + status: "PASSED", + detail: "Correctly reverted", + }); }); }); @@ -698,10 +787,7 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("sweepNative", () => { - // Recovery mechanism for native BNB stuck in the router (e.g. leftover from WBNB unwrapping). - // Since receive() only accepts BNB from WBNB, we use hardhat_setBalance to force BNB into the contract. it("should allow owner to sweep native BNB stuck in the router", async () => { - // Force 0.1 BNB into the router via hardhat cheatcode (can't send directly due to receive() guard) const forcedAmount = parseUnits("0.1", 18); await ethers.provider.send("hardhat_setBalance", [ SWAP_ROUTER, @@ -714,26 +800,40 @@ forking(FORK_BLOCK, async () => { const tx = await swapRouter.connect(impersonatedTimelock).sweepNative(); const receipt = await tx.wait(); - // Verify SweepNative event reports the full amount swept to the owner const events = parseEventFromReceipt(receipt, "SweepNative"); expect(events.length).to.equal(1); expect(events[0].args.amount).to.equal(routerBalance); - // Router should have zero BNB after sweep expect(await ethers.provider.getBalance(SWAP_ROUTER)).to.equal(0); - }); - // Only the owner can sweep native BNB - it("should revert when called by non-owner", async () => { - await expect(swapRouter.connect(testUser).sweepNative()).to.be.revertedWith("Ownable: caller is not the owner"); + swapRouterResults.push({ + section: "sweepNative", + test: "Owner sweeps native BNB", + status: "PASSED", + detail: `Swept ${routerBalance} wei BNB`, + }); }); - // The router's receive() function only accepts BNB from WBNB (during unwrap). - // Direct BNB transfers from any other address are rejected to prevent accidental sends. - it("should revert when non-WBNB address sends BNB directly", async () => { + it("should revert for non-owner and unauthorized native sender", async () => { + await expect(swapRouter.connect(testUser).sweepNative()).to.be.revertedWith("Ownable: caller is not the owner"); await expect( testUser.sendTransaction({ to: SWAP_ROUTER, value: parseUnits("0.1", 18) }), ).to.be.revertedWithCustomError(swapRouter, "UnauthorizedNativeSender"); + + swapRouterResults.push( + { + section: "sweepNative", + test: "Revert when called by non-owner", + status: "PASSED", + detail: "Correctly reverted", + }, + { + section: "sweepNative", + test: "Revert UnauthorizedNativeSender", + status: "PASSED", + detail: "Correctly reverted", + }, + ); }); }); @@ -742,23 +842,22 @@ forking(FORK_BLOCK, async () => { // =========================================================================== describe("Error cases", () => { - // Attempting to supply into a market that isn't registered in the Comptroller should fail - it("should revert swapAndSupply with MarketNotListed for unlisted vToken", async () => { + it("should revert with MarketNotListed and SwapFailed", async () => { + await usdt.connect(testUser).approve(SWAP_ROUTER, parseUnits("100", 18)); + await expect( swapRouter .connect(testUser) .swapAndSupply("0x0000000000000000000000000000000000000001", USDT, parseUnits("100", 18), 0, "0x"), ).to.be.revertedWithCustomError(swapRouter, "MarketNotListed"); - }); - - // Passing garbage swap calldata (0xdeadbeef) causes the SwapHelper multicall to fail, - // which the router surfaces as a SwapFailed error - it("should revert swapAndSupply with SwapFailed for invalid swap data", async () => { - await usdt.connect(testUser).approve(SWAP_ROUTER, parseUnits("100", 18)); - await expect( swapRouter.connect(testUser).swapAndSupply(vUSDC, USDT, parseUnits("100", 18), 0, "0xdeadbeef"), ).to.be.revertedWithCustomError(swapRouter, "SwapFailed"); + + swapRouterResults.push( + { section: "Error cases", test: "Revert MarketNotListed", status: "PASSED", detail: "Correctly reverted" }, + { section: "Error cases", test: "Revert SwapFailed", status: "PASSED", detail: "Correctly reverted" }, + ); }); }); @@ -771,23 +870,16 @@ forking(FORK_BLOCK, async () => { expect(await swapRouter.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); }); - // Two-step ownership transfer (Ownable2Step pattern): - // 1. Current owner calls transferOwnership(newOwner) -> sets pendingOwner - // 2. New owner calls acceptOwnership() -> becomes owner, pendingOwner cleared - // This prevents accidental transfers to wrong addresses (new owner must actively accept). - // After verifying the flow, ownership is transferred back to NORMAL_TIMELOCK for subsequent tests. it("should support two-step ownership transfer", async () => { const newOwner = testUserAddress; - // Step 1: Current owner proposes new owner await swapRouter.connect(impersonatedTimelock).transferOwnership(newOwner); expect(await swapRouter.pendingOwner()).to.equal(newOwner); - // Step 2: New owner accepts await swapRouter.connect(testUser).acceptOwnership(); expect(await swapRouter.owner()).to.equal(newOwner); - // Cleanup: Transfer ownership back to NORMAL_TIMELOCK so other tests aren't affected + // Cleanup await swapRouter.connect(testUser).transferOwnership(bscmainnet.NORMAL_TIMELOCK); await swapRouter.connect(impersonatedTimelock).acceptOwnership(); expect(await swapRouter.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); @@ -795,24 +887,18 @@ forking(FORK_BLOCK, async () => { }); // =========================================================================== - // Integration: Full Supply -> Borrow -> Repay Cycle + // Integration // =========================================================================== - // End-to-end integration test simulating a realistic user journey through the swap router: - // 1. A fresh user swaps BNB -> USDC and supplies it as collateral via swapNativeAndSupply - // 2. The user enters the USDC market and borrows USDT against their collateral - // 3. The user swaps BNB -> USDT and partially repays the borrow via swapNativeAndRepay - // This validates that all swap router functions work together in a real lending workflow. describe("Integration: full supply -> borrow -> repay cycle", () => { it("should complete entire user journey via swap router", async () => { - // Use a separate signer (signers[5]) to start with a clean slate — no prior positions const signers = await ethers.getSigners(); const integrationUser = signers[5]; const integrationUserAddress = await integrationUser.getAddress(); await initMainnetUser(integrationUserAddress, parseUnits("10", 18)); - // Step 1: Swap 2 BNB -> USDC and supply to vUSDC (creates collateral position) + // Step 1: Swap 2 BNB -> USDC and supply to vUSDC const bnbToSupply = parseUnits("2", 18); const { swapData: supplySwapData, minAmountOut: supplyMinOut } = await getNativeSwapData( USDC, @@ -828,14 +914,14 @@ forking(FORK_BLOCK, async () => { const vUSDCBalance = await vUSDCContract.balanceOf(integrationUserAddress); expect(vUSDCBalance).to.be.gt(0); - // Step 2: Enter the USDC market as collateral and borrow 100 USDT against it + // Step 2: Enter market and borrow await comptroller.connect(integrationUser).enterMarkets([vUSDC]); await vUSDTContract.connect(integrationUser).borrow(parseUnits("100", 18)); const borrowBalance = await vUSDTContract.callStatic.borrowBalanceCurrent(integrationUserAddress); expect(borrowBalance).to.be.gt(0); - // Step 3: Swap 0.5 BNB -> USDT and use it to partially repay the borrow + // Step 3: Swap 0.5 BNB -> USDT and partially repay const bnbToRepay = parseUnits("0.5", 18); const { swapData: repaySwapData, minAmountOut: repayMinOut } = await getNativeSwapData( USDT, @@ -848,9 +934,19 @@ forking(FORK_BLOCK, async () => { .connect(integrationUser) .swapNativeAndRepay(vUSDT, repayMinOut, repaySwapData, { value: bnbToRepay }); - // Confirm borrow balance decreased after partial repayment const borrowAfter = await vUSDTContract.callStatic.borrowBalanceCurrent(integrationUserAddress); expect(borrowAfter).to.be.lt(borrowBalance); + + swapRouterResults.push({ + section: "Integration", + test: "Full supply -> borrow -> repay cycle", + status: "PASSED", + detail: `Supply vUSDC: ${vUSDCBalance}, Borrow: ${borrowBalance} -> ${borrowAfter}`, + }); }); }); + + after(() => { + printSwapRouterResultsSummary(); + }); });