UNPKG

@vechain/vebetterdao-contracts

Version:

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

413 lines (412 loc) 22.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const chai_1 = require("chai"); const hardhat_1 = require("hardhat"); const helpers_1 = require("../../scripts/helpers"); const libraries_1 = require("../../scripts/libraries"); const STAKE_AMOUNT = hardhat_1.ethers.parseEther("100"); const MIN_BET_AMOUNT = hardhat_1.ethers.parseEther("100"); const INITIAL_BALANCE = hardhat_1.ethers.parseEther("1000"); const APP_1 = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app-1")); const ChallengeKind = { Stake: 0, Sponsored: 1 }; const ChallengeVisibility = { Public: 0, Private: 1 }; const ChallengeType = { MaxActions: 0, SplitWin: 1 }; const ChallengeStatus = { Pending: 0, Active: 1, Completed: 2, Cancelled: 3, Invalid: 4 }; const SettlementMode = { None: 0, TopWinners: 1, CreatorRefund: 2, SplitWinCompleted: 3 }; async function deployFixture() { const [admin, alice, bob, carol] = await hardhat_1.ethers.getSigners(); const b3tr = (await (await hardhat_1.ethers.getContractFactory("B3TR")).deploy(admin.address, admin.address, admin.address)); await b3tr.waitForDeployment(); const roundGovernor = (await (await hardhat_1.ethers.getContractFactory("MockRoundGovernor")).deploy()); await roundGovernor.waitForDeployment(); const passport = (await (await hardhat_1.ethers.getContractFactory("MockPassportActions")).deploy()); await passport.waitForDeployment(); const x2EarnApps = (await (await hardhat_1.ethers.getContractFactory("MockX2EarnApps")).deploy()); await x2EarnApps.waitForDeployment(); await x2EarnApps.setAppExists(APP_1, true); const { ChallengeCoreLogic: challengeCoreLogic, ChallengeSettlementLogic: challengeSettlementLogic } = await (0, libraries_1.challengesLibraries)({ logOutput: false }); const challenges = (await (0, helpers_1.deployProxy)("B3TRChallenges", [ { b3trAddress: await b3tr.getAddress(), veBetterPassportAddress: await passport.getAddress(), xAllocationVotingAddress: await roundGovernor.getAddress(), x2EarnAppsAddress: await x2EarnApps.getAddress(), maxChallengeDuration: 4, maxSelectedApps: 5, maxParticipants: 100, minBetAmount: MIN_BET_AMOUNT, }, { admin: admin.address, upgrader: admin.address, contractsAddressManager: admin.address, settingsManager: admin.address, }, ], { ChallengeCoreLogic: await challengeCoreLogic.getAddress(), ChallengeSettlementLogic: await challengeSettlementLogic.getAddress(), })); for (const signer of [admin, alice, bob, carol]) { await b3tr.mint(signer.address, INITIAL_BALANCE); await b3tr.connect(signer).approve(await challenges.getAddress(), INITIAL_BALANCE); } return { admin, alice, bob, carol, b3tr, roundGovernor, passport, x2EarnApps, challenges }; } describe("B3TRChallenges - Passport gating - @shard9b", function () { // ──── joinChallenge: block non-persons in both private and public flows ──── it("rejects joinChallenge from a non-person in a private (Stake) challenge", async function () { const { admin, alice, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); // Stake/Private/MaxActions; admin auto-joins as creator. await challenges.createChallenge({ kind: ChallengeKind.Stake, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: STAKE_AMOUNT, startRound: 2, endRound: 3, threshold: 0, numWinners: 0, appIds: [APP_1], invitees: [alice.address], title: "", description: "", imageURI: "", metadataURI: "", }); await passport.setIsPerson(alice.address, false, "User is blacklisted"); await (0, chai_1.expect)(challenges.connect(alice).joinChallenge(1)) .to.be.revertedWithCustomError(challenges, "NotVerifiedPerson") .withArgs(alice.address, "User is blacklisted"); (0, chai_1.expect)(admin.address).to.not.equal(alice.address); // sanity }); it("rejects joinChallenge from a non-person in a public (Sponsored SplitWin) challenge", async function () { const { alice, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); await challenges.createChallenge({ kind: ChallengeKind.Sponsored, visibility: ChallengeVisibility.Public, challengeType: ChallengeType.SplitWin, stakeAmount: hardhat_1.ethers.parseEther("300"), startRound: 2, endRound: 3, threshold: 3, numWinners: 2, appIds: [], invitees: [], title: "", description: "", imageURI: "", metadataURI: "", }); await passport.setIsPerson(alice.address, false, "User has been signaled too many times"); await (0, chai_1.expect)(challenges.connect(alice).joinChallenge(1)) .to.be.revertedWithCustomError(challenges, "NotVerifiedPerson") .withArgs(alice.address, "User has been signaled too many times"); }); // ──── createChallenge: block non-person creators of stake challenges ──── it("rejects createChallenge when a non-person creator would auto-join a Stake challenge", async function () { const { admin, alice, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); await passport.setIsPerson(admin.address, false, "User is blacklisted"); await (0, chai_1.expect)(challenges.createChallenge({ kind: ChallengeKind.Stake, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: STAKE_AMOUNT, startRound: 2, endRound: 3, threshold: 0, numWinners: 0, appIds: [APP_1], invitees: [alice.address], title: "", description: "", imageURI: "", metadataURI: "", })) .to.be.revertedWithCustomError(challenges, "NotVerifiedPerson") .withArgs(admin.address, "User is blacklisted"); }); it("allows createChallenge with a non-person creator for Sponsored challenges (no auto-join)", async function () { const { admin, alice, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); await passport.setIsPerson(admin.address, false, "User is blacklisted"); // Sponsored creators do not participate, so personhood is not required to fund the prize pool. await challenges.createChallenge({ kind: ChallengeKind.Sponsored, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: STAKE_AMOUNT, startRound: 2, endRound: 3, threshold: 0, numWinners: 0, appIds: [APP_1], invitees: [alice.address], title: "", description: "", imageURI: "", metadataURI: "", }); (0, chai_1.expect)(await challenges.challengeCount()).to.equal(1n); }); // ──── claimChallengePayout: block non-person winners; refund branch stays open ──── it("honours the frozen winner snapshot: a winner who later loses personhood can still claim", async function () { const { admin, alice, bob, b3tr, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); await challenges.createChallenge({ kind: ChallengeKind.Stake, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: STAKE_AMOUNT, startRound: 2, endRound: 3, threshold: 0, numWinners: 0, appIds: [APP_1], invitees: [alice.address, bob.address], title: "", description: "", imageURI: "", metadataURI: "", }); await challenges.connect(alice).joinChallenge(1); await challenges.connect(bob).joinChallenge(1); // alice wins outright at completion. await passport.setUserRoundActionCountApp(admin.address, 2, APP_1, 1); await passport.setUserRoundActionCountApp(alice.address, 2, APP_1, 10); await passport.setUserRoundActionCountApp(bob.address, 2, APP_1, 5); await roundGovernor.setCurrentRoundId(4); await challenges.completeChallenge(1); const challenge = await challenges.getChallenge(1); (0, chai_1.expect)(challenge.settlementMode).to.equal(SettlementMode.TopWinners); (0, chai_1.expect)(challenge.bestScore).to.equal(10n); (0, chai_1.expect)(challenge.bestCount).to.equal(1n); // alice is blacklisted AFTER the challenge ended. The winner set was frozen at completion, // so the verdict cannot retroactively revoke her slot — otherwise the pot would be stranded. await passport.setIsPerson(alice.address, false, "User is blacklisted"); const balanceBefore = await b3tr.balanceOf(alice.address); await challenges.connect(alice).claimChallengePayout(1); (0, chai_1.expect)(await b3tr.balanceOf(alice.address)).to.equal(balanceBefore + hardhat_1.ethers.parseEther("300")); }); it("regression: a tied non-person at completion cannot steal the prize after regaining personhood", async function () { // The bug: completeChallenge filtered non-persons out of bestScore/bestCount, but the original // claimChallengePayout re-derived eligibility live from `actions == bestScore` + a live isPerson check. // A non-person tied with the verified winner at completion could become a person again afterwards and // claim a slot that bestCount never accounted for, taking the full pot and leaving the real winner // to revert with TransferFailed on an empty contract. const { admin, alice, bob, b3tr, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); await challenges.createChallenge({ kind: ChallengeKind.Stake, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: STAKE_AMOUNT, startRound: 2, endRound: 3, threshold: 0, numWinners: 0, appIds: [APP_1], invitees: [alice.address, bob.address], title: "", description: "", imageURI: "", metadataURI: "", }); await challenges.connect(alice).joinChallenge(1); await challenges.connect(bob).joinChallenge(1); // A (alice) and B (bob) both have 10 actions — tied top score. B is a non-person at completion. await passport.setUserRoundActionCountApp(admin.address, 2, APP_1, 0); await passport.setUserRoundActionCountApp(alice.address, 2, APP_1, 10); await passport.setUserRoundActionCountApp(bob.address, 2, APP_1, 10); await passport.setIsPerson(bob.address, false, "User is blacklisted"); await roundGovernor.setCurrentRoundId(4); await challenges.completeChallenge(1); // B is skipped at completion → bestCount == 1, snapshot only contains A. const challenge = await challenges.getChallenge(1); (0, chai_1.expect)(challenge.bestScore).to.equal(10n); (0, chai_1.expect)(challenge.bestCount).to.equal(1n); (0, chai_1.expect)(challenge.settlementMode).to.equal(SettlementMode.TopWinners); // B regains personhood after the snapshot. await passport.setIsPerson(bob.address, true, ""); // B is NOT in the eligible-winner snapshot, so the live attempt reverts. // Without the fix, B would pass _isEligibleForPayout (actions == bestScore) and take the entire pot. await (0, chai_1.expect)(challenges.connect(bob).claimChallengePayout(1)) .to.be.revertedWithCustomError(challenges, "NothingToClaim") .withArgs(1, bob.address); // A claims the full pot exactly once. With the bug present, this would revert with TransferFailed. const balanceBefore = await b3tr.balanceOf(alice.address); await challenges.connect(alice).claimChallengePayout(1); (0, chai_1.expect)(await b3tr.balanceOf(alice.address)).to.equal(balanceBefore + hardhat_1.ethers.parseEther("300")); }); it("allows the creator to collect the CreatorRefund branch even if they are no longer a person", async function () { const { admin, alice, bob, b3tr, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); // Sponsored Private MaxActions. Alice and bob join but both are non-persons at settlement, so // completeChallenge skips them, bestCount stays 0, and the contract enters CreatorRefund mode. await challenges.createChallenge({ kind: ChallengeKind.Sponsored, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: STAKE_AMOUNT, startRound: 2, endRound: 3, threshold: 0, numWinners: 0, appIds: [APP_1], invitees: [alice.address, bob.address], title: "", description: "", imageURI: "", metadataURI: "", }); await challenges.connect(alice).joinChallenge(1); await challenges.connect(bob).joinChallenge(1); await passport.setUserRoundActionCountApp(alice.address, 2, APP_1, 5); await passport.setUserRoundActionCountApp(bob.address, 2, APP_1, 7); await passport.setIsPerson(alice.address, false, "User is blacklisted"); await passport.setIsPerson(bob.address, false, "User is blacklisted"); await roundGovernor.setCurrentRoundId(4); await challenges.completeChallenge(1); const challenge = await challenges.getChallenge(1); (0, chai_1.expect)(challenge.bestCount).to.equal(0n); (0, chai_1.expect)(challenge.settlementMode).to.equal(SettlementMode.CreatorRefund); // Creator also becomes non-person; the refund branch must still pay out. await passport.setIsPerson(admin.address, false, "User is blacklisted"); const balanceBefore = await b3tr.balanceOf(admin.address); await challenges.claimChallengePayout(1); (0, chai_1.expect)(await b3tr.balanceOf(admin.address)).to.equal(balanceBefore + STAKE_AMOUNT); }); // ──── claimSplitWinPrize: block non-persons ──── it("rejects claimSplitWinPrize when a participant is no longer a person", async function () { const { alice, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); await challenges.createChallenge({ kind: ChallengeKind.Sponsored, visibility: ChallengeVisibility.Public, challengeType: ChallengeType.SplitWin, stakeAmount: hardhat_1.ethers.parseEther("300"), startRound: 2, endRound: 3, threshold: 3, numWinners: 2, appIds: [], invitees: [], title: "", description: "", imageURI: "", metadataURI: "", }); await challenges.connect(alice).joinChallenge(1); await roundGovernor.setCurrentRoundId(2); await challenges.syncChallenge(1); await passport.setUserRoundActionCount(alice.address, 2, 5); await passport.setIsPerson(alice.address, false, "User does not meet the criteria to be considered a person"); await (0, chai_1.expect)(challenges.connect(alice).claimSplitWinPrize(1)) .to.be.revertedWithCustomError(challenges, "NotVerifiedPerson") .withArgs(alice.address, "User does not meet the criteria to be considered a person"); }); // ──── completeChallenge: skip non-persons from bestScore selection ──── it("completeChallenge skips non-persons so the prize goes to the top verified participant", async function () { const { admin, alice, bob, b3tr, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); await challenges.createChallenge({ kind: ChallengeKind.Stake, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: STAKE_AMOUNT, startRound: 2, endRound: 3, threshold: 0, numWinners: 0, appIds: [APP_1], invitees: [alice.address, bob.address], title: "", description: "", imageURI: "", metadataURI: "", }); await challenges.connect(alice).joinChallenge(1); await challenges.connect(bob).joinChallenge(1); // alice has the highest score, but she is non-person at settlement. // bob is verified with the runner-up score, and should become the sole top-winner. await passport.setUserRoundActionCountApp(admin.address, 2, APP_1, 1); await passport.setUserRoundActionCountApp(alice.address, 2, APP_1, 100); await passport.setUserRoundActionCountApp(bob.address, 2, APP_1, 7); await passport.setIsPerson(alice.address, false, "User is blacklisted"); await roundGovernor.setCurrentRoundId(4); await challenges.completeChallenge(1); const challenge = await challenges.getChallenge(1); (0, chai_1.expect)(challenge.bestScore).to.equal(7n); (0, chai_1.expect)(challenge.bestCount).to.equal(1n); (0, chai_1.expect)(challenge.settlementMode).to.equal(SettlementMode.TopWinners); // alice cannot claim (both NotEligible — score < bestScore — and would also fail personhood). await (0, chai_1.expect)(challenges.connect(alice).claimChallengePayout(1)).to.be.revertedWithCustomError(challenges, "NothingToClaim"); // bob collects the full pot (3 stakes = 300). const bobBalanceBefore = await b3tr.balanceOf(bob.address); await challenges.connect(bob).claimChallengePayout(1); (0, chai_1.expect)(await b3tr.balanceOf(bob.address)).to.equal(bobBalanceBefore + hardhat_1.ethers.parseEther("300")); }); // ──── Refund paths stay open for non-persons ──── it("claimChallengeRefund still succeeds for a non-person participant on a cancelled stake challenge", async function () { const { alice, b3tr, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); await challenges.createChallenge({ kind: ChallengeKind.Stake, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: STAKE_AMOUNT, startRound: 2, endRound: 3, threshold: 0, numWinners: 0, appIds: [APP_1], invitees: [alice.address], title: "", description: "", imageURI: "", metadataURI: "", }); await challenges.connect(alice).joinChallenge(1); await challenges.cancelChallenge(1); (0, chai_1.expect)(await challenges.getChallengeStatus(1)).to.equal(ChallengeStatus.Cancelled); // alice becomes non-person AFTER joining — she should still get her stake back. await passport.setIsPerson(alice.address, false, "User is blacklisted"); const balanceBefore = await b3tr.balanceOf(alice.address); await challenges.connect(alice).claimChallengeRefund(1); (0, chai_1.expect)(await b3tr.balanceOf(alice.address)).to.equal(balanceBefore + STAKE_AMOUNT); }); it("claimCreatorSplitWinRefund still succeeds for a non-person creator after end round", async function () { const { admin, alice, b3tr, roundGovernor, passport, challenges } = await deployFixture(); await roundGovernor.setCurrentRoundId(1); const sponsorAmount = hardhat_1.ethers.parseEther("300"); await challenges.createChallenge({ kind: ChallengeKind.Sponsored, visibility: ChallengeVisibility.Public, challengeType: ChallengeType.SplitWin, stakeAmount: sponsorAmount, startRound: 2, endRound: 3, threshold: 3, numWinners: 2, appIds: [], invitees: [], title: "", description: "", imageURI: "", metadataURI: "", }); // alice joins so the challenge becomes Active rather than Invalid — but does not claim a slot. await challenges.connect(alice).joinChallenge(1); await roundGovernor.setCurrentRoundId(2); await challenges.syncChallenge(1); (0, chai_1.expect)(await challenges.getChallengeStatus(1)).to.equal(ChallengeStatus.Active); // After endRound, no winners; creator reclaims the whole pool via the refund path even when non-person. await roundGovernor.setCurrentRoundId(4); await passport.setIsPerson(admin.address, false, "User is blacklisted"); const balanceBefore = await b3tr.balanceOf(admin.address); await challenges.claimCreatorSplitWinRefund(1); (0, chai_1.expect)(await b3tr.balanceOf(admin.address)).to.equal(balanceBefore + sponsorAmount); }); });