@vechain/vebetterdao-contracts
Version:
Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.
345 lines (344 loc) • 21.1 kB
JavaScript
import { ethers } from "hardhat";
import { expect } from "chai";
import { describe, it } from "mocha";
import { deployProxy, deployAndUpgrade, upgradeProxy } from "../../scripts/helpers/upgrades";
import { getOrDeployContractInstances } from "../helpers/deploy";
import { createTestConfig } from "../helpers/config";
import { getVot3Tokens } from "../helpers";
import { endorseApp } from "../helpers/xnodes";
import { waitForBlock } from "../helpers";
describe("XAllocationVoting Upgrade - @shard14a", function () {
/*
* USAGE PATTERN:
* -------------
* This function is called immediately after each upgrade to ensure:
* - No storage slots were corrupted during upgrade
* - All mappings remain accessible and accurate
* - Historical data spanning multiple rounds is preserved
*
* The test builds up expectedVotes and expectedUserVotes cumulatively,
* so each validation checks the ENTIRE history, not just recent changes.
*/
async function validateContractState(contract, expectedVersion, expectedRoundId, expectedVotes, expectedUserVotes) {
// Verify contract metadata is preserved
expect(await contract.version()).to.equal(expectedVersion);
expect(await contract.currentRoundId()).to.equal(expectedRoundId);
// This ensures no voting data is lost across any upgrade
for (const [roundId, appVotes] of Object.entries(expectedVotes)) {
for (const [appId, expectedAmount] of Object.entries(appVotes)) {
expect(await contract.getAppVotes(parseInt(roundId), appId)).to.equal(expectedAmount);
}
}
// This ensures user participation history is completely preserved
for (const [roundId, userVotes] of Object.entries(expectedUserVotes)) {
for (const [userAddress, hasVoted] of Object.entries(userVotes)) {
expect(await contract.hasVoted(parseInt(roundId), userAddress)).to.equal(hasVoted);
}
}
}
it("Should preserve all storage data across upgrades V3->V4->V5->V6->V7->V8", async () => {
const config = createTestConfig();
const configContracts = await getOrDeployContractInstances({
forceDeploy: true,
});
const { otherAccounts, x2EarnApps, xAllocationPool, b3tr, vot3, galaxyMember, timeLock, treasury, owner, creators, veBetterPassport, minterAccount, governor, } = configContracts;
const creator1 = creators[0];
const creator2 = creators[1];
const creator3 = creators[2];
// Configure veBetterPassport for testing (no personhood requirements)
await veBetterPassport.connect(owner).setThresholdPoPScore(0);
await veBetterPassport.toggleCheck(4);
// ========================================
// DEPLOY: Emissions V1 -> V2 and VoterRewards V1 -> V2
// ========================================
const emissionsV1 = (await deployProxy("Emissions", [
{
minter: minterAccount.address,
admin: owner.address,
upgrader: owner.address,
contractsAddressManager: owner.address,
decaySettingsManager: owner.address,
b3trAddress: await b3tr.getAddress(),
destinations: [
await xAllocationPool.getAddress(),
owner.address,
await treasury.getAddress(),
config.MIGRATION_ADDRESS,
],
initialXAppAllocation: config.INITIAL_X_ALLOCATION,
cycleDuration: config.EMISSIONS_CYCLE_DURATION,
decaySettings: [
config.EMISSIONS_X_ALLOCATION_DECAY_PERCENTAGE,
config.EMISSIONS_VOTE_2_EARN_DECAY_PERCENTAGE,
config.EMISSIONS_X_ALLOCATION_DECAY_PERIOD,
config.EMISSIONS_VOTE_2_EARN_ALLOCATION_DECAY_PERIOD,
],
treasuryPercentage: config.EMISSIONS_TREASURY_PERCENTAGE,
maxVote2EarnDecay: config.EMISSIONS_MAX_VOTE_2_EARN_DECAY_PERCENTAGE,
migrationAmount: config.MIGRATION_AMOUNT,
},
]));
// Upgrade Emissions to V2
const emissions = (await upgradeProxy("EmissionsV1", "Emissions", await emissionsV1.getAddress(), [config.EMISSIONS_IS_NOT_ALIGNED ?? false], {
version: 2,
}));
// Deploy VoterRewards V1
const voterRewardsV1 = (await deployProxy("VoterRewardsV1", [
owner.address, // admin
owner.address, // upgrader
owner.address, // contractsAddressManager
await emissions.getAddress(),
await galaxyMember.getAddress(),
await b3tr.getAddress(),
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[0, 10, 20, 50, 100, 150, 200, 400, 900, 2400],
]));
// Upgrade VoterRewards to V2
const voterRewardsV2 = (await upgradeProxy("VoterRewardsV1", "VoterRewards", await voterRewardsV1.getAddress(), [], {
version: 2,
}));
// Link VoterRewards to Emissions
await emissions.connect(owner).setVote2EarnAddress(await voterRewardsV2.getAddress());
// ========================================
// DEPLOY: XAllocationVoting V1 -> V2 -> V3
// ========================================
const xAllocationVotingV3 = (await deployAndUpgrade(["XAllocationVotingV1", "XAllocationVotingV2", "XAllocationVotingV3"], [
[
{
vot3Token: await vot3.getAddress(),
quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE,
initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1,
timeLock: await timeLock.getAddress(),
voterRewards: await voterRewardsV2.getAddress(),
emissions: await emissions.getAddress(),
admins: [await timeLock.getAddress(), owner.address],
upgrader: owner.address,
contractsAddressManager: owner.address,
x2EarnAppsAddress: await x2EarnApps.getAddress(),
baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE,
appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP,
votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD,
},
],
[await veBetterPassport.getAddress()],
[],
], {
versions: [undefined, 2, 3],
}));
expect(await xAllocationVotingV3.version()).to.equal("3");
// ========================================
// CONFIGURE: Link contracts and set roles
// ========================================
await emissions.setXAllocationsGovernorAddress(await xAllocationVotingV3.getAddress());
expect(await emissions.xAllocationsGovernor()).to.eql(await xAllocationVotingV3.getAddress());
await xAllocationPool.setXAllocationVotingAddress(await xAllocationVotingV3.getAddress());
expect(await xAllocationPool.xAllocationVoting()).to.eql(await xAllocationVotingV3.getAddress());
await xAllocationPool.setEmissionsAddress(await emissions.getAddress());
expect(await xAllocationPool.emissions()).to.eql(await emissions.getAddress());
// Grant Vote registrar role to XAllocationVoting
await voterRewardsV2
.connect(owner)
.grantRole(await voterRewardsV2.VOTE_REGISTRAR_ROLE(), await xAllocationVotingV3.getAddress());
// Grant admin role to voter rewards for registering x allocation voting
await xAllocationVotingV3
.connect(owner)
.grantRole(await xAllocationVotingV3.DEFAULT_ADMIN_ROLE(), emissions.getAddress());
// Set the emissions address and the admin as the ROUND_STARTER_ROLE in XAllocationVoting
const roundStarterRole = await xAllocationVotingV3.ROUND_STARTER_ROLE();
await xAllocationVotingV3
.connect(owner)
.grantRole(roundStarterRole, await emissions.getAddress())
.then(async (tx) => await tx.wait());
await xAllocationVotingV3
.connect(owner)
.grantRole(roundStarterRole, owner.address)
.then(async (tx) => await tx.wait());
// ========================================
// SETUP: Test users, apps, and initial voting round
// ========================================
const user1 = otherAccounts[0];
const user2 = otherAccounts[1];
const user3 = otherAccounts[2];
// Fund test users with VOT3 tokens for voting
await getVot3Tokens(user1, "1000");
await getVot3Tokens(user2, "1000");
await getVot3Tokens(user3, "1000");
// Create and endorse test apps
const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address));
const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address));
const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address));
await x2EarnApps
.connect(creator1)
.submitApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI");
await x2EarnApps
.connect(creator2)
.submitApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI");
await x2EarnApps
.connect(creator3)
.submitApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI");
await endorseApp(app1Id, otherAccounts[2]);
await endorseApp(app2Id, otherAccounts[3]);
await endorseApp(app3Id, otherAccounts[4]);
// Initialize emissions system
await b3tr.connect(owner).grantRole(await b3tr.MINTER_ROLE(), await emissions.getAddress());
await emissions.connect(minterAccount).bootstrap();
// ========================================
// ROUND 1: Initial voting in V3
// ========================================
await emissions.connect(minterAccount).start();
const roundId1 = await xAllocationVotingV3.currentRoundId();
expect(roundId1).to.equal(1n);
// Execute initial votes
await xAllocationVotingV3.connect(user1).castVote(roundId1, [app1Id], [ethers.parseEther("100")]);
await xAllocationVotingV3
.connect(user2)
.castVote(roundId1, [app1Id, app2Id], [ethers.parseEther("100"), ethers.parseEther("200")]);
// Validate V3 state before first upgrade
await validateContractState(xAllocationVotingV3, "3", 1n, {
1: { [app1Id]: ethers.parseEther("200"), [app2Id]: ethers.parseEther("200"), [app3Id]: ethers.parseEther("0") },
}, { 1: { [user1.address]: true, [user2.address]: true, [user3.address]: false } });
// ========================================
// UPGRADE V3 -> V4: First upgrade test
// ========================================
const xAllocationVotingV4 = (await upgradeProxy("XAllocationVotingV3", "XAllocationVotingV4", await xAllocationVotingV3.getAddress(), [], {
version: 4,
}));
// CRITICAL: Validate state preservation after V3->V4 upgrade
await validateContractState(xAllocationVotingV4, "4", 1n, {
1: { [app1Id]: ethers.parseEther("200"), [app2Id]: ethers.parseEther("200"), [app3Id]: ethers.parseEther("0") },
}, { 1: { [user1.address]: true, [user2.address]: true, [user3.address]: false } });
// ========================================
// UPGRADE V4 -> V5: Continue upgrade chain
// ========================================
const xAllocationVotingV5 = (await upgradeProxy("XAllocationVotingV4", "XAllocationVotingV5", await xAllocationVotingV4.getAddress(), [], {
version: 5,
}));
// CRITICAL: Validate state preservation after V4->V5 upgrade
await validateContractState(xAllocationVotingV5, "5", 1n, {
1: { [app1Id]: ethers.parseEther("200"), [app2Id]: ethers.parseEther("200"), [app3Id]: ethers.parseEther("0") },
}, { 1: { [user1.address]: true, [user2.address]: true, [user3.address]: false } });
expect(await xAllocationVotingV5.state(1n)).to.equal(0n); // Active
// Test voting functionality still works after upgrade
await xAllocationVotingV5.connect(user3).castVote(roundId1, [app1Id], [ethers.parseEther("100")]);
expect(await xAllocationVotingV5.getAppVotes(roundId1, app1Id)).to.equal(ethers.parseEther("300"));
expect(await xAllocationVotingV5.hasVoted(roundId1, user3.address)).to.be.true;
// ========================================
// ROUND 1 -> ROUND 2: Complete cycle and test rewards
// ========================================
const blockNextCycle = await emissions.getNextCycleBlock();
await waitForBlock(Number(blockNextCycle));
expect(await emissions.isCycleEnded(1)).to.be.true;
await emissions.distribute();
const roundId2 = await xAllocationVotingV5.currentRoundId();
expect(roundId2).to.equal(2n);
// Verify rewards distribution works correctly
await expect(xAllocationPool.claim(1, app1Id)).not.to.be.reverted;
await expect(xAllocationPool.claim(1, app2Id)).not.to.be.reverted;
await expect(xAllocationPool.claim(1, app3Id)).not.to.be.reverted;
// ========================================
// ROUND 2: Execute votes before V5->V6 upgrade
// ========================================
await xAllocationVotingV5.connect(user1).castVote(roundId2, [app1Id], [ethers.parseEther("100")]);
// ========================================
// UPGRADE V5 -> V6: Continue upgrade chain
// ========================================
const xAllocationVotingV6 = (await upgradeProxy("XAllocationVotingV5", "XAllocationVotingV6", await xAllocationVotingV5.getAddress(), [], {
version: 6,
}));
// CRITICAL: Validate state preservation after V5->V6 upgrade (including both rounds)
await validateContractState(xAllocationVotingV6, "6", roundId2, {
1: { [app1Id]: ethers.parseEther("300"), [app2Id]: ethers.parseEther("200"), [app3Id]: ethers.parseEther("0") },
2: { [app1Id]: ethers.parseEther("100"), [app2Id]: ethers.parseEther("0"), [app3Id]: ethers.parseEther("0") },
}, {
1: { [user1.address]: true, [user2.address]: true, [user3.address]: true },
2: { [user1.address]: true, [user2.address]: false, [user3.address]: false },
});
expect(await xAllocationVotingV6.state(2n)).to.equal(0n); // Active
// Test voting functionality still works after V6 upgrade
await xAllocationVotingV6.connect(user3).castVote(roundId2, [app1Id], [ethers.parseEther("100")]);
expect(await xAllocationVotingV6.getAppVotes(roundId2, app1Id)).to.equal(ethers.parseEther("200"));
// ========================================
// ROUND 2 -> ROUND 3: Complete cycle and test rewards
// ========================================
await waitForBlock(Number(await emissions.getNextCycleBlock()));
expect(await emissions.isCycleEnded(roundId2)).to.be.true;
await emissions.distribute();
const roundId3 = await xAllocationVotingV6.currentRoundId();
expect(roundId3).to.equal(3n);
// Verify rewards distribution works correctly
await expect(xAllocationPool.claim(2, app1Id)).to.not.be.reverted;
await expect(xAllocationPool.claim(2, app2Id)).to.not.be.reverted;
await expect(xAllocationPool.claim(2, app3Id)).to.not.be.reverted;
// ========================================
// ROUND 3: Execute votes before V6->V7 upgrade
// ========================================
await xAllocationVotingV6.connect(user1).castVote(roundId3, [app1Id], [ethers.parseEther("100")]);
await xAllocationVotingV6.connect(user2).castVote(roundId3, [app1Id], [ethers.parseEther("100")]);
// ========================================
// UPGRADE V6 -> V7: Governance features added
// ========================================
const xAllocationVotingV7 = (await upgradeProxy("XAllocationVotingV6", "XAllocationVotingV7", await xAllocationVotingV6.getAddress(), [], {
version: 7,
}));
// CRITICAL: Grant GOVERNANCE_ROLE to owner and set B3TR Governor in V7
await xAllocationVotingV7.connect(owner).grantRole(await xAllocationVotingV7.GOVERNANCE_ROLE(), owner.address);
await xAllocationVotingV7.connect(owner).setB3TRGovernor(await governor.getAddress());
// CRITICAL: Validate state preservation after V6->V7 upgrade (all 3 rounds)
await validateContractState(xAllocationVotingV7, "7", roundId3, {
1: { [app1Id]: ethers.parseEther("300"), [app2Id]: ethers.parseEther("200"), [app3Id]: ethers.parseEther("0") },
2: { [app1Id]: ethers.parseEther("200"), [app2Id]: ethers.parseEther("0"), [app3Id]: ethers.parseEther("0") },
3: { [app1Id]: ethers.parseEther("200"), [app2Id]: ethers.parseEther("0"), [app3Id]: ethers.parseEther("0") },
}, {
1: { [user1.address]: true, [user2.address]: true, [user3.address]: true },
2: { [user1.address]: true, [user2.address]: false, [user3.address]: true },
3: { [user1.address]: true, [user2.address]: true, [user3.address]: false },
});
expect(await xAllocationVotingV7.state(3n)).to.equal(0n); // Active
// ========================================
// UPGRADE V7 -> V8: Final upgrade with auto-voting features
// ========================================
const AutoVotingLogicV8Lib = await (await ethers.getContractFactory("AutoVotingLogicV8")).deploy();
await AutoVotingLogicV8Lib.waitForDeployment();
const xAllocationVoting = (await upgradeProxy("XAllocationVotingV7", "XAllocationVotingV8", await xAllocationVotingV7.getAddress(), [], {
version: 8,
libraries: {
AutoVotingLogicV8: await AutoVotingLogicV8Lib.getAddress(),
},
}));
// CRITICAL: Grant CONTRACTS_ADDRESS_MANAGER_ROLE and set B3TR Governor in V8
await xAllocationVoting
.connect(owner)
.grantRole(await xAllocationVoting.CONTRACTS_ADDRESS_MANAGER_ROLE(), owner.address);
await xAllocationVoting.connect(owner).setB3TRGovernor(await governor.getAddress());
// ========================================
// FINAL VALIDATION: All historical data preserved in V8
// ========================================
await validateContractState(xAllocationVoting, "8", 3n, {
1: { [app1Id]: ethers.parseEther("300"), [app2Id]: ethers.parseEther("200"), [app3Id]: ethers.parseEther("0") },
2: { [app1Id]: ethers.parseEther("200"), [app2Id]: ethers.parseEther("0"), [app3Id]: ethers.parseEther("0") },
3: { [app1Id]: ethers.parseEther("200"), [app2Id]: ethers.parseEther("0"), [app3Id]: ethers.parseEther("0") },
}, {
1: { [user1.address]: true, [user2.address]: true, [user3.address]: true },
2: { [user1.address]: true, [user2.address]: false, [user3.address]: true },
3: { [user1.address]: true, [user2.address]: true, [user3.address]: false },
});
// Verify all historical data is still intact after final upgrade
expect(await xAllocationVoting.getAppVotes(1, app1Id)).to.equal(ethers.parseEther("300"));
expect(await xAllocationVoting.getAppVotes(2, app1Id)).to.equal(ethers.parseEther("200"));
expect(await xAllocationVoting.hasVoted(1, user1.address)).to.be.true;
expect(await xAllocationVoting.hasVoted(2, user3.address)).to.be.true;
// ========================================
// ROUND 3 -> ROUND 4: Test final functionality
// ========================================
await waitForBlock(Number(await emissions.getNextCycleBlock()));
expect(await emissions.isCycleEnded(roundId3)).to.be.true;
expect(await xAllocationVoting.state(roundId3)).to.equal(1n); // NOT ACTIVE
expect(await xAllocationVoting.votingThreshold()).to.be.greaterThan(0);
await emissions.distribute();
const getRoundId4 = await xAllocationVoting.currentRoundId();
// Test that new votes can still be cast in V8
await xAllocationVoting.connect(user2).castVote(getRoundId4, [app2Id], [ethers.parseEther("150")]);
expect(await xAllocationVoting.getAppVotes(getRoundId4, app2Id)).to.equal(ethers.parseEther("150"));
expect(await xAllocationVoting.hasVoted(getRoundId4, user2.address)).to.be.true;
});
});