@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
JavaScript
"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);
});
});
});