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