Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions simulations/vip-675/bsctestnet.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
});
});
194 changes: 194 additions & 0 deletions vips/vip-675/bsctestnet.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading