Skip to content

Commit d0ee9db

Browse files
authored
Merge pull request #676 from VenusProtocol/feat/VPD-675
[VPD-675]: Repay Logic Improvement
2 parents 2d2df02 + a6b450d commit d0ee9db

File tree

8 files changed

+8944
-0
lines changed

8 files changed

+8944
-0
lines changed

simulations/vip-597/abi/Comptroller.json

Lines changed: 3830 additions & 0 deletions
Large diffs are not rendered by default.

simulations/vip-597/abi/VBep20Delegator.json

Lines changed: 1546 additions & 0 deletions
Large diffs are not rendered by default.

simulations/vip-597/abi/VToken.json

Lines changed: 2022 additions & 0 deletions
Large diffs are not rendered by default.

simulations/vip-597/abi/erc20.json

Lines changed: 669 additions & 0 deletions
Large diffs are not rendered by default.

simulations/vip-597/bscmainnet.ts

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
import { TransactionResponse } from "@ethersproject/providers";
2+
import { expect } from "chai";
3+
import { BigNumber, Contract } from "ethers";
4+
import { parseUnits } from "ethers/lib/utils";
5+
import { ethers } from "hardhat";
6+
import { NETWORK_ADDRESSES } from "src/networkAddresses";
7+
import {
8+
expectEvents,
9+
initMainnetUser,
10+
setMaxStalePeriodInBinanceOracle,
11+
setMaxStalePeriodInChainlinkOracle,
12+
setRedstonePrice,
13+
} from "src/utils";
14+
import { forking, testVip } from "src/vip-framework";
15+
import { checkCorePoolComptroller } from "src/vip-framework/checks/checkCorePoolComptroller";
16+
17+
import vip597, { CORE_MARKETS, NEW_VBEP20_DELEGATE_IMPL } from "../../vips/vip-597/bscmainnet";
18+
import COMPTROLLER_ABI from "./abi/Comptroller.json";
19+
import VBEP20_DELEGATOR_ABI from "./abi/VBep20Delegator.json";
20+
import VTOKEN_ABI from "./abi/VToken.json";
21+
import ERC20_ABI from "./abi/erc20.json";
22+
23+
const { bscmainnet } = NETWORK_ADDRESSES;
24+
25+
const COMPTROLLER_LENS = "0x732138e18fa6f8f8E456ad829DB429A450a79758";
26+
const GENERIC_ETH_ACCOUNT = "0xF77055DBFAfdD56578Ace54E62e749d12802ce36";
27+
28+
// vUSDT market for core operations testing
29+
const VUSDT_ADDRESS = "0xfD5840Cd36d94D7229439859C0112a4185BC0255";
30+
const USDT_ADDRESS = "0x55d398326f99059fF775485246999027B3197955";
31+
const USDT_WHALE = "0xF977814e90dA44bFA03b6295A0616a897441aceC";
32+
33+
// vUSDC market as collateral
34+
const VUSDC_ADDRESS = "0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8";
35+
const USDC_ADDRESS = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d";
36+
const USDC_WHALE = "0xF977814e90dA44bFA03b6295A0616a897441aceC";
37+
38+
const UNITROLLER = "0xfD36E2c2a6789Db23113685031d7F16329158384";
39+
40+
const BLOCK_NUMBER = 84228546;
41+
42+
forking(BLOCK_NUMBER, async () => {
43+
let comptroller: Contract;
44+
let vUSDT: Contract;
45+
let usdt: Contract;
46+
let vUSDC: Contract;
47+
let usdc: Contract;
48+
49+
before(async () => {
50+
comptroller = new ethers.Contract(UNITROLLER, COMPTROLLER_ABI, ethers.provider);
51+
vUSDT = new ethers.Contract(VUSDT_ADDRESS, VTOKEN_ABI, ethers.provider);
52+
usdt = new ethers.Contract(USDT_ADDRESS, ERC20_ABI, ethers.provider);
53+
vUSDC = new ethers.Contract(VUSDC_ADDRESS, VTOKEN_ABI, ethers.provider);
54+
usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, ethers.provider);
55+
56+
for (const market of CORE_MARKETS) {
57+
await setMaxStalePeriodInChainlinkOracle(
58+
bscmainnet.CHAINLINK_ORACLE,
59+
market.underlying,
60+
ethers.constants.AddressZero,
61+
bscmainnet.NORMAL_TIMELOCK,
62+
315360000,
63+
);
64+
65+
await setMaxStalePeriodInChainlinkOracle(
66+
bscmainnet.REDSTONE_ORACLE,
67+
market.underlying,
68+
ethers.constants.AddressZero,
69+
bscmainnet.NORMAL_TIMELOCK,
70+
315360000,
71+
);
72+
await setMaxStalePeriodInBinanceOracle(bscmainnet.BINANCE_ORACLE, market.symbol.slice(1), 315360000);
73+
}
74+
75+
const xSolvBTC = "0x1346b618dC92810EC74163e4c27004c921D446a5";
76+
const xSolvBTC_RedStone_Feed = "0x24c8964338Deb5204B096039147B8e8C3AEa42Cc";
77+
await setRedstonePrice(bscmainnet.REDSTONE_ORACLE, xSolvBTC, xSolvBTC_RedStone_Feed, bscmainnet.NORMAL_TIMELOCK);
78+
79+
const THE = "0xF4C8E32EaDEC4BFe97E0F595AdD0f4450a863a11";
80+
const THE_REDSTONE_FEED = "0xFB1267A29C0aa19daae4a483ea895862A69e4AA5";
81+
await setRedstonePrice(bscmainnet.REDSTONE_ORACLE, THE, THE_REDSTONE_FEED, bscmainnet.NORMAL_TIMELOCK);
82+
83+
const TRX = "0xCE7de646e7208a4Ef112cb6ed5038FA6cC6b12e3";
84+
const TRX_REDSTONE_FEED = "0xa17362dd9AD6d0aF646D7C8f8578fddbfc90B916";
85+
await setRedstonePrice(bscmainnet.REDSTONE_ORACLE, TRX, TRX_REDSTONE_FEED, bscmainnet.NORMAL_TIMELOCK, 3153600000, {
86+
tokenDecimals: 6,
87+
});
88+
});
89+
90+
describe("Pre-VIP state", async () => {
91+
it("markets should have old implementation", async () => {
92+
for (const market of CORE_MARKETS) {
93+
const marketContract = await ethers.getContractAt(VBEP20_DELEGATOR_ABI, market.address);
94+
const currentImpl = await marketContract.implementation();
95+
expect(currentImpl).to.not.equal(NEW_VBEP20_DELEGATE_IMPL);
96+
}
97+
});
98+
});
99+
100+
testVip("VIP-597 Mainnet", await vip597(), {
101+
callbackAfterExecution: async (txResponse: TransactionResponse) => {
102+
const totalMarkets = CORE_MARKETS.length;
103+
await expectEvents(txResponse, [VBEP20_DELEGATOR_ABI], ["NewImplementation"], [totalMarkets]);
104+
},
105+
});
106+
107+
describe("Post-VIP state", async () => {
108+
it("markets should have new implementation", async () => {
109+
for (const market of CORE_MARKETS) {
110+
const marketContract = await ethers.getContractAt(VBEP20_DELEGATOR_ABI, market.address);
111+
expect(await marketContract.implementation()).to.equal(NEW_VBEP20_DELEGATE_IMPL);
112+
}
113+
});
114+
});
115+
116+
describe("Core operations: mint, borrow, repay, redeem", async () => {
117+
let user: any;
118+
let userAddress: string;
119+
120+
before(async () => {
121+
const usdcWhale = await initMainnetUser(USDC_WHALE, ethers.utils.parseEther("1"));
122+
const usdtWhale = await initMainnetUser(USDT_WHALE, ethers.utils.parseEther("1"));
123+
124+
const [signer] = await ethers.getSigners();
125+
user = signer;
126+
userAddress = await user.getAddress();
127+
128+
await usdc.connect(usdcWhale).transfer(userAddress, parseUnits("10000", 18));
129+
await usdt.connect(usdtWhale).transfer(userAddress, parseUnits("10000", 18));
130+
});
131+
132+
it("should mint (supply) USDC to vUSDC", async () => {
133+
const mintAmount = parseUnits("5000", 18);
134+
await usdc.connect(user).approve(VUSDC_ADDRESS, mintAmount);
135+
await vUSDC.connect(user).mint(mintAmount);
136+
137+
const vUSDCBalance = await vUSDC.balanceOf(userAddress);
138+
expect(vUSDCBalance).to.be.gt(0);
139+
});
140+
141+
it("should borrow USDT against USDC collateral", async () => {
142+
await comptroller.connect(user).enterMarkets([VUSDC_ADDRESS]);
143+
144+
const borrowAmount = parseUnits("1000", 18);
145+
const usdtBefore = await usdt.balanceOf(userAddress);
146+
await vUSDT.connect(user).borrow(borrowAmount);
147+
const usdtAfter = await usdt.balanceOf(userAddress);
148+
149+
expect(usdtAfter.sub(usdtBefore)).to.equal(borrowAmount);
150+
});
151+
152+
it("should repay partial USDT borrow", async () => {
153+
const borrowBefore = await vUSDT.callStatic.borrowBalanceCurrent(userAddress);
154+
expect(borrowBefore).to.be.gt(0);
155+
156+
const repayAmount = parseUnits("500", 18);
157+
await usdt.connect(user).approve(VUSDT_ADDRESS, repayAmount);
158+
await vUSDT.connect(user).repayBorrow(repayAmount);
159+
160+
const borrowAfter = await vUSDT.callStatic.borrowBalanceCurrent(userAddress);
161+
expect(borrowAfter).to.be.lt(borrowBefore);
162+
});
163+
164+
it("should fully repay using type(uint256).max", async () => {
165+
const borrowBefore = await vUSDT.callStatic.borrowBalanceCurrent(userAddress);
166+
expect(borrowBefore).to.be.gt(0);
167+
168+
await usdt.connect(user).approve(VUSDT_ADDRESS, ethers.constants.MaxUint256);
169+
await vUSDT.connect(user).repayBorrow(ethers.constants.MaxUint256);
170+
171+
const borrowAfter = await vUSDT.callStatic.borrowBalanceCurrent(userAddress);
172+
expect(borrowAfter).to.equal(0);
173+
});
174+
175+
it("should redeem USDC from vUSDC", async () => {
176+
const usdcBefore = await usdc.balanceOf(userAddress);
177+
const redeemAmount = parseUnits("1000", 18);
178+
await vUSDC.connect(user).redeemUnderlying(redeemAmount);
179+
const usdcAfter = await usdc.balanceOf(userAddress);
180+
181+
expect(usdcAfter.sub(usdcBefore)).to.equal(redeemAmount);
182+
});
183+
});
184+
185+
describe("Repay logic change: caps repayment to actual borrow balance", async () => {
186+
let user: any;
187+
let userAddress: string;
188+
189+
before(async () => {
190+
const usdcWhale = await initMainnetUser(USDC_WHALE, ethers.utils.parseEther("1"));
191+
const usdtWhale = await initMainnetUser(USDT_WHALE, ethers.utils.parseEther("1"));
192+
193+
const signers = await ethers.getSigners();
194+
user = signers[1];
195+
userAddress = await user.getAddress();
196+
197+
await usdc.connect(usdcWhale).transfer(userAddress, parseUnits("10000", 18));
198+
await usdt.connect(usdtWhale).transfer(userAddress, parseUnits("10000", 18));
199+
200+
// Supply collateral and borrow
201+
await usdc.connect(user).approve(VUSDC_ADDRESS, parseUnits("5000", 18));
202+
await vUSDC.connect(user).mint(parseUnits("5000", 18));
203+
await comptroller.connect(user).enterMarkets([VUSDC_ADDRESS]);
204+
await vUSDT.connect(user).borrow(parseUnits("100", 18));
205+
});
206+
207+
it("should cap repayment when amount exceeds borrow balance", async () => {
208+
const borrowBalance: BigNumber = await vUSDT.callStatic.borrowBalanceCurrent(userAddress);
209+
expect(borrowBalance).to.be.gt(0);
210+
211+
// Approve and repay 2x the borrow balance — should be capped to actual debt
212+
const excessAmount = borrowBalance.mul(2);
213+
const usdtBefore = await usdt.balanceOf(userAddress);
214+
215+
await usdt.connect(user).approve(VUSDT_ADDRESS, excessAmount);
216+
await vUSDT.connect(user).repayBorrow(excessAmount);
217+
218+
const borrowAfter = await vUSDT.callStatic.borrowBalanceCurrent(userAddress);
219+
expect(borrowAfter).to.equal(0);
220+
221+
// Verify only the actual borrow amount was taken, not the excess
222+
const usdtAfter = await usdt.balanceOf(userAddress);
223+
const actualRepaid = usdtBefore.sub(usdtAfter);
224+
225+
// actualRepaid should be close to borrowBalance (within interest accrual tolerance)
226+
const tolerance = borrowBalance.mul(1).div(100); // 1% tolerance for interest
227+
expect(actualRepaid).to.be.closeTo(borrowBalance, tolerance);
228+
});
229+
});
230+
231+
describe("repayBorrowBehalf: third-party repay (SwapRouter path)", async () => {
232+
let borrower: any;
233+
let borrowerAddress: string;
234+
let repayer: any;
235+
let repayerAddress: string;
236+
237+
before(async () => {
238+
const usdcWhale = await initMainnetUser(USDC_WHALE, ethers.utils.parseEther("1"));
239+
const usdtWhale = await initMainnetUser(USDT_WHALE, ethers.utils.parseEther("1"));
240+
241+
const signers = await ethers.getSigners();
242+
borrower = signers[4];
243+
borrowerAddress = await borrower.getAddress();
244+
repayer = signers[5];
245+
repayerAddress = await repayer.getAddress();
246+
247+
await usdc.connect(usdcWhale).transfer(borrowerAddress, parseUnits("5000", 18));
248+
await usdt.connect(usdtWhale).transfer(repayerAddress, parseUnits("10000", 18));
249+
250+
// Borrower: supply USDC and borrow USDT
251+
await usdc.connect(borrower).approve(VUSDC_ADDRESS, parseUnits("5000", 18));
252+
await vUSDC.connect(borrower).mint(parseUnits("5000", 18));
253+
await comptroller.connect(borrower).enterMarkets([VUSDC_ADDRESS]);
254+
await vUSDT.connect(borrower).borrow(parseUnits("100", 18));
255+
});
256+
257+
it("repayBorrowBehalf should work with exact amount", async () => {
258+
const borrowBalance: BigNumber = await vUSDT.callStatic.borrowBalanceCurrent(borrowerAddress);
259+
expect(borrowBalance).to.be.gt(0);
260+
261+
const repayAmount = parseUnits("50", 18);
262+
await usdt.connect(repayer).approve(VUSDT_ADDRESS, repayAmount);
263+
await vUSDT.connect(repayer).repayBorrowBehalf(borrowerAddress, repayAmount);
264+
265+
const borrowAfter = await vUSDT.callStatic.borrowBalanceCurrent(borrowerAddress);
266+
expect(borrowAfter).to.be.lt(borrowBalance);
267+
});
268+
269+
it("repayBorrowBehalf should cap when amount exceeds borrow balance", async () => {
270+
const borrowBalance: BigNumber = await vUSDT.callStatic.borrowBalanceCurrent(borrowerAddress);
271+
expect(borrowBalance).to.be.gt(0);
272+
273+
// Repayer sends 2x the borrow balance — should be capped
274+
const excessAmount = borrowBalance.mul(2);
275+
const usdtBefore = await usdt.balanceOf(repayerAddress);
276+
277+
await usdt.connect(repayer).approve(VUSDT_ADDRESS, excessAmount);
278+
await vUSDT.connect(repayer).repayBorrowBehalf(borrowerAddress, excessAmount);
279+
280+
const borrowAfter = await vUSDT.callStatic.borrowBalanceCurrent(borrowerAddress);
281+
expect(borrowAfter).to.equal(0);
282+
283+
// Verify only the actual borrow amount was taken from repayer
284+
const usdtAfter = await usdt.balanceOf(repayerAddress);
285+
const actualRepaid = usdtBefore.sub(usdtAfter);
286+
287+
const tolerance = borrowBalance.mul(1).div(100); // 1% tolerance for interest
288+
expect(actualRepaid).to.be.closeTo(borrowBalance, tolerance);
289+
});
290+
});
291+
292+
describe("Liquidation path", async () => {
293+
let borrower: any;
294+
let borrowerAddress: string;
295+
let liquidator: any;
296+
let liquidatorAddress: string;
297+
298+
before(async () => {
299+
const usdcWhale = await initMainnetUser(USDC_WHALE, ethers.utils.parseEther("1"));
300+
const usdtWhale = await initMainnetUser(USDT_WHALE, ethers.utils.parseEther("1"));
301+
302+
const signers = await ethers.getSigners();
303+
borrower = signers[2];
304+
borrowerAddress = await borrower.getAddress();
305+
liquidator = signers[3];
306+
liquidatorAddress = await liquidator.getAddress();
307+
308+
await usdc.connect(usdcWhale).transfer(borrowerAddress, parseUnits("1000", 18));
309+
await usdt.connect(usdtWhale).transfer(liquidatorAddress, parseUnits("10000", 18));
310+
311+
// Borrower: supply USDC collateral and borrow USDT near max
312+
await usdc.connect(borrower).approve(VUSDC_ADDRESS, parseUnits("1000", 18));
313+
await vUSDC.connect(borrower).mint(parseUnits("1000", 18));
314+
await comptroller.connect(borrower).enterMarkets([VUSDC_ADDRESS]);
315+
await vUSDT.connect(borrower).borrow(parseUnits("750", 18));
316+
});
317+
318+
it("liquidateBorrow should work with the new implementation", async () => {
319+
// Advance blocks to accrue interest and push borrower into shortfall
320+
for (let i = 0; i < 100000; i++) {
321+
await ethers.provider.send("evm_mine", []);
322+
}
323+
await vUSDT.connect(liquidator).accrueInterest();
324+
325+
const [, , shortfall] = await comptroller.getAccountLiquidity(borrowerAddress);
326+
327+
if (shortfall.gt(0)) {
328+
const borrowBalance = await vUSDT.callStatic.borrowBalanceCurrent(borrowerAddress);
329+
const repayAmount = borrowBalance.div(2);
330+
331+
await usdt.connect(liquidator).approve(VUSDT_ADDRESS, repayAmount);
332+
await vUSDT.connect(liquidator).liquidateBorrow(borrowerAddress, repayAmount, VUSDC_ADDRESS);
333+
334+
const borrowAfter = await vUSDT.callStatic.borrowBalanceCurrent(borrowerAddress);
335+
expect(borrowAfter).to.be.lt(borrowBalance);
336+
} else {
337+
console.log("Borrower not yet in shortfall after mining blocks, skipping liquidation test");
338+
}
339+
});
340+
});
341+
342+
describe("generic tests", async () => {
343+
checkCorePoolComptroller({
344+
account: GENERIC_ETH_ACCOUNT,
345+
lens: COMPTROLLER_LENS,
346+
});
347+
});
348+
});

