UNPKG

@vechain/vebetterdao-contracts

Version:

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

347 lines (346 loc) 18.1 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("1"); const MIN_BET_AMOUNT = hardhat_1.ethers.parseEther("1"); const APP_IDS = Array.from({ length: 5 }, (_, i) => hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(`app-${i + 1}`))); const ChallengeKind = { Stake: 0, Sponsored: 1 }; const ChallengeVisibility = { Private: 1 }; const ChallengeType = { MaxActions: 0 }; const MAX_PARTICIPANTS = 100; const MAX_DURATION = 4; const START_ROUND = 2; const END_ROUND = START_ROUND + MAX_DURATION - 1; // 4 rounds inclusive: 2,3,4,5 describe("B3TRChallenges - complete gas worst case @shard-gas", function () { this.timeout(600000); async function deployFixtureWorstCase() { const [admin] = 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(); const { ChallengeCoreLogic: challengeCoreLogic, ChallengeSettlementLogic: challengeSettlementLogic } = await (0, libraries_1.challengesLibraries)({ logOutput: false }); for (const appId of APP_IDS) { await x2EarnApps.setAppExists(appId, true); } 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: MAX_DURATION, maxSelectedApps: 5, maxParticipants: MAX_PARTICIPANTS, minBetAmount: MIN_BET_AMOUNT, }, { admin: admin.address, upgrader: admin.address, contractsAddressManager: admin.address, settingsManager: admin.address, }, ], { ChallengeCoreLogic: await challengeCoreLogic.getAddress(), ChallengeSettlementLogic: await challengeSettlementLogic.getAddress(), })); return { admin, b3tr, roundGovernor, passport, x2EarnApps, challenges }; } async function makeFundedWallets(count, b3tr, challengesAddress) { const wallets = []; for (let i = 0; i < count; i++) { const w = hardhat_1.ethers.Wallet.createRandom().connect(hardhat_1.ethers.provider); wallets.push(w); } // Fund ETH for gas + mint B3TR + approve const [admin] = await hardhat_1.ethers.getSigners(); for (const w of wallets) { await hardhat_1.network.provider.send("hardhat_setBalance", [w.address, "0x56BC75E2D63100000"]); // 100 ETH await b3tr.connect(admin).mint(w.address, STAKE_AMOUNT); await b3tr.connect(w).approve(challengesAddress, STAKE_AMOUNT); } return wallets; } it("measures completeChallenge gas at worst case (100 participants × 4 rounds × 5 apps, Sponsored Private MaxActions)", async function () { const { admin, b3tr, roundGovernor, passport, challenges } = await deployFixtureWorstCase(); await roundGovernor.setCurrentRoundId(1); const sponsorAmount = STAKE_AMOUNT * BigInt(MAX_PARTICIPANTS); await b3tr.mint(admin.address, sponsorAmount); await b3tr.connect(admin).approve(await challenges.getAddress(), sponsorAmount); const joiners = await makeFundedWallets(MAX_PARTICIPANTS, b3tr, await challenges.getAddress()); await challenges.connect(admin).createChallenge({ kind: ChallengeKind.Sponsored, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: sponsorAmount, startRound: START_ROUND, endRound: END_ROUND, threshold: 0, numWinners: 0, appIds: APP_IDS, invitees: joiners.map(w => w.address), title: "", description: "", imageURI: "", metadataURI: "", }); for (const w of joiners) { await challenges.connect(w).joinChallenge(1); } for (const w of joiners) { for (let round = START_ROUND; round <= END_ROUND; round++) { for (const appId of APP_IDS) { await passport.setUserRoundActionCountApp(w.address, round, appId, 1); } } } await roundGovernor.setCurrentRoundId(END_ROUND + 1); const tx = await challenges.connect(admin).completeChallenge(1); const receipt = await tx.wait(); const gasUsed = receipt.gasUsed; const VECHAIN_BLOCK_GAS = 40000000n; const ETH_BLOCK_GAS = 30000000n; // eslint-disable-next-line no-console console.log(`\n========== completeChallenge WORST-CASE gas ==========`); // eslint-disable-next-line no-console console.log(`Participants: ${MAX_PARTICIPANTS}`); // eslint-disable-next-line no-console console.log(`Rounds: ${MAX_DURATION}`); // eslint-disable-next-line no-console console.log(`Apps: ${APP_IDS.length}`); // eslint-disable-next-line no-console console.log(`Cross-contract calls: ${MAX_PARTICIPANTS * MAX_DURATION * APP_IDS.length}`); // eslint-disable-next-line no-console console.log(`Gas used: ${gasUsed.toString()}`); // eslint-disable-next-line no-console console.log(`Fraction of VeChain 40M block: ${(Number(gasUsed) / Number(VECHAIN_BLOCK_GAS)).toFixed(3)}`); // eslint-disable-next-line no-console console.log(`Fraction of Ethereum 30M block: ${(Number(gasUsed) / Number(ETH_BLOCK_GAS)).toFixed(3)}`); // eslint-disable-next-line no-console console.log(`======================================================\n`); (0, chai_1.expect)(gasUsed).to.be.gt(0n); }); it("measures completeChallenge gas at 5× the configured maxes to check DoS ceiling", async function () { // Raise maxParticipants to 500 (keep 4 rounds × 5 apps). If this still fits in 40M → DoS claim is false even at 5×. const [admin] = 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(); const { ChallengeCoreLogic: challengeCoreLogic, ChallengeSettlementLogic: challengeSettlementLogic } = await (0, libraries_1.challengesLibraries)({ logOutput: false }); for (const appId of APP_IDS) await x2EarnApps.setAppExists(appId, true); const HUGE_PARTICIPANTS = 500; 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: MAX_DURATION, maxSelectedApps: 5, maxParticipants: HUGE_PARTICIPANTS, minBetAmount: MIN_BET_AMOUNT, }, { admin: admin.address, upgrader: admin.address, contractsAddressManager: admin.address, settingsManager: admin.address, }, ], { ChallengeCoreLogic: await challengeCoreLogic.getAddress(), ChallengeSettlementLogic: await challengeSettlementLogic.getAddress(), })); await roundGovernor.setCurrentRoundId(1); const sponsorAmount = STAKE_AMOUNT * BigInt(HUGE_PARTICIPANTS); await b3tr.mint(admin.address, sponsorAmount); await b3tr.connect(admin).approve(await challenges.getAddress(), sponsorAmount); const joiners = await makeFundedWallets(HUGE_PARTICIPANTS, b3tr, await challenges.getAddress()); await challenges.connect(admin).createChallenge({ kind: ChallengeKind.Sponsored, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: sponsorAmount, startRound: START_ROUND, endRound: END_ROUND, threshold: 0, numWinners: 0, appIds: APP_IDS, invitees: joiners.map(w => w.address), title: "", description: "", imageURI: "", metadataURI: "", }); for (const w of joiners) await challenges.connect(w).joinChallenge(1); for (const w of joiners) { for (let round = START_ROUND; round <= END_ROUND; round++) { for (const appId of APP_IDS) { await passport.setUserRoundActionCountApp(w.address, round, appId, 1); } } } await roundGovernor.setCurrentRoundId(END_ROUND + 1); const tx = await challenges.connect(admin).completeChallenge(1); const receipt = await tx.wait(); const gasUsed = receipt.gasUsed; // eslint-disable-next-line no-console console.log(`\n========== 5× participants (500 × 4 × 5 = 10000 calls) ==========`); // eslint-disable-next-line no-console console.log(`Gas used: ${gasUsed.toString()} (40M block fraction: ${(Number(gasUsed) / 40000000).toFixed(3)})`); // eslint-disable-next-line no-console console.log(`==================================================================\n`); (0, chai_1.expect)(gasUsed).to.be.gt(0n); }); it("sweeps participant counts at 2 rounds × 5 apps to find VeChain-40M ceiling", async function () { // We re-use the same mock stack and vary maxParticipants / actual joiners. const CANDIDATES = [400, 600, 800, 1000]; const DURATION_2 = 2; const END_ROUND_2 = START_ROUND + DURATION_2 - 1; // rounds 2, 3 const results = []; for (const N of CANDIDATES) { const [admin] = 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(); const { ChallengeCoreLogic: challengeCoreLogic, ChallengeSettlementLogic: challengeSettlementLogic } = await (0, libraries_1.challengesLibraries)({ logOutput: false }); for (const appId of APP_IDS) await x2EarnApps.setAppExists(appId, true); 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: DURATION_2, maxSelectedApps: 5, maxParticipants: N, minBetAmount: MIN_BET_AMOUNT, }, { admin: admin.address, upgrader: admin.address, contractsAddressManager: admin.address, settingsManager: admin.address, }, ], { ChallengeCoreLogic: await challengeCoreLogic.getAddress(), ChallengeSettlementLogic: await challengeSettlementLogic.getAddress(), })); await roundGovernor.setCurrentRoundId(1); const sponsorAmount = STAKE_AMOUNT * BigInt(N); await b3tr.mint(admin.address, sponsorAmount); await b3tr.connect(admin).approve(await challenges.getAddress(), sponsorAmount); const joiners = await makeFundedWallets(N, b3tr, await challenges.getAddress()); await challenges.connect(admin).createChallenge({ kind: ChallengeKind.Sponsored, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: sponsorAmount, startRound: START_ROUND, endRound: END_ROUND_2, threshold: 0, numWinners: 0, appIds: APP_IDS, invitees: joiners.map(w => w.address), title: "", description: "", imageURI: "", metadataURI: "", }); for (const w of joiners) await challenges.connect(w).joinChallenge(1); for (const w of joiners) { for (let round = START_ROUND; round <= END_ROUND_2; round++) { for (const appId of APP_IDS) { await passport.setUserRoundActionCountApp(w.address, round, appId, 1); } } } await roundGovernor.setCurrentRoundId(END_ROUND_2 + 1); let gasUsed = null; try { const tx = await challenges.connect(admin).completeChallenge(1, { gasLimit: 50000000 }); const receipt = await tx.wait(); gasUsed = receipt.gasUsed; } catch { gasUsed = null; // out of gas } results.push({ n: N, gas: gasUsed }); // eslint-disable-next-line no-console console.log(` N=${N} → ${gasUsed === null ? "OUT OF GAS" : `${gasUsed.toString()} gas (40M frac: ${(Number(gasUsed) / 40000000).toFixed(3)})`}`); } // eslint-disable-next-line no-console console.log(`\n========== 2 rounds × 5 apps sweep ==========`); for (const r of results) { // eslint-disable-next-line no-console console.log(` participants=${r.n} gas=${r.gas === null ? "OOG" : r.gas.toString()}`); } // eslint-disable-next-line no-console console.log(`=============================================\n`); }); it("measures completeChallenge gas at worst case (allApps=true, 100 participants × 4 rounds)", async function () { const { admin, b3tr, roundGovernor, passport, challenges } = await deployFixtureWorstCase(); await roundGovernor.setCurrentRoundId(1); const sponsorAmount = STAKE_AMOUNT * BigInt(MAX_PARTICIPANTS); await b3tr.mint(admin.address, sponsorAmount); await b3tr.connect(admin).approve(await challenges.getAddress(), sponsorAmount); const joiners = await makeFundedWallets(MAX_PARTICIPANTS, b3tr, await challenges.getAddress()); await challenges.connect(admin).createChallenge({ kind: ChallengeKind.Sponsored, visibility: ChallengeVisibility.Private, challengeType: ChallengeType.MaxActions, stakeAmount: sponsorAmount, startRound: START_ROUND, endRound: END_ROUND, threshold: 0, numWinners: 0, appIds: [], invitees: joiners.map(w => w.address), title: "", description: "", imageURI: "", metadataURI: "", }); for (const w of joiners) { await challenges.connect(w).joinChallenge(1); } for (const w of joiners) { for (let round = START_ROUND; round <= END_ROUND; round++) { await passport.setUserRoundActionCount(w.address, round, 1); } } await roundGovernor.setCurrentRoundId(END_ROUND + 1); const tx = await challenges.connect(admin).completeChallenge(1); const receipt = await tx.wait(); const gasUsed = receipt.gasUsed; // eslint-disable-next-line no-console console.log(`\n========== completeChallenge allApps gas ==========`); // eslint-disable-next-line no-console console.log(`Gas used: ${gasUsed.toString()}`); // eslint-disable-next-line no-console console.log(`===================================================\n`); (0, chai_1.expect)(gasUsed).to.be.gt(0n); }); });