From ca3c34e288efcd15f9577d2929cfc0b9ac5cdb3f Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Fri, 29 May 2026 17:13:29 +0530 Subject: [PATCH 1/7] feat: add PrimeV2 and PrimeLeaderboard testnet setup Bring PrimeV2 + PrimeLeaderboard live on bsctestnet: accept ownership, grant ACM perms (epoch ops to NormalTimelock + Guardian), wire the pair, repoint PrimeLiquidityProvider, add Core pool markets, open the mint window, and pause the legacy Prime. Includes fork simulation. --- simulations/vip-675/bsctestnet.ts | 145 ++++++++++++++++++++++ vips/vip-675/bsctestnet.ts | 194 ++++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 simulations/vip-675/bsctestnet.ts create mode 100644 vips/vip-675/bsctestnet.ts diff --git a/simulations/vip-675/bsctestnet.ts b/simulations/vip-675/bsctestnet.ts new file mode 100644 index 000000000..5bb3a0bfd --- /dev/null +++ b/simulations/vip-675/bsctestnet.ts @@ -0,0 +1,145 @@ +import { expect } from "chai"; +import { Contract, constants } from "ethers"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { forking, testVip } from "src/vip-framework"; + +import vip675, { + LEGACY_PRIME, + MINT_DEADLINE, + MINT_THRESHOLD, + PLP, + PRIME_LEADERBOARD, + PRIME_MARKETS, + PRIME_V2, +} from "../../vips/vip-675/bsctestnet"; + +const { bsctestnet } = NETWORK_ADDRESSES; +const ACM_ABI = ["function hasRole(bytes32 role, address account) view returns (bool)"]; + +// Minimal inline ABIs +const PRIME_V2_ABI = [ + "function owner() view returns (address)", + "function pendingOwner() view returns (address)", + "function primeLeaderboard() view returns (address)", + "function tokenLimit() view returns (uint256)", + "function mintThreshold() view returns (uint256)", + "function mintDeadline() view returns (uint256)", + "function markets(address) view returns (uint256 supplyMultiplier, uint256 borrowMultiplier, uint256 rewardIndex, uint256 sumOfMembersScore, bool exists)", + "event MarketAdded(address indexed market, uint256 supplyMultiplier, uint256 borrowMultiplier)", + "event MintThresholdUpdated(uint256 oldThreshold, uint256 newThreshold, uint256 deadline)", + "event PrimeLeaderboardSet(address indexed oldLeaderboard, address indexed newLeaderboard)", +]; +const PRIME_LEADERBOARD_ABI = [ + "function owner() view returns (address)", + "function pendingOwner() view returns (address)", + "function primeV2() view returns (address)", +]; +const PLP_ABI = ["function prime() view returns (address)"]; +const LEGACY_PRIME_ABI = ["function paused() view returns (bool)"]; + +const BLOCK_NUMBER = 110244560; + +forking(BLOCK_NUMBER, async () => { + let primeV2: Contract; + let primeLeaderboard: Contract; + let plp: Contract; + let legacyPrime: Contract; + let acm: Contract; + + before(async () => { + primeV2 = new ethers.Contract(PRIME_V2, PRIME_V2_ABI, ethers.provider); + primeLeaderboard = new ethers.Contract(PRIME_LEADERBOARD, PRIME_LEADERBOARD_ABI, ethers.provider); + plp = new ethers.Contract(PLP, PLP_ABI, ethers.provider); + legacyPrime = new ethers.Contract(LEGACY_PRIME, LEGACY_PRIME_ABI, ethers.provider); + acm = new ethers.Contract(bsctestnet.ACCESS_CONTROL_MANAGER, ACM_ABI, ethers.provider); + }); + + const roleFor = (target: string, signature: string) => + ethers.utils.solidityKeccak256(["address", "string"], [target, signature]); + + describe("Pre-VIP behavior", () => { + it("PrimeV2 ownership pending on NormalTimelock (not accepted)", async () => { + expect(await primeV2.pendingOwner()).to.equal(bsctestnet.NORMAL_TIMELOCK); + }); + + it("PrimeLeaderboard ownership pending on NormalTimelock (not accepted)", async () => { + expect(await primeLeaderboard.pendingOwner()).to.equal(bsctestnet.NORMAL_TIMELOCK); + }); + + it("PLP prime token is not yet PrimeV2", async () => { + expect(await plp.prime()).to.not.equal(PRIME_V2); + }); + + it("legacy Prime is active (unpaused)", async () => { + expect(await legacyPrime.paused()).to.equal(false); + }); + }); + + testVip("VIP-675 [Testnet] PrimeV2 + PrimeLeaderboard setup", await vip675(), { + callbackAfterExecution: async txResponse => { + await expect(txResponse) + .to.emit(primeV2, "PrimeLeaderboardSet") + .withArgs(constants.AddressZero, PRIME_LEADERBOARD); + await expect(txResponse).to.emit(primeV2, "MintThresholdUpdated").withArgs(0, MINT_THRESHOLD, MINT_DEADLINE); + for (const market of PRIME_MARKETS) { + await expect(txResponse) + .to.emit(primeV2, "MarketAdded") + .withArgs(market.vToken, market.supplyMultiplier, market.borrowMultiplier); + } + }, + }); + + describe("Post-VIP behavior", () => { + it("PrimeV2 owner is the NormalTimelock", async () => { + expect(await primeV2.owner()).to.equal(bsctestnet.NORMAL_TIMELOCK); + }); + + it("PrimeLeaderboard owner is the NormalTimelock", async () => { + expect(await primeLeaderboard.owner()).to.equal(bsctestnet.NORMAL_TIMELOCK); + }); + + it("PLP points at PrimeV2", async () => { + expect(await plp.prime()).to.equal(PRIME_V2); + }); + + it("PrimeV2 <-> PrimeLeaderboard are wired", async () => { + expect(await primeV2.primeLeaderboard()).to.equal(PRIME_LEADERBOARD); + expect(await primeLeaderboard.primeV2()).to.equal(PRIME_V2); + }); + + it("PrimeV2 token limit is 500", async () => { + expect(await primeV2.tokenLimit()).to.equal(500); + }); + + it("PrimeV2 mint window is configured", async () => { + expect(await primeV2.mintThreshold()).to.equal(MINT_THRESHOLD); + expect(await primeV2.mintDeadline()).to.equal(MINT_DEADLINE); + }); + + it("Guardian can call issue / burn / setMintThreshold on PrimeV2", async () => { + for (const sig of ["issue(address)", "burn(address)", "setMintThreshold(uint256,uint256)"]) { + expect(await acm.hasRole(roleFor(PRIME_V2, sig), bsctestnet.GUARDIAN)).to.equal(true); + } + }); + + it("Guardian can seed stakers on PrimeLeaderboard", async () => { + for (const sig of ["initializeStakers(address[],uint256[],uint64[])", "finalizeInitialization()"]) { + expect(await acm.hasRole(roleFor(PRIME_LEADERBOARD, sig), bsctestnet.GUARDIAN)).to.equal(true); + } + }); + + it("legacy Prime is decommissioned (paused)", async () => { + expect(await legacyPrime.paused()).to.equal(true); + }); + + for (const market of PRIME_MARKETS) { + it(`market ${market.vToken} is configured on PrimeV2`, async () => { + const m = await primeV2.markets(market.vToken); + expect(m.exists).to.equal(true); + expect(m.supplyMultiplier).to.equal(market.supplyMultiplier); + expect(m.borrowMultiplier).to.equal(market.borrowMultiplier); + }); + } + }); +}); diff --git a/vips/vip-675/bsctestnet.ts b/vips/vip-675/bsctestnet.ts new file mode 100644 index 000000000..4fdabae81 --- /dev/null +++ b/vips/vip-675/bsctestnet.ts @@ -0,0 +1,194 @@ +import { parseUnits } from "ethers/lib/utils"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +const { bsctestnet } = NETWORK_ADDRESSES; + +const ACM = bsctestnet.ACCESS_CONTROL_MANAGER; +const NORMAL_TIMELOCK = bsctestnet.NORMAL_TIMELOCK; +const FAST_TRACK_TIMELOCK = bsctestnet.FAST_TRACK_TIMELOCK; +const CRITICAL_TIMELOCK = bsctestnet.CRITICAL_TIMELOCK; +const GUARDIAN = bsctestnet.GUARDIAN; + +// Deployed via venus-protocol PR #677 (feat/VPD-1313). On live networks the deploy script +// initiates transferOwnership of both contracts to the NormalTimelock (pending acceptance), +// but does NOT wire them — the setPrimeV2 / setPrimeLeaderboard wiring is ACM-gated and done +// here. This VIP accepts ownership, grants ACM permissions, wires PrimeV2 <-> PrimeLeaderboard, +// configures the Prime markets, opens the mint window, and pauses the legacy Prime. +export const PRIME_V2 = "0x878e6B88f8F9e85c88bb21396A7637330b9Cd5Ec"; +export const PRIME_LEADERBOARD = "0x45E9b8A46558c359b6Ee30580A599AAa1e5d9cDE"; + +// Existing contracts reused by PrimeV2 +export const PLP = "0xAdeddc73eAFCbed174e6C400165b111b0cb80B7E"; // PrimeLiquidityProvider (existing) +export const LEGACY_PRIME = "0xe840F8EC2Dc50E7D22e5e2991975b9F6e34b62Ad"; // current Prime (to be replaced) + +// Permissionless mint window config for PrimeV2.setMintThreshold(mintThreshold, mintDeadline) +export const MINT_THRESHOLD = parseUnits("1", 18).toString(); // 1 XVS effective stake (testnet) +export const MINT_DEADLINE = "0"; // 0 = no expiry + +// Prime markets on the Core pool (bsctestnet) — mirrors the legacy Prime markets +// and their supply/borrow multipliers (read from the legacy Prime at LEGACY_PRIME). +const VUSDT = "0xb7526572FFE56AB9D7489838Bf2E18e3323b441A"; +const VUSDC = "0xD5C4C2e2facBEB59D0216D0595d63FcDc6F9A1a7"; +const VBTC = "0xb6e9322C49FD75a367Fcb17B0Fcd62C5070EbCBe"; +const VETH = "0x162D005F0Fff510E54958Cfc5CF32A3180A84aab"; + +interface PrimeMarket { + vToken: string; + supplyMultiplier: string; + borrowMultiplier: string; +} + +export const PRIME_MARKETS: PrimeMarket[] = [ + { vToken: VUSDT, supplyMultiplier: parseUnits("2", 18).toString(), borrowMultiplier: "0" }, + { vToken: VUSDC, supplyMultiplier: parseUnits("2", 18).toString(), borrowMultiplier: "0" }, + { vToken: VBTC, supplyMultiplier: parseUnits("2", 18).toString(), borrowMultiplier: parseUnits("4", 18).toString() }, + { vToken: VETH, supplyMultiplier: parseUnits("2", 18).toString(), borrowMultiplier: parseUnits("4", 18).toString() }, +]; + +const ALL_TIMELOCKS = [NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; + +// Grant a single ACM permission for `target.signature` to every timelock in `accounts`. +const grant = (target: string, signature: string, accounts: string[] = [NORMAL_TIMELOCK]) => + accounts.map(account => ({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [target, signature, account], + })); + +// The off-chain admin (Guardian) runs the epoch operations directly (ranking + issue/burn, +// mint-threshold updates, and the one-time staker seeding), so it gets those permissions in +// addition to the NormalTimelock. +const KEEPER_ACCOUNTS = [NORMAL_TIMELOCK, GUARDIAN]; + +// ACM-gated functions on PrimeV2 (see PrimeV2.sol _checkAccessAllowed) +const PRIME_V2_PERMISSIONS = [ + ...grant(PRIME_V2, "issue(address)", KEEPER_ACCOUNTS), + ...grant(PRIME_V2, "issueBatch(address[])", KEEPER_ACCOUNTS), + ...grant(PRIME_V2, "burn(address)", KEEPER_ACCOUNTS), + ...grant(PRIME_V2, "burnBatch(address[])", KEEPER_ACCOUNTS), + ...grant(PRIME_V2, "setPrimeLeaderboard(address)"), + ...grant(PRIME_V2, "addMarket(address,uint256,uint256)"), + ...grant(PRIME_V2, "removeMarket(address)"), + ...grant(PRIME_V2, "setLimit(uint256)"), + ...grant(PRIME_V2, "updateAlpha(uint128,uint128)"), + ...grant(PRIME_V2, "updateMultipliers(address,uint256,uint256)"), + ...grant(PRIME_V2, "setMaxLoopsLimit(uint256)"), + ...grant(PRIME_V2, "setMintThreshold(uint256,uint256)", KEEPER_ACCOUNTS), + ...grant(PRIME_V2, "pause()", ALL_TIMELOCKS), + ...grant(PRIME_V2, "unpause()", ALL_TIMELOCKS), +]; + +// ACM-gated functions on PrimeLeaderboard (see PrimeLeaderboard.sol _checkAccessAllowed) +const PRIME_LEADERBOARD_PERMISSIONS = [ + ...grant(PRIME_LEADERBOARD, "initializeStakers(address[],uint256[],uint64[])", KEEPER_ACCOUNTS), + ...grant(PRIME_LEADERBOARD, "finalizeInitialization()", KEEPER_ACCOUNTS), + ...grant(PRIME_LEADERBOARD, "setMultiplierTiers(uint256[],uint256[])"), + ...grant(PRIME_LEADERBOARD, "setPrimeV2(address)"), + ...grant(PRIME_LEADERBOARD, "setMaxLoopsLimit(uint256)"), +]; + +const vip675 = () => { + const meta = { + version: "v2", + title: "VIP-675 [Testnet] Deploy and configure PrimeV2 and PrimeLeaderboard", + description: `#### Summary + +If passed, this VIP will bring the new PrimeV2 and PrimeLeaderboard contracts live on BNB Chain testnet: it accepts their ownership, grants the required ACM permissions, wires the two contracts together, points the existing PrimeLiquidityProvider at PrimeV2, configures the Prime markets, opens the permissionless mint window, and pauses the legacy Prime. + +#### Description + +If passed, this VIP will: + +- Accept ownership of PrimeV2 and PrimeLeaderboard (previously transferred to the Normal Timelock). +- Grant ACM permissions: configuration functions to the Normal Timelock; the epoch operations (issue/issueBatch/burn/burnBatch and setMintThreshold on PrimeV2, and initializeStakers/finalizeInitialization on PrimeLeaderboard) to both the Normal Timelock and the Guardian; pause/unpause to all three timelocks. +- Wire the contracts together by setting PrimeLeaderboard on PrimeV2 and PrimeV2 on PrimeLeaderboard. +- Point the existing PrimeLiquidityProvider at PrimeV2 so Prime rewards accrue to the new contract. +- Add the Core pool markets to PrimeV2 (vUSDT, vUSDC, vBTC, vETH) with the same supply/borrow multipliers used by the legacy Prime. +- Open the permissionless mint window via setMintThreshold (minimum effective stake of 1 XVS, no deadline). +- Pause the legacy Prime to decommission it. + +The leaderboard multiplier tiers (30/60/90 days mapping to 1.3x/1.6x/2.0x) and the PrimeV2 token limit (500) are set in the contracts' initializers, so they are not re-set here. Seeding existing stakers into PrimeLeaderboard (initializeStakers + finalizeInitialization) is performed off-chain by the Guardian using a staker snapshot built from XVS vault history. + +#### References + +- PrimeV2 / PrimeLeaderboard implementation: https://github.com/VenusProtocol/venus-protocol/pull/676 +- PrimeV2 / PrimeLeaderboard testnet deployments: https://github.com/VenusProtocol/venus-protocol/pull/677`, + forDescription: "I agree that Venus Protocol should proceed with this proposal", + againstDescription: "I do not think that Venus Protocol should proceed with this proposal", + abstainDescription: "I am indifferent to whether Venus Protocol proceeds or not", + }; + + return makeProposal( + [ + // 1. Accept ownership (deploy script transferred ownership to NormalTimelock) + { + target: PRIME_V2, + signature: "acceptOwnership()", + params: [], + }, + { + target: PRIME_LEADERBOARD, + signature: "acceptOwnership()", + params: [], + }, + + // 2. Grant ACM permissions + ...PRIME_V2_PERMISSIONS, + ...PRIME_LEADERBOARD_PERMISSIONS, + + // 3. Wire PrimeV2 <-> PrimeLeaderboard (ACM-gated; deploy does NOT wire on live networks) + { + target: PRIME_V2, + signature: "setPrimeLeaderboard(address)", + params: [PRIME_LEADERBOARD], + }, + { + target: PRIME_LEADERBOARD, + signature: "setPrimeV2(address)", + params: [PRIME_V2], + }, + + // 4. Point the existing PrimeLiquidityProvider at PrimeV2 (onlyOwner = NormalTimelock) + { + target: PLP, + signature: "setPrimeToken(address)", + params: [PRIME_V2], + }, + + // 5. Configure PrimeV2 markets (supply/borrow multipliers mirror the legacy Prime) + ...PRIME_MARKETS.map(market => ({ + target: PRIME_V2, + signature: "addMarket(address,uint256,uint256)", + params: [market.vToken, market.supplyMultiplier, market.borrowMultiplier], + })), + + // 6. Open the permissionless mint window (mintThreshold must be > 0; reverts + // MintThresholdNotSet while 0). Second param is the mint deadline (unix ts, 0 = no expiry). + { + target: PRIME_V2, + signature: "setMintThreshold(uint256,uint256)", + params: [MINT_THRESHOLD, MINT_DEADLINE], + }, + + // Note: PrimeLeaderboard staker seeding (initializeStakers + finalizeInitialization) is + // NOT done in this VIP. The full staker snapshot (addresses, amounts, timestamps) must be + // built off-chain from XVS vault logs and submitted in batches by the Guardian, which holds + // the initializeStakers / finalizeInitialization ACM permissions granted above. + + // 7. Decommission the legacy Prime: pause it (halts claim / score updates / issuance). + // NormalTimelock already holds the togglePause ACM permission, so no grant is needed. + // Legacy Prime is currently unpaused, so a single togglePause pauses it. + { + target: LEGACY_PRIME, + signature: "togglePause()", + params: [], + }, + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip675; From 47310d84e333a4b07a83294b77a3cbb5dd3a43ba Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Mon, 1 Jun 2026 19:26:48 +0530 Subject: [PATCH 2/7] fix: address PR review comments Add missing XVSVault.setPrimeToken (-> PrimeLeaderboard) and Comptroller.setPrimeToken (-> PrimeV2); extend sim assertions and fix grant helper comment. --- simulations/vip-675/bsctestnet.ts | 54 +++++++++++++++++++++++++++++-- vips/vip-675/bsctestnet.ts | 32 +++++++++++++++--- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/simulations/vip-675/bsctestnet.ts b/simulations/vip-675/bsctestnet.ts index 5bb3a0bfd..cec651373 100644 --- a/simulations/vip-675/bsctestnet.ts +++ b/simulations/vip-675/bsctestnet.ts @@ -5,6 +5,7 @@ import { NETWORK_ADDRESSES } from "src/networkAddresses"; import { forking, testVip } from "src/vip-framework"; import vip675, { + COMPTROLLER, LEGACY_PRIME, MINT_DEADLINE, MINT_THRESHOLD, @@ -12,10 +13,16 @@ import vip675, { PRIME_LEADERBOARD, PRIME_MARKETS, PRIME_V2, + XVS_VAULT, } from "../../vips/vip-675/bsctestnet"; const { bsctestnet } = NETWORK_ADDRESSES; -const ACM_ABI = ["function hasRole(bytes32 role, address account) view returns (bool)"]; +const ACM_ABI = [ + "function hasRole(bytes32 role, address account) view returns (bool)", + "event PermissionGranted(address account, address contractAddress, string functionSig)", +]; +const VAULT_ABI = ["function primeToken() view returns (address)"]; +const COMPTROLLER_ABI = ["function prime() view returns (address)"]; // Minimal inline ABIs const PRIME_V2_ABI = [ @@ -46,6 +53,8 @@ forking(BLOCK_NUMBER, async () => { let plp: Contract; let legacyPrime: Contract; let acm: Contract; + let xvsVault: Contract; + let comptroller: Contract; before(async () => { primeV2 = new ethers.Contract(PRIME_V2, PRIME_V2_ABI, ethers.provider); @@ -53,6 +62,8 @@ forking(BLOCK_NUMBER, async () => { plp = new ethers.Contract(PLP, PLP_ABI, ethers.provider); legacyPrime = new ethers.Contract(LEGACY_PRIME, LEGACY_PRIME_ABI, ethers.provider); acm = new ethers.Contract(bsctestnet.ACCESS_CONTROL_MANAGER, ACM_ABI, ethers.provider); + xvsVault = new ethers.Contract(XVS_VAULT, VAULT_ABI, ethers.provider); + comptroller = new ethers.Contract(COMPTROLLER, COMPTROLLER_ABI, ethers.provider); }); const roleFor = (target: string, signature: string) => @@ -74,6 +85,14 @@ forking(BLOCK_NUMBER, async () => { it("legacy Prime is active (unpaused)", async () => { expect(await legacyPrime.paused()).to.equal(false); }); + + it("XVSVault prime hook still points at legacy Prime", async () => { + expect(await xvsVault.primeToken()).to.equal(LEGACY_PRIME); + }); + + it("Comptroller prime still points at legacy Prime", async () => { + expect(await comptroller.prime()).to.equal(LEGACY_PRIME); + }); }); testVip("VIP-675 [Testnet] PrimeV2 + PrimeLeaderboard setup", await vip675(), { @@ -87,6 +106,11 @@ forking(BLOCK_NUMBER, async () => { .to.emit(primeV2, "MarketAdded") .withArgs(market.vToken, market.supplyMultiplier, market.borrowMultiplier); } + // ACM emits PermissionGranted for every ACM-gated function granted by this VIP; + // spot-check the Guardian's issue(address) grant on PrimeV2. + await expect(txResponse) + .to.emit(acm, "PermissionGranted") + .withArgs(bsctestnet.GUARDIAN, PRIME_V2, "issue(address)"); }, }); @@ -117,8 +141,14 @@ forking(BLOCK_NUMBER, async () => { expect(await primeV2.mintDeadline()).to.equal(MINT_DEADLINE); }); - it("Guardian can call issue / burn / setMintThreshold on PrimeV2", async () => { - for (const sig of ["issue(address)", "burn(address)", "setMintThreshold(uint256,uint256)"]) { + it("Guardian can call epoch ops on PrimeV2 (issue / burn / setMintThreshold)", async () => { + for (const sig of [ + "issue(address)", + "issueBatch(address[])", + "burn(address)", + "burnBatch(address[])", + "setMintThreshold(uint256,uint256)", + ]) { expect(await acm.hasRole(roleFor(PRIME_V2, sig), bsctestnet.GUARDIAN)).to.equal(true); } }); @@ -129,6 +159,24 @@ forking(BLOCK_NUMBER, async () => { } }); + it("NormalTimelock can configure PrimeLeaderboard (tiers / primeV2 / maxLoops)", async () => { + for (const sig of [ + "setMultiplierTiers(uint256[],uint256[])", + "setPrimeV2(address)", + "setMaxLoopsLimit(uint256)", + ]) { + expect(await acm.hasRole(roleFor(PRIME_LEADERBOARD, sig), bsctestnet.NORMAL_TIMELOCK)).to.equal(true); + } + }); + + it("XVSVault prime hook points at PrimeLeaderboard", async () => { + expect(await xvsVault.primeToken()).to.equal(PRIME_LEADERBOARD); + }); + + it("Comptroller prime points at PrimeV2", async () => { + expect(await comptroller.prime()).to.equal(PRIME_V2); + }); + it("legacy Prime is decommissioned (paused)", async () => { expect(await legacyPrime.paused()).to.equal(true); }); diff --git a/vips/vip-675/bsctestnet.ts b/vips/vip-675/bsctestnet.ts index 4fdabae81..87362debf 100644 --- a/vips/vip-675/bsctestnet.ts +++ b/vips/vip-675/bsctestnet.ts @@ -22,6 +22,10 @@ export const PRIME_LEADERBOARD = "0x45E9b8A46558c359b6Ee30580A599AAa1e5d9cDE"; // Existing contracts reused by PrimeV2 export const PLP = "0xAdeddc73eAFCbed174e6C400165b111b0cb80B7E"; // PrimeLiquidityProvider (existing) export const LEGACY_PRIME = "0xe840F8EC2Dc50E7D22e5e2991975b9F6e34b62Ad"; // current Prime (to be replaced) +export const COMPTROLLER = bsctestnet.UNITROLLER; +export const XVS_VAULT = bsctestnet.XVS_VAULT_PROXY; +export const XVS = bsctestnet.XVS; +export const XVS_VAULT_POOL_ID = 1; // Permissionless mint window config for PrimeV2.setMintThreshold(mintThreshold, mintDeadline) export const MINT_THRESHOLD = parseUnits("1", 18).toString(); // 1 XVS effective stake (testnet) @@ -49,7 +53,8 @@ export const PRIME_MARKETS: PrimeMarket[] = [ const ALL_TIMELOCKS = [NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; -// Grant a single ACM permission for `target.signature` to every timelock in `accounts`. +// Grant ACM permission for `target.signature` to every account in `accounts` +// (defaults to NormalTimelock only). const grant = (target: string, signature: string, accounts: string[] = [NORMAL_TIMELOCK]) => accounts.map(account => ({ target: ACM, @@ -95,18 +100,20 @@ const vip675 = () => { title: "VIP-675 [Testnet] Deploy and configure PrimeV2 and PrimeLeaderboard", description: `#### Summary -If passed, this VIP will bring the new PrimeV2 and PrimeLeaderboard contracts live on BNB Chain testnet: it accepts their ownership, grants the required ACM permissions, wires the two contracts together, points the existing PrimeLiquidityProvider at PrimeV2, configures the Prime markets, opens the permissionless mint window, and pauses the legacy Prime. +If passed, this VIP will bring the new PrimeV2 and PrimeLeaderboard contracts live on BNB Chain testnet: it accepts their ownership, grants the required ACM permissions, wires the two contracts together, points the existing PrimeLiquidityProvider at PrimeV2, configures the Prime markets, opens the permissionless mint window, switches the XVS Vault hook and the Core pool Comptroller from the legacy Prime to the new contracts, and pauses the legacy Prime. #### Description If passed, this VIP will: -- Accept ownership of PrimeV2 and PrimeLeaderboard (previously transferred to the Normal Timelock). +- Accept ownership of PrimeV2 and PrimeLeaderboard (previously transferred to the Normal Timelock by the deploy script). - Grant ACM permissions: configuration functions to the Normal Timelock; the epoch operations (issue/issueBatch/burn/burnBatch and setMintThreshold on PrimeV2, and initializeStakers/finalizeInitialization on PrimeLeaderboard) to both the Normal Timelock and the Guardian; pause/unpause to all three timelocks. - Wire the contracts together by setting PrimeLeaderboard on PrimeV2 and PrimeV2 on PrimeLeaderboard. - Point the existing PrimeLiquidityProvider at PrimeV2 so Prime rewards accrue to the new contract. - Add the Core pool markets to PrimeV2 (vUSDT, vUSDC, vBTC, vETH) with the same supply/borrow multipliers used by the legacy Prime. - Open the permissionless mint window via setMintThreshold (minimum effective stake of 1 XVS, no deadline). +- Switch the XVS Vault prime hook from the legacy Prime to PrimeLeaderboard, so vault deposits/withdrawals update the leaderboard (which in turn calls PrimeV2). Reward token and pool id are unchanged (XVS, pool 1). +- Update the Core pool Comptroller's prime address from the legacy Prime to PrimeV2, so market hooks call the new contract. - Pause the legacy Prime to decommission it. The leaderboard multiplier tiers (30/60/90 days mapping to 1.3x/1.6x/2.0x) and the PrimeV2 token limit (500) are set in the contracts' initializers, so they are not re-set here. Seeding existing stakers into PrimeLeaderboard (initializeStakers + finalizeInitialization) is performed off-chain by the Guardian using a staker snapshot built from XVS vault history. @@ -172,12 +179,29 @@ The leaderboard multiplier tiers (30/60/90 days mapping to 1.3x/1.6x/2.0x) and t params: [MINT_THRESHOLD, MINT_DEADLINE], }, + // 7. Switch the XVS Vault hook from the legacy Prime to PrimeLeaderboard, so vault + // deposit/withdraw events update the leaderboard (which in turn calls PrimeV2). + // setPrimeToken on the vault is onlyAdmin (admin = NormalTimelock). + { + target: XVS_VAULT, + signature: "setPrimeToken(address,address,uint256)", + params: [PRIME_LEADERBOARD, XVS, XVS_VAULT_POOL_ID], + }, + + // 8. Point the Core pool Comptroller at PrimeV2 so market hooks call the new contract. + // setPrimeToken is admin-gated (admin = NormalTimelock); no ACM grant needed. + { + target: COMPTROLLER, + signature: "setPrimeToken(address)", + params: [PRIME_V2], + }, + // Note: PrimeLeaderboard staker seeding (initializeStakers + finalizeInitialization) is // NOT done in this VIP. The full staker snapshot (addresses, amounts, timestamps) must be // built off-chain from XVS vault logs and submitted in batches by the Guardian, which holds // the initializeStakers / finalizeInitialization ACM permissions granted above. - // 7. Decommission the legacy Prime: pause it (halts claim / score updates / issuance). + // 9. Decommission the legacy Prime: pause it (halts claim / score updates / issuance). // NormalTimelock already holds the togglePause ACM permission, so no grant is needed. // Legacy Prime is currently unpaused, so a single togglePause pauses it. { From ef6c7f2c8165f63ba3bac3640945c07901b4ec27 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 2 Jun 2026 12:57:45 +0530 Subject: [PATCH 3/7] chore: tighten VIP-675 testnet sim pre-checks and header comment Add pre-VIP assertions for wiring=0 and mint window=0; tighten PLP prime equality check; mention vault/comptroller switches in header. --- simulations/vip-675/bsctestnet.ts | 16 +++++++++++++--- vips/vip-675/bsctestnet.ts | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/simulations/vip-675/bsctestnet.ts b/simulations/vip-675/bsctestnet.ts index cec651373..532973ad3 100644 --- a/simulations/vip-675/bsctestnet.ts +++ b/simulations/vip-675/bsctestnet.ts @@ -78,8 +78,18 @@ forking(BLOCK_NUMBER, async () => { expect(await primeLeaderboard.pendingOwner()).to.equal(bsctestnet.NORMAL_TIMELOCK); }); - it("PLP prime token is not yet PrimeV2", async () => { - expect(await plp.prime()).to.not.equal(PRIME_V2); + it("PLP prime token is still the legacy Prime", async () => { + expect(await plp.prime()).to.equal(LEGACY_PRIME); + }); + + it("PrimeV2 <-> PrimeLeaderboard are not yet wired", async () => { + expect(await primeV2.primeLeaderboard()).to.equal(constants.AddressZero); + expect(await primeLeaderboard.primeV2()).to.equal(constants.AddressZero); + }); + + it("PrimeV2 mint window is not yet open", async () => { + expect(await primeV2.mintThreshold()).to.equal(0); + expect(await primeV2.mintDeadline()).to.equal(0); }); it("legacy Prime is active (unpaused)", async () => { @@ -141,7 +151,7 @@ forking(BLOCK_NUMBER, async () => { expect(await primeV2.mintDeadline()).to.equal(MINT_DEADLINE); }); - it("Guardian can call epoch ops on PrimeV2 (issue / burn / setMintThreshold)", async () => { + it("Guardian can call epoch ops on PrimeV2 (issue / issueBatch / burn / burnBatch / setMintThreshold)", async () => { for (const sig of [ "issue(address)", "issueBatch(address[])", diff --git a/vips/vip-675/bsctestnet.ts b/vips/vip-675/bsctestnet.ts index 87362debf..1e495edc3 100644 --- a/vips/vip-675/bsctestnet.ts +++ b/vips/vip-675/bsctestnet.ts @@ -15,7 +15,9 @@ const GUARDIAN = bsctestnet.GUARDIAN; // initiates transferOwnership of both contracts to the NormalTimelock (pending acceptance), // but does NOT wire them — the setPrimeV2 / setPrimeLeaderboard wiring is ACM-gated and done // here. This VIP accepts ownership, grants ACM permissions, wires PrimeV2 <-> PrimeLeaderboard, -// configures the Prime markets, opens the mint window, and pauses the legacy Prime. +// repoints the PrimeLiquidityProvider, configures the Prime markets, opens the mint window, +// switches the XVS Vault hook and the Core pool Comptroller to the new contracts, and pauses +// the legacy Prime. export const PRIME_V2 = "0x878e6B88f8F9e85c88bb21396A7637330b9Cd5Ec"; export const PRIME_LEADERBOARD = "0x45E9b8A46558c359b6Ee30580A599AAa1e5d9cDE"; From bd3c2863a1671bc753b447c9e1992c32d9c0b330 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Tue, 2 Jun 2026 16:31:35 +0530 Subject: [PATCH 4/7] chore: update PrimeV2 testnet address and sim fork block --- simulations/vip-675/bsctestnet.ts | 2 +- vips/vip-675/bsctestnet.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/simulations/vip-675/bsctestnet.ts b/simulations/vip-675/bsctestnet.ts index 532973ad3..2121a7f52 100644 --- a/simulations/vip-675/bsctestnet.ts +++ b/simulations/vip-675/bsctestnet.ts @@ -45,7 +45,7 @@ const PRIME_LEADERBOARD_ABI = [ const PLP_ABI = ["function prime() view returns (address)"]; const LEGACY_PRIME_ABI = ["function paused() view returns (bool)"]; -const BLOCK_NUMBER = 110244560; +const BLOCK_NUMBER = 111010308; forking(BLOCK_NUMBER, async () => { let primeV2: Contract; diff --git a/vips/vip-675/bsctestnet.ts b/vips/vip-675/bsctestnet.ts index 1e495edc3..34f251108 100644 --- a/vips/vip-675/bsctestnet.ts +++ b/vips/vip-675/bsctestnet.ts @@ -18,7 +18,7 @@ const GUARDIAN = bsctestnet.GUARDIAN; // repoints the PrimeLiquidityProvider, configures the Prime markets, opens the mint window, // switches the XVS Vault hook and the Core pool Comptroller to the new contracts, and pauses // the legacy Prime. -export const PRIME_V2 = "0x878e6B88f8F9e85c88bb21396A7637330b9Cd5Ec"; +export const PRIME_V2 = "0x4B8b963324dB0D40f64539032AB49CB07c88e312"; export const PRIME_LEADERBOARD = "0x45E9b8A46558c359b6Ee30580A599AAa1e5d9cDE"; // Existing contracts reused by PrimeV2 From 6bd5326ece987342dc7b7bcb07269291bd7f4f1c Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Wed, 3 Jun 2026 11:19:46 +0530 Subject: [PATCH 5/7] chore: scaffold VIP-675 PrimeV2 setup for bscmainnet --- simulations/vip-675/bscmainnet.ts | 68 ++++++++++++ vips/vip-675/bscmainnet.ts | 173 ++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 simulations/vip-675/bscmainnet.ts create mode 100644 vips/vip-675/bscmainnet.ts diff --git a/simulations/vip-675/bscmainnet.ts b/simulations/vip-675/bscmainnet.ts new file mode 100644 index 000000000..5eeca7250 --- /dev/null +++ b/simulations/vip-675/bscmainnet.ts @@ -0,0 +1,68 @@ +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 vip675, { PLP, PRIME_LEADERBOARD, PRIME_V2 } from "../../vips/vip-675/bscmainnet"; + +const { bscmainnet } = NETWORK_ADDRESSES; + +// Minimal inline ABIs — PrimeV2 / PrimeLeaderboard not deployed yet, so no +// generated ABI files exist. Copy full ABIs into ./abi once deployed and swap these out. +const OWNABLE2STEP_ABI = ["function owner() view returns (address)", "function pendingOwner() view returns (address)"]; +const PLP_ABI = ["function prime() view returns (address)"]; + +// TODO: set to a block after PrimeV2 / PrimeLeaderboard are deployed on bscmainnet +const BLOCK_NUMBER = 0; + +forking(BLOCK_NUMBER, async () => { + let primeV2: Contract; + let primeLeaderboard: Contract; + let plp: Contract; + + before(async () => { + primeV2 = new ethers.Contract(PRIME_V2, OWNABLE2STEP_ABI, ethers.provider); + primeLeaderboard = new ethers.Contract(PRIME_LEADERBOARD, OWNABLE2STEP_ABI, ethers.provider); + plp = new ethers.Contract(PLP, PLP_ABI, ethers.provider); + }); + + describe("Pre-VIP behavior", () => { + it("PrimeV2 ownership pending on NormalTimelock (not accepted)", async () => { + expect(await primeV2.pendingOwner()).to.equal(bscmainnet.NORMAL_TIMELOCK); + }); + + it("PrimeLeaderboard ownership pending on NormalTimelock (not accepted)", async () => { + expect(await primeLeaderboard.pendingOwner()).to.equal(bscmainnet.NORMAL_TIMELOCK); + }); + + it("PLP prime token is not yet PrimeV2", async () => { + expect(await plp.prime()).to.not.equal(PRIME_V2); + }); + }); + + testVip("VIP-675 PrimeV2 + PrimeLeaderboard setup", await vip675(), { + callbackAfterExecution: async () => { + // TODO: assert OwnershipTransferred / PermissionGranted / MarketAdded events + // once PrimeV2 / PrimeLeaderboard ABIs are available + }, + }); + + describe("Post-VIP behavior", () => { + it("PrimeV2 owner is the NormalTimelock", async () => { + expect(await primeV2.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); + }); + + it("PrimeLeaderboard owner is the NormalTimelock", async () => { + expect(await primeLeaderboard.owner()).to.equal(bscmainnet.NORMAL_TIMELOCK); + }); + + it("PLP points at PrimeV2", async () => { + expect(await plp.prime()).to.equal(PRIME_V2); + }); + + it("Prime markets are configured on PrimeV2", async () => { + // TODO: assert each addMarket entry exists with the expected multipliers + }); + }); +}); diff --git a/vips/vip-675/bscmainnet.ts b/vips/vip-675/bscmainnet.ts new file mode 100644 index 000000000..e13842f7c --- /dev/null +++ b/vips/vip-675/bscmainnet.ts @@ -0,0 +1,173 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +const { bscmainnet } = NETWORK_ADDRESSES; + +const ACM = bscmainnet.ACCESS_CONTROL_MANAGER; +const NORMAL_TIMELOCK = bscmainnet.NORMAL_TIMELOCK; +const FAST_TRACK_TIMELOCK = bscmainnet.FAST_TRACK_TIMELOCK; +const CRITICAL_TIMELOCK = bscmainnet.CRITICAL_TIMELOCK; + +// ============================================================================ +// TODO: replace with the deployed PrimeV2 / PrimeLeaderboard proxy addresses +// (not yet deployed on bscmainnet — see venus-protocol deploy/014-deploy-prime-v2.ts). +// On live networks the deploy script initiates transferOwnership of both contracts to the +// NormalTimelock (pending acceptance) but does NOT wire them — the setPrimeV2 / +// setPrimeLeaderboard wiring is ACM-gated and done here. This VIP accepts ownership, grants +// ACM permissions, wires the pair, and configures the markets. +// ============================================================================ +export const PRIME_V2 = "0x0000000000000000000000000000000000000000"; // TODO +export const PRIME_LEADERBOARD = "0x0000000000000000000000000000000000000000"; // TODO + +// Existing contracts reused by PrimeV2 +export const PLP = "0x23c4F844ffDdC6161174eB32c770D4D8C07833F2"; // PrimeLiquidityProvider (existing) +export const LEGACY_PRIME = "0xBbCD063efE506c3D42a0Fa2dB5C08430288C71FC"; // current Prime (to be replaced) + +// Prime markets on the Core pool (bscmainnet) +const VUSDT = "0xfD5840Cd36d94D7229439859C0112a4185BC0255"; +const VUSDC = "0xecA88125a5ADbe82614ffC12D0DB554E2e2867C8"; +const VBTC = "0x882C173bC7Ff3b7786CA16dfeD3DFFfb9Ee7847B"; +const VETH = "0xf508fCD89b8bd15579dc79A6827cB4686A3592c8"; + +const ALL_TIMELOCKS = [NORMAL_TIMELOCK, FAST_TRACK_TIMELOCK, CRITICAL_TIMELOCK]; + +// Grant a single ACM permission for `target.signature` to every timelock in `accounts`. +const grant = (target: string, signature: string, accounts: string[] = [NORMAL_TIMELOCK]) => + accounts.map(account => ({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [target, signature, account], + })); + +// ACM-gated functions on PrimeV2 (see PrimeV2.sol _checkAccessAllowed) +const PRIME_V2_PERMISSIONS = [ + ...grant(PRIME_V2, "issue(address)"), + ...grant(PRIME_V2, "issueBatch(address[])"), + ...grant(PRIME_V2, "burn(address)"), + ...grant(PRIME_V2, "burnBatch(address[])"), + ...grant(PRIME_V2, "setPrimeLeaderboard(address)"), + ...grant(PRIME_V2, "addMarket(address,uint256,uint256)"), + ...grant(PRIME_V2, "removeMarket(address)"), + ...grant(PRIME_V2, "setLimit(uint256)"), + ...grant(PRIME_V2, "updateAlpha(uint128,uint128)"), + ...grant(PRIME_V2, "updateMultipliers(address,uint256,uint256)"), + ...grant(PRIME_V2, "setMaxLoopsLimit(uint256)"), + ...grant(PRIME_V2, "setMintThreshold(uint256,uint256)"), + ...grant(PRIME_V2, "pause()", ALL_TIMELOCKS), + ...grant(PRIME_V2, "unpause()", ALL_TIMELOCKS), +]; + +// ACM-gated functions on PrimeLeaderboard (see PrimeLeaderboard.sol _checkAccessAllowed) +const PRIME_LEADERBOARD_PERMISSIONS = [ + ...grant(PRIME_LEADERBOARD, "initializeStakers(address[],uint256[],uint64[])"), + ...grant(PRIME_LEADERBOARD, "finalizeInitialization()"), + ...grant(PRIME_LEADERBOARD, "setMultiplierTiers(uint256[],uint256[])"), + ...grant(PRIME_LEADERBOARD, "setPrimeV2(address)"), + ...grant(PRIME_LEADERBOARD, "setMaxLoopsLimit(uint256)"), +]; + +const vip675 = () => { + const meta = { + version: "v2", + title: "VIP-675 Deploy and configure PrimeV2 and PrimeLeaderboard", + description: `#### Summary + +If passed, this VIP will accept ownership of the new PrimeV2 and PrimeLeaderboard contracts on BNB Chain, grant the governance timelocks the required ACM permissions, point the existing PrimeLiquidityProvider at PrimeV2, and configure the Prime markets and leaderboard tiers. + +#### Description + +TODO: describe the PrimeV2 / PrimeLeaderboard rollout, the markets added, the leaderboard multiplier tiers, and the migration of existing Prime holders. + +#### Security and additional considerations + +TODO: list audits (incl. Sherlock VPD-1292), testnet deployment and simulation links. + +#### References + +- PrimeV2 / PrimeLeaderboard implementation: https://github.com/VenusProtocol/venus-protocol/pull/676`, + forDescription: "I agree that Venus Protocol should proceed with this proposal", + againstDescription: "I do not think that Venus Protocol should proceed with this proposal", + abstainDescription: "I am indifferent to whether Venus Protocol proceeds or not", + }; + + return makeProposal( + [ + // 1. Accept ownership (deploy script transferred ownership to NormalTimelock) + { + target: PRIME_V2, + signature: "acceptOwnership()", + params: [], + }, + { + target: PRIME_LEADERBOARD, + signature: "acceptOwnership()", + params: [], + }, + + // 2. Grant ACM permissions + ...PRIME_V2_PERMISSIONS, + ...PRIME_LEADERBOARD_PERMISSIONS, + + // 3. Wire PrimeV2 <-> PrimeLeaderboard (ACM-gated; deploy does NOT wire on live networks) + { + target: PRIME_V2, + signature: "setPrimeLeaderboard(address)", + params: [PRIME_LEADERBOARD], + }, + { + target: PRIME_LEADERBOARD, + signature: "setPrimeV2(address)", + params: [PRIME_V2], + }, + + // 4. Point the existing PrimeLiquidityProvider at PrimeV2 (onlyOwner = NormalTimelock) + { + target: PLP, + signature: "setPrimeToken(address)", + params: [PRIME_V2], + }, + + // 5. Configure PrimeV2 markets + // TODO: confirm supplyMultiplier / borrowMultiplier (1e18 scaled) per market + { + target: PRIME_V2, + signature: "addMarket(address,uint256,uint256)", + params: [VUSDT, "0", "0"], // TODO multipliers + }, + { + target: PRIME_V2, + signature: "addMarket(address,uint256,uint256)", + params: [VUSDC, "0", "0"], // TODO multipliers + }, + { + target: PRIME_V2, + signature: "addMarket(address,uint256,uint256)", + params: [VBTC, "0", "0"], // TODO multipliers + }, + { + target: PRIME_V2, + signature: "addMarket(address,uint256,uint256)", + params: [VETH, "0", "0"], // TODO multipliers + }, + + // Note: setMintThreshold is intentionally not called here — governance sets it + // after the first epoch ends (the setMintThreshold ACM permission is granted above). + + // 6. Decommission the legacy Prime: pause it (halts claim / score updates / issuance). + // NormalTimelock already holds the togglePause ACM permission, so no grant is needed. + // Legacy Prime is currently unpaused, so a single togglePause pauses it. + // (setLimit(0,0) is NOT used: it reverts with InvalidLimit because the legacy Prime + // already has revocable tokens minted, and limits cannot be set below the minted total.) + { + target: LEGACY_PRIME, + signature: "togglePause()", + params: [], + }, + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip675; From c307c3a1e77cba7ab6d09cf12d3137030adf9317 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 4 Jun 2026 11:31:25 +0530 Subject: [PATCH 6/7] feat: add VIP-675 bsctestnet addendum Extend Guardian ACM permissions to every remaining ACM-gated function on PrimeV2 / PrimeLeaderboard, and compress the leaderboard multiplier tiers from days (30/60/90d) to hours (1/2/3h) so testnet integration can exercise the tier progression without long waits. --- simulations/vip-675/bsctestnet-addendum.ts | 113 +++++++++++++++++++++ vips/vip-675/bsctestnet-addendum.ts | 90 ++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 simulations/vip-675/bsctestnet-addendum.ts create mode 100644 vips/vip-675/bsctestnet-addendum.ts diff --git a/simulations/vip-675/bsctestnet-addendum.ts b/simulations/vip-675/bsctestnet-addendum.ts new file mode 100644 index 000000000..0b9384409 --- /dev/null +++ b/simulations/vip-675/bsctestnet-addendum.ts @@ -0,0 +1,113 @@ +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 { PRIME_LEADERBOARD, PRIME_V2 } from "../../vips/vip-675/bsctestnet"; +import vip675Addendum, { TIER_DURATIONS, TIER_MULTIPLIERS } from "../../vips/vip-675/bsctestnet-addendum"; + +const { bsctestnet } = NETWORK_ADDRESSES; + +const ACM_ABI = [ + "function hasRole(bytes32 role, address account) view returns (bool)", + "event PermissionGranted(address account, address contractAddress, string functionSig)", +]; +const LEADERBOARD_ABI = [ + "function getMultiplierTiers() view returns (uint256[] durations, uint256[] multipliers)", + "event MultiplierTiersUpdated(uint256[] durations, uint256[] multipliers)", +]; + +const BLOCK_NUMBER = 111353484; + +// All ACM-gated functions on PrimeV2 / PrimeLeaderboard that the addendum grants to the Guardian. +const PRIME_V2_NEW_GUARDIAN_SIGS = [ + "setPrimeLeaderboard(address)", + "addMarket(address,uint256,uint256)", + "removeMarket(address)", + "setLimit(uint256)", + "updateAlpha(uint128,uint128)", + "updateMultipliers(address,uint256,uint256)", + "setMaxLoopsLimit(uint256)", + "pause()", + "unpause()", +]; +const PRIME_LEADERBOARD_NEW_GUARDIAN_SIGS = [ + "setMultiplierTiers(uint256[],uint256[])", + "setPrimeV2(address)", + "setMaxLoopsLimit(uint256)", +]; + +forking(BLOCK_NUMBER, async () => { + let acm: Contract; + let leaderboard: Contract; + + before(async () => { + acm = new ethers.Contract(bsctestnet.ACCESS_CONTROL_MANAGER, ACM_ABI, ethers.provider); + leaderboard = new ethers.Contract(PRIME_LEADERBOARD, LEADERBOARD_ABI, ethers.provider); + }); + + const roleFor = (target: string, signature: string) => + ethers.utils.solidityKeccak256(["address", "string"], [target, signature]); + + describe("Pre-VIP behavior", () => { + it("Guardian does not yet hold the addendum permissions on PrimeV2", async () => { + for (const sig of PRIME_V2_NEW_GUARDIAN_SIGS) { + expect(await acm.hasRole(roleFor(PRIME_V2, sig), bsctestnet.GUARDIAN)).to.equal(false); + } + }); + + it("Guardian does not yet hold the addendum permissions on PrimeLeaderboard", async () => { + for (const sig of PRIME_LEADERBOARD_NEW_GUARDIAN_SIGS) { + expect(await acm.hasRole(roleFor(PRIME_LEADERBOARD, sig), bsctestnet.GUARDIAN)).to.equal(false); + } + }); + + it("PrimeLeaderboard tiers are still the day-scale defaults", async () => { + const { durations, multipliers } = await leaderboard.getMultiplierTiers(); + expect(durations.map((d: { toString: () => string }) => d.toString())).to.deep.equal([ + (30 * 24 * 60 * 60).toString(), + (60 * 24 * 60 * 60).toString(), + (90 * 24 * 60 * 60).toString(), + ]); + expect(multipliers.map((m: { toString: () => string }) => m.toString())).to.deep.equal([ + "1300000000000000000", + "1600000000000000000", + "2000000000000000000", + ]); + }); + }); + + testVip("VIP-675 addendum [Testnet]", await vip675Addendum(), { + callbackAfterExecution: async txResponse => { + await expect(txResponse) + .to.emit(leaderboard, "MultiplierTiersUpdated") + .withArgs(TIER_DURATIONS, TIER_MULTIPLIERS); + await expect(txResponse) + .to.emit(acm, "PermissionGranted") + .withArgs(bsctestnet.GUARDIAN, PRIME_LEADERBOARD, "setMultiplierTiers(uint256[],uint256[])"); + }, + }); + + describe("Post-VIP behavior", () => { + it("Guardian holds all addendum permissions on PrimeV2", async () => { + for (const sig of PRIME_V2_NEW_GUARDIAN_SIGS) { + expect(await acm.hasRole(roleFor(PRIME_V2, sig), bsctestnet.GUARDIAN)).to.equal(true); + } + }); + + it("Guardian holds all addendum permissions on PrimeLeaderboard", async () => { + for (const sig of PRIME_LEADERBOARD_NEW_GUARDIAN_SIGS) { + expect(await acm.hasRole(roleFor(PRIME_LEADERBOARD, sig), bsctestnet.GUARDIAN)).to.equal(true); + } + }); + + it("PrimeLeaderboard tiers are compressed to the hour-scale schedule", async () => { + const { durations, multipliers } = await leaderboard.getMultiplierTiers(); + expect(durations.map((d: { toString: () => string }) => d.toString())).to.deep.equal( + TIER_DURATIONS.map(d => d.toString()), + ); + expect(multipliers.map((m: { toString: () => string }) => m.toString())).to.deep.equal(TIER_MULTIPLIERS); + }); + }); +}); diff --git a/vips/vip-675/bsctestnet-addendum.ts b/vips/vip-675/bsctestnet-addendum.ts new file mode 100644 index 000000000..2dc411f76 --- /dev/null +++ b/vips/vip-675/bsctestnet-addendum.ts @@ -0,0 +1,90 @@ +import { parseUnits } from "ethers/lib/utils"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { PRIME_LEADERBOARD, PRIME_V2 } from "./bsctestnet"; + +const { bsctestnet } = NETWORK_ADDRESSES; + +const ACM = bsctestnet.ACCESS_CONTROL_MANAGER; +const GUARDIAN = bsctestnet.GUARDIAN; + +// Grant ACM permission for `target.signature` to `account`. +const grant = (target: string, signature: string, account: string) => ({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [target, signature, account], +}); + +// ACM-gated functions on PrimeV2 / PrimeLeaderboard that the Guardian did NOT yet hold +// after the original VIP-675. Granting them here gives the Guardian full operational +// control over both contracts (issue/burn/setMintThreshold/initializeStakers/finalizeInitialization +// were already granted in the original VIP-675 and are intentionally not re-granted). +const PRIME_V2_GUARDIAN_PERMISSIONS = [ + grant(PRIME_V2, "setPrimeLeaderboard(address)", GUARDIAN), + grant(PRIME_V2, "addMarket(address,uint256,uint256)", GUARDIAN), + grant(PRIME_V2, "removeMarket(address)", GUARDIAN), + grant(PRIME_V2, "setLimit(uint256)", GUARDIAN), + grant(PRIME_V2, "updateAlpha(uint128,uint128)", GUARDIAN), + grant(PRIME_V2, "updateMultipliers(address,uint256,uint256)", GUARDIAN), + grant(PRIME_V2, "setMaxLoopsLimit(uint256)", GUARDIAN), + grant(PRIME_V2, "pause()", GUARDIAN), + grant(PRIME_V2, "unpause()", GUARDIAN), +]; + +const PRIME_LEADERBOARD_GUARDIAN_PERMISSIONS = [ + grant(PRIME_LEADERBOARD, "setMultiplierTiers(uint256[],uint256[])", GUARDIAN), + grant(PRIME_LEADERBOARD, "setPrimeV2(address)", GUARDIAN), + grant(PRIME_LEADERBOARD, "setMaxLoopsLimit(uint256)", GUARDIAN), +]; + +// Compressed multiplier tiers for faster testnet iteration: +// <1h -> 1.0x (implicit base, not a tier entry) +// 1-2h -> 1.3x +// 2-3h -> 1.6x +// >=3h -> 2.0x +export const TIER_DURATIONS = [3600, 7200, 10800]; +export const TIER_MULTIPLIERS = [ + parseUnits("1.3", 18).toString(), + parseUnits("1.6", 18).toString(), + parseUnits("2", 18).toString(), +]; + +const vip675Addendum = () => { + const meta = { + version: "v2", + title: "VIP-675 addendum [Testnet] Extend Guardian permissions and compress leaderboard tiers", + description: `#### Summary + +If passed, this addendum to VIP-675 will: + +- Grant the Guardian the remaining ACM permissions on PrimeV2 and PrimeLeaderboard (every ACM-gated function), so the Guardian can fully operate both contracts without going through governance for each call during testnet integration. +- Compress the PrimeLeaderboard multiplier tiers from day-scale (30/60/90 days -> 1.3x/1.6x/2.0x) to hour-scale (1h/2h/3h -> 1.3x/1.6x/2.0x), so FE/BE testing can exercise the tier progression without waiting weeks. + +The base 1.0x multiplier (<1h) is implicit and is not a tier entry. + +#### References + +- VIP-675 (original): https://github.com/VenusProtocol/vips/pull/712`, + forDescription: "I agree that Venus Protocol should proceed with this proposal", + againstDescription: "I do not think that Venus Protocol should proceed with this proposal", + abstainDescription: "I am indifferent to whether Venus Protocol proceeds or not", + }; + + return makeProposal( + [ + ...PRIME_V2_GUARDIAN_PERMISSIONS, + ...PRIME_LEADERBOARD_GUARDIAN_PERMISSIONS, + { + target: PRIME_LEADERBOARD, + signature: "setMultiplierTiers(uint256[],uint256[])", + params: [TIER_DURATIONS, TIER_MULTIPLIERS], + }, + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip675Addendum; From 3052dee4c50d048697a45ffb0865b183c28e5867 Mon Sep 17 00:00:00 2001 From: Debugger022 Date: Thu, 4 Jun 2026 18:15:14 +0530 Subject: [PATCH 7/7] =?UTF-8?q?feat(vip-675):=20testnet=20addendum=202=20?= =?UTF-8?q?=E2=80=94=20upgrade=20PrimeV2=20impl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrades the bsctestnet PrimeV2 proxy to the new implementation deployed via venus-protocol PR #677 and grants ACM permission for the new recordCycleSnapshot(uint256) function to NormalTimelock and Guardian, matching the keeper grant pattern from the original VIP-675. --- .../vip-675/abi/AccessControlManager.json | 360 ++++ simulations/vip-675/abi/PrimeV2.json | 1822 +++++++++++++++++ simulations/vip-675/abi/ProxyAdmin.json | 151 ++ simulations/vip-675/bsctestnet-addendum2.ts | 92 + vips/vip-675/bsctestnet-addendum2.ts | 82 + 5 files changed, 2507 insertions(+) create mode 100644 simulations/vip-675/abi/AccessControlManager.json create mode 100644 simulations/vip-675/abi/PrimeV2.json create mode 100644 simulations/vip-675/abi/ProxyAdmin.json create mode 100644 simulations/vip-675/bsctestnet-addendum2.ts create mode 100644 vips/vip-675/bsctestnet-addendum2.ts diff --git a/simulations/vip-675/abi/AccessControlManager.json b/simulations/vip-675/abi/AccessControlManager.json new file mode 100644 index 000000000..4a118fcc4 --- /dev/null +++ b/simulations/vip-675/abi/AccessControlManager.json @@ -0,0 +1,360 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "functionSig", + "type": "string" + } + ], + "name": "PermissionGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "functionSig", + "type": "string" + } + ], + "name": "PermissionRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "functionSig", + "type": "string" + }, + { + "internalType": "address", + "name": "accountToPermit", + "type": "address" + } + ], + "name": "giveCallPermission", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "functionSig", + "type": "string" + } + ], + "name": "hasPermission", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "string", + "name": "functionSig", + "type": "string" + } + ], + "name": "isAllowedToCall", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "string", + "name": "functionSig", + "type": "string" + }, + { + "internalType": "address", + "name": "accountToRevoke", + "type": "address" + } + ], + "name": "revokeCallPermission", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-675/abi/PrimeV2.json b/simulations/vip-675/abi/PrimeV2.json new file mode 100644 index 000000000..1bdca9052 --- /dev/null +++ b/simulations/vip-675/abi/PrimeV2.json @@ -0,0 +1,1822 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "wrappedNativeToken_", + "type": "address" + }, + { + "internalType": "address", + "name": "nativeMarket_", + "type": "address" + }, + { + "internalType": "address", + "name": "xvsVault_", + "type": "address" + }, + { + "internalType": "address", + "name": "xvsVaultRewardToken_", + "type": "address" + }, + { + "internalType": "uint256", + "name": "xvsVaultPoolId_", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "timeBased_", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "blocksPerYear_", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AssetAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "score", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "threshold", + "type": "uint256" + } + ], + "name": "EligibilityBelowThreshold", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "x", + "type": "int256" + } + ], + "name": "ExpTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAlphaArguments", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidBlocksPerYear", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidDeadline", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidFixedPoint", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "n", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "d", + "type": "uint256" + } + ], + "name": "InvalidFraction", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidLimit", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidMultipliers", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTimeBasedConfiguration", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidVToken", + "type": "error" + }, + { + "inputs": [], + "name": "LeaderboardNotSet", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "x", + "type": "int256" + } + ], + "name": "LnNonRealResult", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "x", + "type": "int256" + } + ], + "name": "LnTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "MarketAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "MarketHasActiveMembers", + "type": "error" + }, + { + "inputs": [], + "name": "MarketNotSupported", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "loopsLimit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "requiredLoops", + "type": "uint256" + } + ], + "name": "MaxLoopsLimitExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MintThresholdNotSet", + "type": "error" + }, + { + "inputs": [], + "name": "MintWindowClosed", + "type": "error" + }, + { + "inputs": [], + "name": "NoScoreUpdatesRequired", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyPrimeLeaderboard", + "type": "error" + }, + { + "inputs": [], + "name": "ScoreUpdateInProgress", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "calledContract", + "type": "address" + }, + { + "internalType": "string", + "name": "methodSignature", + "type": "string" + } + ], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "decimals", + "type": "uint256" + } + ], + "name": "UnsupportedUnderlyingDecimals", + "type": "error" + }, + { + "inputs": [], + "name": "UserAlreadyHasPrimeToken", + "type": "error" + }, + { + "inputs": [], + "name": "UserHasNoPrimeToken", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint128", + "name": "oldNumerator", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "oldDenominator", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "newNumerator", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "newDenominator", + "type": "uint128" + } + ], + "name": "AlphaUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "cycleId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "name": "CycleSnapshotRecorded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "roundId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "remainingUpdates", + "type": "uint256" + } + ], + "name": "IncompleteRoundDiscarded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "InterestClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "supplyMultiplier", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "borrowMultiplier", + "type": "uint256" + } + ], + "name": "MarketAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "MarketRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldMaxLoopsLimit", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newmaxLoopsLimit", + "type": "uint256" + } + ], + "name": "MaxLoopsLimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldLimit", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newLimit", + "type": "uint256" + } + ], + "name": "MintLimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldThreshold", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newThreshold", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "MintThresholdUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldSupplyMultiplier", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldBorrowMultiplier", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newSupplyMultiplier", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBorrowMultiplier", + "type": "uint256" + } + ], + "name": "MultiplierUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldAccessControlManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAccessControlManager", + "type": "address" + } + ], + "name": "NewAccessControlManager", + "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": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldLeaderboard", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newLeaderboard", + "type": "address" + } + ], + "name": "PrimeLeaderboardSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "score", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "threshold", + "type": "uint256" + } + ], + "name": "SkippedIneligibleUser", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "underlying", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "UndistributedSwept", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "UserScoreUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "NATIVE_MARKET", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "WRAPPED_NATIVE_TOKEN", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "accessControlManager", + "outputs": [ + { + "internalType": "contract IAccessControlManagerV8", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "accrueInterest", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "accrueInterestAndUpdateScore", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "accrueInterestAndUpdateScore", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint256", + "name": "supplyMultiplier", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowMultiplier", + "type": "uint256" + } + ], + "name": "addMarket", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "alphaDenominator", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "alphaNumerator", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "blocksOrSecondsPerYear", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "users", + "type": "address[]" + } + ], + "name": "burnBatch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + } + ], + "name": "claimInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "claimInterest", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "claimPrime", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "users", + "type": "address[]" + } + ], + "name": "claimPrimeBatch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "corePoolComptroller", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAllMarkets", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBlockNumberOrTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "address[]", + "name": "users", + "type": "address[]" + } + ], + "name": "getLifetimeAccruedByMarket", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "address[]", + "name": "markets_", + "type": "address[]" + } + ], + "name": "getLifetimeAccruedByUser", + "outputs": [ + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "getPendingRewards", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "rewardToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct PrimeV2StorageV1.PendingReward[]", + "name": "pendingRewards", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "getPendingRewardsStatic", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "rewardToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct PrimeV2StorageV1.PendingReward[]", + "name": "pendingRewards", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "alphaNumerator_", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "alphaDenominator_", + "type": "uint128" + }, + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + }, + { + "internalType": "address", + "name": "primeLiquidityProvider_", + "type": "address" + }, + { + "internalType": "address", + "name": "corePoolComptroller_", + "type": "address" + }, + { + "internalType": "address", + "name": "oracle_", + "type": "address" + }, + { + "internalType": "uint256", + "name": "loopsLimit_", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "interests", + "outputs": [ + { + "internalType": "uint256", + "name": "accrued", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "score", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lifetimeAccrued", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isPrimeHolder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isScoreUpdated", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isTimeBased", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "isUserPrimeHolder", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "issue", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "users", + "type": "address[]" + } + ], + "name": "issueBatch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "markets", + "outputs": [ + { + "internalType": "uint256", + "name": "supplyMultiplier", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowMultiplier", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "rewardIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sumOfMembersScore", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "exists", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxLoopsLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "mintDeadline", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "mintThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextScoreUpdateRoundId", + "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": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingScoreUpdates", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "primeLeaderboard", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "primeLiquidityProvider", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "cycleId", + "type": "uint256" + } + ], + "name": "recordCycleSnapshot", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + } + ], + "name": "removeMarket", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "accessControlManager_", + "type": "address" + } + ], + "name": "setAccessControlManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenLimit_", + "type": "uint256" + } + ], + "name": "setLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "loopsLimit", + "type": "uint256" + } + ], + "name": "setMaxLoopsLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "mintThreshold_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "mintDeadline_", + "type": "uint256" + } + ], + "name": "setMintThreshold", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "primeLeaderboard_", + "type": "address" + } + ], + "name": "setPrimeLeaderboard", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "vToken", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "sweepUndistributed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tokenLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "undistributedReward", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "unreleasedPLPIncome", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "alphaNumerator_", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "alphaDenominator_", + "type": "uint128" + } + ], + "name": "updateAlpha", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "market", + "type": "address" + }, + { + "internalType": "uint256", + "name": "supplyMultiplier", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "borrowMultiplier", + "type": "uint256" + } + ], + "name": "updateMultipliers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "users", + "type": "address[]" + } + ], + "name": "updateScores", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "vTokenForAsset", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "xvsBalanceOfUser", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "xvsVault", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "xvsVaultPoolId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "xvsVaultRewardToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/simulations/vip-675/abi/ProxyAdmin.json b/simulations/vip-675/abi/ProxyAdmin.json new file mode 100644 index 000000000..b4c51d8df --- /dev/null +++ b/simulations/vip-675/abi/ProxyAdmin.json @@ -0,0 +1,151 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "contract TransparentUpgradeableProxy", + "name": "proxy", + "type": "address" + }, + { + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "changeProxyAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract TransparentUpgradeableProxy", + "name": "proxy", + "type": "address" + } + ], + "name": "getProxyAdmin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract TransparentUpgradeableProxy", + "name": "proxy", + "type": "address" + } + ], + "name": "getProxyImplementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract TransparentUpgradeableProxy", + "name": "proxy", + "type": "address" + }, + { + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "upgrade", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract TransparentUpgradeableProxy", + "name": "proxy", + "type": "address" + }, + { + "internalType": "address", + "name": "implementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/simulations/vip-675/bsctestnet-addendum2.ts b/simulations/vip-675/bsctestnet-addendum2.ts new file mode 100644 index 000000000..6306d7bb1 --- /dev/null +++ b/simulations/vip-675/bsctestnet-addendum2.ts @@ -0,0 +1,92 @@ +import { expect } from "chai"; +import { Contract } from "ethers"; +import { ethers } from "hardhat"; +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { expectEvents } from "src/utils"; +import { forking, testVip } from "src/vip-framework"; + +import { PRIME_V2 } from "../../vips/vip-675/bsctestnet"; +import vip675Addendum2, { + NEW_CYCLE_GRANT_ACCOUNTS, + NEW_CYCLE_SIG, + NEW_PRIME_V2_IMPL, + PROXY_ADMIN, +} from "../../vips/vip-675/bsctestnet-addendum2"; +import ACM_ABI from "./abi/AccessControlManager.json"; +import PROXY_ADMIN_ABI from "./abi/ProxyAdmin.json"; + +const { bsctestnet } = NETWORK_ADDRESSES; +const { ACCESS_CONTROL_MANAGER } = bsctestnet; + +// Current bsctestnet impl behind the PrimeV2 proxy (pre-upgrade snapshot). +const OLD_PRIME_V2_IMPL = "0xf33Ab2625B94c73B1041c03ded18bDD0F8C681A7"; + +// Selector for the new ACM-gated function added by the upgrade. Used to assert +// the new selector is callable post-upgrade. +const RECORD_CYCLE_SELECTOR = ethers.utils.id(NEW_CYCLE_SIG).slice(0, 10); + +const FORK_BLOCK = 111408294; + +const roleFor = (target: string, signature: string) => + ethers.utils.solidityKeccak256(["address", "string"], [target, signature]); + +forking(FORK_BLOCK, async () => { + let acm: Contract; + let proxyAdmin: Contract; + + before(async () => { + acm = new ethers.Contract(ACCESS_CONTROL_MANAGER, ACM_ABI, ethers.provider); + proxyAdmin = new ethers.Contract(PROXY_ADMIN, PROXY_ADMIN_ABI, ethers.provider); + }); + + describe("Pre-VIP state", () => { + it("PrimeV2 proxy still points at the old implementation", async () => { + expect((await proxyAdmin.getProxyImplementation(PRIME_V2)).toLowerCase()).to.equal( + OLD_PRIME_V2_IMPL.toLowerCase(), + ); + }); + + it("ProxyAdmin is owned by NormalTimelock", async () => { + expect((await proxyAdmin.owner()).toLowerCase()).to.equal(bsctestnet.NORMAL_TIMELOCK.toLowerCase()); + }); + + it("recordCycleSnapshot selector is not present on the old impl", async () => { + const code = await ethers.provider.getCode(OLD_PRIME_V2_IMPL); + expect(code.includes(RECORD_CYCLE_SELECTOR.slice(2)), "selector unexpectedly present").to.equal(false); + }); + + it("NormalTimelock + Guardian do not yet hold recordCycleSnapshot permission", async () => { + for (const account of NEW_CYCLE_GRANT_ACCOUNTS) { + expect(await acm.hasRole(roleFor(PRIME_V2, NEW_CYCLE_SIG), account)).to.equal(false); + } + }); + }); + + testVip("VIP-675 addendum 2 [Testnet]", await vip675Addendum2(), { + callbackAfterExecution: async txResponse => { + // 2 PermissionGranted events from ACM (NormalTimelock + Guardian). + // Proxy Upgraded event isn't in ProxyAdmin's ABI; impl change is asserted + // via getProxyImplementation in the Post-VIP section instead. + await expectEvents(txResponse, [ACM_ABI], ["PermissionGranted"], [2]); + }, + }); + + describe("Post-VIP state", () => { + it("PrimeV2 proxy now points at the new implementation", async () => { + expect((await proxyAdmin.getProxyImplementation(PRIME_V2)).toLowerCase()).to.equal( + NEW_PRIME_V2_IMPL.toLowerCase(), + ); + }); + + it("recordCycleSnapshot selector is present on the new impl", async () => { + const code = await ethers.provider.getCode(NEW_PRIME_V2_IMPL); + expect(code.includes(RECORD_CYCLE_SELECTOR.slice(2)), "selector missing on new impl").to.equal(true); + }); + + it("NormalTimelock + Guardian hold recordCycleSnapshot permission", async () => { + for (const account of NEW_CYCLE_GRANT_ACCOUNTS) { + expect(await acm.hasRole(roleFor(PRIME_V2, NEW_CYCLE_SIG), account)).to.equal(true); + } + }); + }); +}); diff --git a/vips/vip-675/bsctestnet-addendum2.ts b/vips/vip-675/bsctestnet-addendum2.ts new file mode 100644 index 000000000..b98b3e853 --- /dev/null +++ b/vips/vip-675/bsctestnet-addendum2.ts @@ -0,0 +1,82 @@ +import { NETWORK_ADDRESSES } from "src/networkAddresses"; +import { ProposalType } from "src/types"; +import { makeProposal } from "src/utils"; + +import { PRIME_V2 } from "./bsctestnet"; + +const { bsctestnet } = NETWORK_ADDRESSES; + +const ACM = bsctestnet.ACCESS_CONTROL_MANAGER; +const NORMAL_TIMELOCK = bsctestnet.NORMAL_TIMELOCK; +const GUARDIAN = bsctestnet.GUARDIAN; + +// DefaultProxyAdmin owning the PrimeV2 / PrimeLeaderboard transparent proxies on +// bsctestnet. Its owner is NormalTimelock (verified on-chain). +export const PROXY_ADMIN = "0xef480a5654b231ff7d80A0681F938f3Db71a6Ca6"; + +// New PrimeV2 implementation deployed via venus-protocol PR #677 +export const NEW_PRIME_V2_IMPL = "0xa327c5F6858113e228edA782D59e5A70387669a1"; + +// New ACM-gated function introduced by the new impl. The view-only additions +// (getLifetimeAccruedByMarket / getLifetimeAccruedByUser) need no ACM grant. +export const NEW_CYCLE_SIG = "recordCycleSnapshot(uint256)"; + +// Mirrors the keeper grant pattern in the original VIP-675: NormalTimelock for +// governance-driven operation, Guardian for off-chain epoch operations. +export const NEW_CYCLE_GRANT_ACCOUNTS = [NORMAL_TIMELOCK, GUARDIAN]; + +const grant = (target: string, signature: string, account: string) => ({ + target: ACM, + signature: "giveCallPermission(address,string,address)", + params: [target, signature, account], +}); + +const vip675Addendum2 = () => { + const meta = { + version: "v2", + title: "VIP-675 addendum 2 [Testnet] Upgrade PrimeV2 implementation", + description: `#### Summary + +If passed, this second addendum to VIP-675 will: + +- Upgrade the PrimeV2 proxy at \`${PRIME_V2}\` to a new implementation at \`${NEW_PRIME_V2_IMPL}\` (deployed via venus-protocol PR [#677](https://github.com/VenusProtocol/venus-protocol/pull/677)). +- Grant ACM permission for the new \`${NEW_CYCLE_SIG}\` function on PrimeV2 to the NormalTimelock and the Guardian, matching the keeper grant pattern established for the existing epoch operations (issue/burn/setMintThreshold) in the original VIP-675. + +#### Description + +The new implementation adds: + +- \`${NEW_CYCLE_SIG}\` — ACM-gated; records a per-cycle accrual snapshot consumed by the new lifetime-accrued view functions below. Granted here to NormalTimelock + Guardian. +- \`getLifetimeAccruedByMarket(address,address[])\` — view, no ACM grant required. +- \`getLifetimeAccruedByUser(address,address[])\` — view, no ACM grant required. + +No existing function signatures are removed, no storage layout changes that affect already-initialized state. + +#### References + +- venus-protocol PR #677 (deploy script + bsctestnet artifacts): https://github.com/VenusProtocol/venus-protocol/pull/677 +- Original VIP-675 (PrimeV2 / PrimeLeaderboard setup): https://github.com/VenusProtocol/vips/pull/712`, + forDescription: "I agree that Venus Protocol should proceed with this proposal", + againstDescription: "I do not think that Venus Protocol should proceed with this proposal", + abstainDescription: "I am indifferent to whether Venus Protocol proceeds or not", + }; + + return makeProposal( + [ + // 1. Upgrade the PrimeV2 proxy to the new implementation. + { + target: PROXY_ADMIN, + signature: "upgrade(address,address)", + params: [PRIME_V2, NEW_PRIME_V2_IMPL], + }, + + // 2. Grant ACM permission for the new recordCycleSnapshot(uint256) function + // to NormalTimelock + Guardian, matching the keeper pattern from VIP-675. + ...NEW_CYCLE_GRANT_ACCOUNTS.map(account => grant(PRIME_V2, NEW_CYCLE_SIG, account)), + ], + meta, + ProposalType.REGULAR, + ); +}; + +export default vip675Addendum2;