UNPKG

@vechain/vebetterdao-contracts

Version:

Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.

544 lines (543 loc) 33.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const local_1 = require("@repo/config/contracts/envs/local"); const chai_1 = require("chai"); const hardhat_1 = require("hardhat"); const helpers_1 = require("../../scripts/helpers"); const helpers_2 = require("../helpers"); const xnodes_1 = require("../helpers/xnodes"); let config; let otherAccounts; let owner; describe("X-Apps - V8 Upgrade - @shard15f", function () { beforeEach(async function () { config = (0, local_1.createLocalConfig)(); const contracts = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, deployMocks: true, }); if (!contracts) { throw new Error("Contracts not deployed"); } otherAccounts = contracts.otherAccounts; owner = contracts.owner; }); it("Should upgrade from V7 to V8", async function () { const { timeLock, nodeManagement, veBetterPassport, x2EarnCreator, xAllocationVoting: freshXAllocationVoting, x2EarnRewardsPool, stargateNftMock: freshStargateNftMock, // X2EarnApps V2 administrationUtilsV2, endorsementUtilsV2, voteEligibilityUtilsV2, // X2EarnApps V3 administrationUtilsV3, endorsementUtilsV3, voteEligibilityUtilsV3, // X2EarnApps V4 administrationUtilsV4, endorsementUtilsV4, voteEligibilityUtilsV4, // X2EarnApps V5 administrationUtilsV5, endorsementUtilsV5, voteEligibilityUtilsV5, // X2EarnApps V6 administrationUtilsV6, endorsementUtilsV6, voteEligibilityUtilsV6, // X2EarnApps V7 administrationUtilsV7, endorsementUtilsV7, voteEligibilityUtilsV7, // Latest administrationUtils, endorsementUtils, voteEligibilityUtils, appStorageUtils, } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, deployMocks: true }); const x2EarnAppsV7 = (await (0, helpers_1.deployAndUpgrade)(["X2EarnAppsV1", "X2EarnAppsV2", "X2EarnAppsV3", "X2EarnAppsV4", "X2EarnAppsV5", "X2EarnAppsV6", "X2EarnAppsV7"], [ ["ipfs://", [await timeLock.getAddress(), owner.address], owner.address, owner.address], [ config.XAPP_GRACE_PERIOD, await nodeManagement.getAddress(), await veBetterPassport.getAddress(), await x2EarnCreator.getAddress(), ], [config.X2EARN_NODE_COOLDOWN_PERIOD, await freshXAllocationVoting.getAddress()], [await x2EarnRewardsPool.getAddress()], [], [], [await freshStargateNftMock.getAddress()], ], { versions: [undefined, 2, 3, 4, 5, 6, 7], libraries: [ undefined, { AdministrationUtilsV2: await administrationUtilsV2.getAddress(), EndorsementUtilsV2: await endorsementUtilsV2.getAddress(), VoteEligibilityUtilsV2: await voteEligibilityUtilsV2.getAddress(), }, { AdministrationUtilsV3: await administrationUtilsV3.getAddress(), EndorsementUtilsV3: await endorsementUtilsV3.getAddress(), VoteEligibilityUtilsV3: await voteEligibilityUtilsV3.getAddress(), }, { AdministrationUtilsV4: await administrationUtilsV4.getAddress(), EndorsementUtilsV4: await endorsementUtilsV4.getAddress(), VoteEligibilityUtilsV4: await voteEligibilityUtilsV4.getAddress(), }, { AdministrationUtilsV5: await administrationUtilsV5.getAddress(), EndorsementUtilsV5: await endorsementUtilsV5.getAddress(), VoteEligibilityUtilsV5: await voteEligibilityUtilsV5.getAddress(), }, { AdministrationUtilsV6: await administrationUtilsV6.getAddress(), EndorsementUtilsV6: await endorsementUtilsV6.getAddress(), VoteEligibilityUtilsV6: await voteEligibilityUtilsV6.getAddress(), }, { AdministrationUtilsV7: await administrationUtilsV7.getAddress(), EndorsementUtilsV7: await endorsementUtilsV7.getAddress(), VoteEligibilityUtilsV7: await voteEligibilityUtilsV7.getAddress(), }, ], })); const x2EarnAppsV8 = (await (0, helpers_1.upgradeProxy)("X2EarnAppsV7", "X2EarnApps", await x2EarnAppsV7.getAddress(), [], { version: 8, libraries: { AdministrationUtils: await administrationUtils.getAddress(), EndorsementUtils: await endorsementUtils.getAddress(), VoteEligibilityUtils: await voteEligibilityUtils.getAddress(), AppStorageUtils: await appStorageUtils.getAddress(), }, })); (0, chai_1.expect)(await x2EarnAppsV8.version()).to.equal("8"); // Update X2EarnRewardsPool to point to the upgraded contract const CONTRACTS_ADDRESS_MANAGER_ROLE = await x2EarnRewardsPool.CONTRACTS_ADDRESS_MANAGER_ROLE(); await x2EarnRewardsPool.connect(owner).grantRole(CONTRACTS_ADDRESS_MANAGER_ROLE, owner.address); await x2EarnRewardsPool.connect(owner).setX2EarnApps(await x2EarnAppsV8.getAddress()); // quick sanity: contract still callable const appId = await x2EarnAppsV8.hashAppName(otherAccounts[0].address); await x2EarnAppsV8 .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); (0, chai_1.expect)(await x2EarnAppsV8.isAppUnendorsed(appId)).to.eql(true); }); it("Should preserve storage slots during V7 to V8 upgrade (library refactoring)", async function () { const { timeLock, nodeManagement, veBetterPassport, x2EarnCreator, xAllocationVoting: freshXAllocationVoting, x2EarnRewardsPool, stargateNftMock: freshStargateNftMock, // X2EarnApps V2 administrationUtilsV2, endorsementUtilsV2, voteEligibilityUtilsV2, // X2EarnApps V3 administrationUtilsV3, endorsementUtilsV3, voteEligibilityUtilsV3, // X2EarnApps V4 administrationUtilsV4, endorsementUtilsV4, voteEligibilityUtilsV4, // X2EarnApps V5 administrationUtilsV5, endorsementUtilsV5, voteEligibilityUtilsV5, // X2EarnApps V6 administrationUtilsV6, endorsementUtilsV6, voteEligibilityUtilsV6, // X2EarnApps V7 administrationUtilsV7, endorsementUtilsV7, voteEligibilityUtilsV7, // Latest administrationUtils, endorsementUtils, voteEligibilityUtils, appStorageUtils, } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, deployMocks: true }); // Storage slot locations (ERC-7201) const initialSlotVoteEligibility = BigInt("0xb5b8d618af1ffb8d5bcc4bd23f445ba34ed08d7a16d1e1b5411cfbe7913e5900"); const initialSlotSettings = BigInt("0x83b9a7e51f394efa93107c3888716138908bbbe611dfc86afa3639a826441100"); const initialSlotAppsStorage = BigInt("0xb6909058bd527140b8d55a44344c5e42f1f148f1b3b16df7641882df8dd72900"); const initialSlotAdministration = BigInt("0x5830f0e95c01712d916c34d9e2fa42e9f749b325b67bce7382d70bb99c623500"); const initialEndorsementSlot = BigInt("0xc1a7bcdc0c77e8c77ade4541d1777901ab96ca598d164d89afa5c8dfbfc44300"); // Deploy up to V7 const x2EarnAppsV7 = (await (0, helpers_1.deployAndUpgrade)(["X2EarnAppsV1", "X2EarnAppsV2", "X2EarnAppsV3", "X2EarnAppsV4", "X2EarnAppsV5", "X2EarnAppsV6", "X2EarnAppsV7"], [ ["ipfs://", [await timeLock.getAddress(), owner.address], owner.address, owner.address], [ config.XAPP_GRACE_PERIOD, await nodeManagement.getAddress(), await veBetterPassport.getAddress(), await x2EarnCreator.getAddress(), ], [config.X2EARN_NODE_COOLDOWN_PERIOD, await freshXAllocationVoting.getAddress()], [await x2EarnRewardsPool.getAddress()], [], [], [await freshStargateNftMock.getAddress()], ], { versions: [undefined, 2, 3, 4, 5, 6, 7], libraries: [ undefined, { AdministrationUtilsV2: await administrationUtilsV2.getAddress(), EndorsementUtilsV2: await endorsementUtilsV2.getAddress(), VoteEligibilityUtilsV2: await voteEligibilityUtilsV2.getAddress(), }, { AdministrationUtilsV3: await administrationUtilsV3.getAddress(), EndorsementUtilsV3: await endorsementUtilsV3.getAddress(), VoteEligibilityUtilsV3: await voteEligibilityUtilsV3.getAddress(), }, { AdministrationUtilsV4: await administrationUtilsV4.getAddress(), EndorsementUtilsV4: await endorsementUtilsV4.getAddress(), VoteEligibilityUtilsV4: await voteEligibilityUtilsV4.getAddress(), }, { AdministrationUtilsV5: await administrationUtilsV5.getAddress(), EndorsementUtilsV5: await endorsementUtilsV5.getAddress(), VoteEligibilityUtilsV5: await voteEligibilityUtilsV5.getAddress(), }, { AdministrationUtilsV6: await administrationUtilsV6.getAddress(), EndorsementUtilsV6: await endorsementUtilsV6.getAddress(), VoteEligibilityUtilsV6: await voteEligibilityUtilsV6.getAddress(), }, { AdministrationUtilsV7: await administrationUtilsV7.getAddress(), EndorsementUtilsV7: await endorsementUtilsV7.getAddress(), VoteEligibilityUtilsV7: await voteEligibilityUtilsV7.getAddress(), }, ], })); // Verify V7 state (0, chai_1.expect)(await x2EarnAppsV7.version()).to.equal("7"); (0, chai_1.expect)(await x2EarnAppsV7.baseURI()).to.equal("ipfs://"); (0, chai_1.expect)(await x2EarnAppsV7.gracePeriod()).to.equal(config.XAPP_GRACE_PERIOD); (0, chai_1.expect)(await x2EarnAppsV7.cooldownPeriod()).to.equal(config.X2EARN_NODE_COOLDOWN_PERIOD); // Capture storage slots BEFORE upgrade const storageSlotsV7 = await (0, helpers_2.getStorageSlots)(x2EarnAppsV7.getAddress(), initialSlotVoteEligibility, initialSlotSettings, initialSlotAppsStorage, initialSlotAdministration, initialEndorsementSlot); // Upgrade to V8 const x2EarnAppsV8 = (await (0, helpers_1.upgradeProxy)("X2EarnAppsV7", "X2EarnApps", await x2EarnAppsV7.getAddress(), [], { version: 8, libraries: { AdministrationUtils: await administrationUtils.getAddress(), EndorsementUtils: await endorsementUtils.getAddress(), VoteEligibilityUtils: await voteEligibilityUtils.getAddress(), AppStorageUtils: await appStorageUtils.getAddress(), }, })); // Verify version updated (0, chai_1.expect)(await x2EarnAppsV8.version()).to.equal("8"); // Capture storage slots AFTER upgrade const storageSlotsV8 = await (0, helpers_2.getStorageSlots)(x2EarnAppsV8.getAddress(), initialSlotVoteEligibility, initialSlotSettings, initialSlotAppsStorage, initialSlotAdministration, initialEndorsementSlot); // Verify all storage slots are preserved for (let i = 0; i < storageSlotsV7.length; i++) { (0, chai_1.expect)(storageSlotsV7[i]).to.equal(storageSlotsV8[i], `Storage slot ${i} changed after upgrade`); } // Verify contract settings are preserved (0, chai_1.expect)(await x2EarnAppsV8.baseURI()).to.equal("ipfs://"); (0, chai_1.expect)(await x2EarnAppsV8.gracePeriod()).to.equal(config.XAPP_GRACE_PERIOD); (0, chai_1.expect)(await x2EarnAppsV8.cooldownPeriod()).to.equal(config.X2EARN_NODE_COOLDOWN_PERIOD); }); describe("seedEndorsement", function () { it("Should seed endorsement correctly", async function () { const { x2EarnApps, otherAccounts, owner } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); // Grant MIGRATION_ROLE to owner const MIGRATION_ROLE = await x2EarnApps.MIGRATION_ROLE(); await x2EarnApps.connect(owner).grantRole(MIGRATION_ROLE, owner.address); // Submit an app await x2EarnApps .connect(owner) .submitApp(otherAccounts[1].address, otherAccounts[1].address, owner.address, "metadataURI"); const appId = await x2EarnApps.hashAppName(otherAccounts[1].address); // Create a node holder with level 7 (MjolnirX) const nodeId = await (0, helpers_2.createNodeHolder)(7, otherAccounts[2]); // Seed endorsement await x2EarnApps.connect(owner).seedEndorsement(appId, nodeId, 49); // Verify endorsement score (0, chai_1.expect)(await x2EarnApps.getScore(appId)).to.equal(49); // Verify endorsers list const endorsers = await x2EarnApps.getEndorsers(appId); (0, chai_1.expect)(endorsers.length).to.equal(1); }); it("Should update existing endorsement on duplicate seed instead of creating new entry", async function () { const { x2EarnApps, otherAccounts, owner } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); // Grant MIGRATION_ROLE to owner const MIGRATION_ROLE = await x2EarnApps.MIGRATION_ROLE(); await x2EarnApps.connect(owner).grantRole(MIGRATION_ROLE, owner.address); // Submit an app await x2EarnApps .connect(owner) .submitApp(otherAccounts[1].address, otherAccounts[1].address, owner.address, "metadataURI"); const appId = await x2EarnApps.hashAppName(otherAccounts[1].address); // Create a node holder with level 7 (MjolnirX) const nodeId = await (0, helpers_2.createNodeHolder)(7, otherAccounts[2]); // Seed endorsement first time with 20 points await x2EarnApps.connect(owner).seedEndorsement(appId, nodeId, 20); (0, chai_1.expect)(await x2EarnApps.getScore(appId)).to.equal(20); // Get endorsers count after first seed const endorsersAfterFirst = await x2EarnApps.getEndorsers(appId); (0, chai_1.expect)(endorsersAfterFirst.length).to.equal(1); // Seed endorsement second time with 30 points (duplicate) await x2EarnApps.connect(owner).seedEndorsement(appId, nodeId, 30); // Should update to 30, not add to 50 (duplicate guard) (0, chai_1.expect)(await x2EarnApps.getScore(appId)).to.equal(30); // Endorsers count should remain 1, not 2 const endorsersAfterSecond = await x2EarnApps.getEndorsers(appId); (0, chai_1.expect)(endorsersAfterSecond.length).to.equal(1); }); it("Should revert seeding after migration is complete", async function () { const { x2EarnApps, otherAccounts, owner } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); // Grant MIGRATION_ROLE to owner const MIGRATION_ROLE = await x2EarnApps.MIGRATION_ROLE(); await x2EarnApps.connect(owner).grantRole(MIGRATION_ROLE, owner.address); // Submit an app await x2EarnApps .connect(owner) .submitApp(otherAccounts[1].address, otherAccounts[1].address, owner.address, "metadataURI"); const appId = await x2EarnApps.hashAppName(otherAccounts[1].address); // Create a node holder with level 7 (MjolnirX) const nodeId = await (0, helpers_2.createNodeHolder)(7, otherAccounts[2]); // Mark migration complete await x2EarnApps.connect(owner).markMigrationComplete(); // Seed endorsement should revert (error comes from EndorsementUtils library) await (0, chai_1.expect)(x2EarnApps.connect(owner).seedEndorsement(appId, nodeId, 49)).to.be.reverted; }); it("Should only allow MIGRATION_ROLE to seed endorsements", async function () { const { x2EarnApps, otherAccounts, owner } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); // Submit an app await x2EarnApps .connect(owner) .submitApp(otherAccounts[1].address, otherAccounts[1].address, owner.address, "metadataURI"); const appId = await x2EarnApps.hashAppName(otherAccounts[1].address); // Create a node holder with level 7 (MjolnirX) const nodeId = await (0, helpers_2.createNodeHolder)(7, otherAccounts[2]); // Random user without MIGRATION_ROLE should not be able to seed await (0, chai_1.expect)(x2EarnApps.connect(otherAccounts[3]).seedEndorsement(appId, nodeId, 49)).to.be.reverted; }); }); it("Should emit AppAdded event when app reaches endorsement threshold on V7 and V8", async function () { const { timeLock, nodeManagement, veBetterPassport, x2EarnCreator, xAllocationVoting: freshXAllocationVoting, x2EarnRewardsPool, stargateNftMock: freshStargateNftMock, administrationUtilsV2, endorsementUtilsV2, voteEligibilityUtilsV2, administrationUtilsV3, endorsementUtilsV3, voteEligibilityUtilsV3, administrationUtilsV4, endorsementUtilsV4, voteEligibilityUtilsV4, administrationUtilsV5, endorsementUtilsV5, voteEligibilityUtilsV5, administrationUtilsV6, endorsementUtilsV6, voteEligibilityUtilsV6, administrationUtilsV7, endorsementUtilsV7, voteEligibilityUtilsV7, administrationUtils, endorsementUtils, voteEligibilityUtils, appStorageUtils, } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, deployMocks: true }); // Deploy V1 → V7 const x2EarnAppsV7 = (await (0, helpers_1.deployAndUpgrade)(["X2EarnAppsV1", "X2EarnAppsV2", "X2EarnAppsV3", "X2EarnAppsV4", "X2EarnAppsV5", "X2EarnAppsV6", "X2EarnAppsV7"], [ ["ipfs://", [await timeLock.getAddress(), owner.address], owner.address, owner.address], [ config.XAPP_GRACE_PERIOD, await nodeManagement.getAddress(), await veBetterPassport.getAddress(), await x2EarnCreator.getAddress(), ], [config.X2EARN_NODE_COOLDOWN_PERIOD, await freshXAllocationVoting.getAddress()], [await x2EarnRewardsPool.getAddress()], [], [], [await freshStargateNftMock.getAddress()], ], { versions: [undefined, 2, 3, 4, 5, 6, 7], libraries: [ undefined, { AdministrationUtilsV2: await administrationUtilsV2.getAddress(), EndorsementUtilsV2: await endorsementUtilsV2.getAddress(), VoteEligibilityUtilsV2: await voteEligibilityUtilsV2.getAddress(), }, { AdministrationUtilsV3: await administrationUtilsV3.getAddress(), EndorsementUtilsV3: await endorsementUtilsV3.getAddress(), VoteEligibilityUtilsV3: await voteEligibilityUtilsV3.getAddress(), }, { AdministrationUtilsV4: await administrationUtilsV4.getAddress(), EndorsementUtilsV4: await endorsementUtilsV4.getAddress(), VoteEligibilityUtilsV4: await voteEligibilityUtilsV4.getAddress(), }, { AdministrationUtilsV5: await administrationUtilsV5.getAddress(), EndorsementUtilsV5: await endorsementUtilsV5.getAddress(), VoteEligibilityUtilsV5: await voteEligibilityUtilsV5.getAddress(), }, { AdministrationUtilsV6: await administrationUtilsV6.getAddress(), EndorsementUtilsV6: await endorsementUtilsV6.getAddress(), VoteEligibilityUtilsV6: await voteEligibilityUtilsV6.getAddress(), }, { AdministrationUtilsV7: await administrationUtilsV7.getAddress(), EndorsementUtilsV7: await endorsementUtilsV7.getAddress(), VoteEligibilityUtilsV7: await voteEligibilityUtilsV7.getAddress(), }, ], })); // Point rewards pool to this proxy so submitApp can enable rewards const CONTRACTS_ADDRESS_MANAGER_ROLE = await x2EarnRewardsPool.CONTRACTS_ADDRESS_MANAGER_ROLE(); await x2EarnRewardsPool.connect(owner).grantRole(CONTRACTS_ADDRESS_MANAGER_ROLE, owner.address); await x2EarnRewardsPool.connect(owner).setX2EarnApps(await x2EarnAppsV7.getAddress()); // Allow the V7 proxy to call setAppSecurity on VeBetterPassport await veBetterPassport .connect(owner) .grantRole(await veBetterPassport.ACTION_SCORE_MANAGER_ROLE(), await x2EarnAppsV7.getAddress()); // Grant GOVERNANCE_ROLE to owner so we can call governance-gated functions const GOVERNANCE_ROLE = await x2EarnAppsV7.GOVERNANCE_ROLE(); await x2EarnAppsV7.connect(owner).grantRole(GOVERNANCE_ROLE, owner.address); // --- V7: Submit app and endorse to threshold --- // otherAccounts[0..7] already have creator NFTs from the deploy — use higher indices const app1Creator = otherAccounts[8]; await x2EarnCreator.connect(owner).safeMint(app1Creator.address); const app1Name = "V7TestApp"; await x2EarnAppsV7.connect(app1Creator).submitApp(app1Creator.address, app1Creator.address, app1Name, "uri"); const app1Id = await x2EarnAppsV7.hashAppName(app1Name); (0, chai_1.expect)(await x2EarnAppsV7.isAppUnendorsed(app1Id)).to.eql(true); // Create MjolnirX node (level 7 = 100 points, meets threshold of 100) const nodeId1 = await (0, helpers_2.createNodeHolder)(7, otherAccounts[9]); // Endorse on V7 — single node with 100 points reaches the threshold const v7EndorseTx = await x2EarnAppsV7.connect(otherAccounts[9]).endorseApp(app1Id, nodeId1); const v7Receipt = await v7EndorseTx.wait(); if (!v7Receipt) throw new Error("No receipt"); // Verify AppAdded event emitted on V7 with appAvailableForAllocationVoting = true const v7AppAddedEvents = (0, helpers_2.filterEventsByName)(v7Receipt.logs, "AppAdded"); (0, chai_1.expect)(v7AppAddedEvents.length).to.equal(1); const v7Decoded = x2EarnAppsV7.interface.parseLog({ topics: v7AppAddedEvents[0].topics, data: v7AppAddedEvents[0].data, }); (0, chai_1.expect)(v7Decoded?.args[0]).to.equal(app1Id); // id (0, chai_1.expect)(v7Decoded?.args[1]).to.equal(app1Creator.address); // teamWalletAddress (0, chai_1.expect)(v7Decoded?.args[2]).to.equal(app1Name); // name (0, chai_1.expect)(v7Decoded?.args[3]).to.equal(true); // appAvailableForAllocationVoting (0, chai_1.expect)(await x2EarnAppsV7.appExists(app1Id)).to.eql(true); (0, chai_1.expect)(await x2EarnAppsV7.isAppUnendorsed(app1Id)).to.eql(false); // --- Upgrade to V8 --- const x2EarnAppsV8 = (await (0, helpers_1.upgradeProxy)("X2EarnAppsV7", "X2EarnApps", await x2EarnAppsV7.getAddress(), [49, 110], { version: 8, libraries: { AdministrationUtils: await administrationUtils.getAddress(), EndorsementUtils: await endorsementUtils.getAddress(), VoteEligibilityUtils: await voteEligibilityUtils.getAddress(), AppStorageUtils: await appStorageUtils.getAddress(), }, })); (0, chai_1.expect)(await x2EarnAppsV8.version()).to.equal("8"); await x2EarnRewardsPool.connect(owner).setX2EarnApps(await x2EarnAppsV8.getAddress()); // --- V8: Submit a new app and endorse to threshold --- const app2Creator = otherAccounts[10]; await x2EarnCreator.connect(owner).safeMint(app2Creator.address); const app2Name = "V8TestApp"; await x2EarnAppsV8.connect(app2Creator).submitApp(app2Creator.address, app2Creator.address, app2Name, "uri"); const app2Id = await x2EarnAppsV8.hashAppName(app2Name); (0, chai_1.expect)(await x2EarnAppsV8.isAppUnendorsed(app2Id)).to.eql(true); // V8 limits 49 points per node per app, so we need 3 endorsers: 49 + 49 + 2 = 100 const nodeId2 = await (0, helpers_2.createNodeHolder)(7, otherAccounts[11]); const nodeId3 = await (0, helpers_2.createNodeHolder)(7, otherAccounts[12]); const nodeId4 = await (0, helpers_2.createNodeHolder)(7, otherAccounts[13]); await x2EarnAppsV8.connect(otherAccounts[11]).endorseApp(app2Id, nodeId2, 49); await x2EarnAppsV8.connect(otherAccounts[12]).endorseApp(app2Id, nodeId3, 49); // Third endorsement crosses the 100-point threshold — should emit AppAdded const v8EndorseTx = await x2EarnAppsV8.connect(otherAccounts[13]).endorseApp(app2Id, nodeId4, 2); const v8Receipt = await v8EndorseTx.wait(); if (!v8Receipt) throw new Error("No receipt"); // Verify AppAdded event emitted on V8 with appAvailableForAllocationVoting = true const v8AppAddedEvents = (0, helpers_2.filterEventsByName)(v8Receipt.logs, "AppAdded"); (0, chai_1.expect)(v8AppAddedEvents.length).to.equal(1); const v8Decoded = x2EarnAppsV8.interface.parseLog({ topics: v8AppAddedEvents[0].topics, data: v8AppAddedEvents[0].data, }); (0, chai_1.expect)(v8Decoded?.args[0]).to.equal(app2Id); // id (0, chai_1.expect)(v8Decoded?.args[1]).to.equal(app2Creator.address); // teamWalletAddress (0, chai_1.expect)(v8Decoded?.args[2]).to.equal(app2Name); // name (0, chai_1.expect)(v8Decoded?.args[3]).to.equal(true); // appAvailableForAllocationVoting (0, chai_1.expect)(await x2EarnAppsV8.appExists(app2Id)).to.eql(true); (0, chai_1.expect)(await x2EarnAppsV8.isAppUnendorsed(app2Id)).to.eql(false); }); describe("selfBlacklistApp", function () { it("App admin can self-blacklist their app", async function () { const { x2EarnApps, otherAccounts, owner } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); // Submit and endorse an app await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); const appId = await x2EarnApps.hashAppName(otherAccounts[0].address); await (0, xnodes_1.endorseApp)(appId, otherAccounts[0]); // Verify app is eligible and not blacklisted (0, chai_1.expect)(await x2EarnApps.isEligibleNow(appId)).to.eql(true); (0, chai_1.expect)(await x2EarnApps.isBlacklisted(appId)).to.eql(false); // App admin self-blacklists await x2EarnApps.connect(otherAccounts[0]).selfBlacklistApp(appId); // App should be blacklisted and not eligible (0, chai_1.expect)(await x2EarnApps.isBlacklisted(appId)).to.eql(true); (0, chai_1.expect)(await x2EarnApps.isEligibleNow(appId)).to.eql(false); }); it("Should emit AppSelfBlacklisted event", async function () { const { x2EarnApps, otherAccounts, owner } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); const appId = await x2EarnApps.hashAppName(otherAccounts[0].address); await (0, xnodes_1.endorseApp)(appId, otherAccounts[0]); const tx = await x2EarnApps.connect(otherAccounts[0]).selfBlacklistApp(appId); const receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); const events = (0, helpers_2.filterEventsByName)(receipt.logs, "AppSelfBlacklisted"); (0, chai_1.expect)(events.length).to.equal(1); const decoded = x2EarnApps.interface.parseLog({ topics: events[0].topics, data: events[0].data, }); (0, chai_1.expect)(decoded?.args[0]).to.equal(appId); (0, chai_1.expect)(decoded?.args[1]).to.equal(otherAccounts[0].address); }); it("Should revoke creator NFTs on self-blacklist", async function () { const { x2EarnApps, otherAccounts, owner, x2EarnCreator } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); const appId = await x2EarnApps.hashAppName(otherAccounts[0].address); await (0, xnodes_1.endorseApp)(appId, otherAccounts[0]); // Creator should have NFT before delist (0, chai_1.expect)(await x2EarnCreator.balanceOf(owner.address)).to.be.greaterThan(0n); await x2EarnApps.connect(otherAccounts[0]).selfBlacklistApp(appId); // Creator apps count should be 0 after delist (0, chai_1.expect)(await x2EarnApps.creatorApps(owner.address)).to.eql(0n); }); it("Non-admin cannot self-blacklist an app", async function () { const { x2EarnApps, otherAccounts, owner } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); const appId = await x2EarnApps.hashAppName(otherAccounts[0].address); await (0, xnodes_1.endorseApp)(appId, otherAccounts[0]); // otherAccounts[1] is not the app admin await (0, helpers_2.catchRevert)(x2EarnApps.connect(otherAccounts[1]).selfBlacklistApp(appId)); }); it("Cannot self-blacklist a non-existent app", async function () { const { x2EarnApps, otherAccounts } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); const fakeAppId = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("NonExistentApp")); await (0, helpers_2.catchRevert)(x2EarnApps.connect(otherAccounts[0]).selfBlacklistApp(fakeAppId)); }); it("Cannot self-blacklist an already blacklisted app", async function () { const { x2EarnApps, otherAccounts, owner } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); const appId = await x2EarnApps.hashAppName(otherAccounts[0].address); await (0, xnodes_1.endorseApp)(appId, otherAccounts[0]); // Blacklist via governance first await x2EarnApps.connect(owner).setVotingEligibility(appId, false); (0, chai_1.expect)(await x2EarnApps.isBlacklisted(appId)).to.eql(true); // Self-delist should revert since app is already blacklisted await (0, helpers_2.catchRevert)(x2EarnApps.connect(otherAccounts[0]).selfBlacklistApp(appId)); }); it("Governance can restore a self-blacklisted app", async function () { const { x2EarnApps, otherAccounts, owner } = await (0, helpers_2.getOrDeployContractInstances)({ forceDeploy: true, }); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); const appId = await x2EarnApps.hashAppName(otherAccounts[0].address); await (0, xnodes_1.endorseApp)(appId, otherAccounts[0]); // Self-delist await x2EarnApps.connect(otherAccounts[0]).selfBlacklistApp(appId); (0, chai_1.expect)(await x2EarnApps.isBlacklisted(appId)).to.eql(true); (0, chai_1.expect)(await x2EarnApps.isEligibleNow(appId)).to.eql(false); // Governance restores the app await x2EarnApps.connect(owner).setVotingEligibility(appId, true); (0, chai_1.expect)(await x2EarnApps.isBlacklisted(appId)).to.eql(false); (0, chai_1.expect)(await x2EarnApps.isEligibleNow(appId)).to.eql(true); }); }); });