simulations/vip-597/bsctestnet.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { TransactionResponse } from "@ethersproject/providers";
2+
import { expect } from "chai";
3+
import { ethers } from "hardhat";
4+
import { expectEvents } from "src/utils";
5+
import { forking, testVip } from "src/vip-framework";
6+
7+
import vip597Testnet, { CORE_MARKETS, NEW_VBEP20_DELEGATE_IMPL } from "../../vips/vip-597/bsctestnet";
8+
import VBEP20_DELEGATOR_ABI from "./abi/VBep20Delegator.json";
9+
10+
const BLOCK_NUMBER = 93362756;
11+
12+
forking(BLOCK_NUMBER, async () => {
13+
describe("Pre-VIP state", async () => {
14+
it("markets should have old implementation", async () => {
15+
for (const market of CORE_MARKETS) {
16+
const marketContract = await ethers.getContractAt(VBEP20_DELEGATOR_ABI, market.address);
17+
const currentImpl = await marketContract.implementation();
18+
expect(currentImpl).to.not.equal(NEW_VBEP20_DELEGATE_IMPL);
19+
}
20+
});
21+
});
22+
23+
testVip("VIP-597 testnet", await vip597Testnet(), {
24+
callbackAfterExecution: async (txResponse: TransactionResponse) => {
25+
const totalMarkets = CORE_MARKETS.length;
26+
await expectEvents(txResponse, [VBEP20_DELEGATOR_ABI], ["NewImplementation"], [totalMarkets]);
27+
},
28+
});
29+
30+
describe("Post-VIP state", async () => {
31+
it("markets should have new implementation", async () => {
32+
for (const market of CORE_MARKETS) {
33+
const marketContract = await ethers.getContractAt(VBEP20_DELEGATOR_ABI, market.address);
34+
expect(await marketContract.implementation()).to.equal(NEW_VBEP20_DELEGATE_IMPL);
35+
}
36+
});
37+
});
38+
});

0 commit comments

Comments
 (0)