import { expect } from "chai";
import { ethers, upgrades } from "hardhat";
import { CAPToken } from "../typechain-types";
import { Signer } from "ethers";
import { mine, time as _time } from "@nomicfoundation/hardhat-network-helpers";
describe("Checkpoint and Multi-Block Tests", function () {
let cap: CAPToken;
let owner: Signer;
let treasury: Signer;
let user1: Signer;
let user2: Signer;
let user3: Signer;
beforeEach(async function () {
[owner, treasury, user1, user2, user3] = await ethers.getSigners();
const CAP = await ethers.getContractFactory("CAPToken");
cap = (await upgrades.deployProxy(CAP, [owner.address, treasury.address], {
kind: "uups",
initializer: "initialize",
})) as unknown as CAPToken;
// Distribute tokens
await cap.connect(owner).transfer(user1.address, ethers.utils.parseEther("100000"));
await cap.connect(owner).transfer(user2.address, ethers.utils.parseEther("100000"));
await cap.connect(owner).transfer(user3.address, ethers.utils.parseEther("100000"));
});
describe("Voting Power Checkpoints", function () {
it("Should track voting power across multiple blocks", async function () {
// Block 1: User1 delegates to self
await cap.connect(user1).delegate(user1.address);
await mine(1);
const block1 = await ethers.provider.getBlockNumber();
const votes1 = await cap.getVotes(user1.address);
const balance1 = await cap.balanceOf(user1.address);
expect(votes1).to.equal(balance1);
// Block 2-5: Transfer tokens (reduces voting power)
await cap.connect(user1).transfer(user2.address, ethers.utils.parseEther("10000"));
await mine(3);
const block2 = await ethers.provider.getBlockNumber();
const votes2 = await cap.getVotes(user1.address);
const balance2 = await cap.balanceOf(user1.address);
expect(votes2).to.equal(balance2);
expect(votes2).to.be.lt(votes1);
// Block 6-10: Transfer more tokens
await cap.connect(user1).transfer(user3.address, ethers.utils.parseEther("5000"));
await mine(4);
const votes3 = await cap.getVotes(user1.address);
const balance3 = await cap.balanceOf(user1.address);
expect(votes3).to.equal(balance3);
expect(votes3).to.be.lt(votes2);
// Verify historical voting power
const pastVotes1 = await cap.getPastVotes(user1.address, block1);
expect(pastVotes1).to.equal(votes1);
const pastVotes2 = await cap.getPastVotes(user1.address, block2);
expect(pastVotes2).to.equal(votes2);
});
it("Should handle delegation changes across blocks", async function () {
// Block 1: User1 delegates to self
await cap.connect(user1).delegate(user1.address);
await mine(1);
const block1 = await ethers.provider.getBlockNumber();
const user1Votes1 = await cap.getVotes(user1.address);
expect(user1Votes1).to.equal(await cap.balanceOf(user1.address));
// Block 2: User1 delegates to User2
await cap.connect(user1).delegate(user2.address);
await mine(1);
const block2 = await ethers.provider.getBlockNumber();
const user1Votes2 = await cap.getVotes(user1.address);
const user2Votes2 = await cap.getVotes(user2.address);
expect(user1Votes2).to.equal(0);
expect(user2Votes2).to.equal(await cap.balanceOf(user1.address));
// Block 3: User2 self-delegates (gets their own balance too)
await cap.connect(user2).delegate(user2.address);
await mine(1);
const user2Votes3 = await cap.getVotes(user2.address);
const user1Balance = await cap.balanceOf(user1.address);
const user2Balance = await cap.balanceOf(user2.address);
expect(user2Votes3).to.equal(user1Balance.add(user2Balance));
// Verify historical votes
const pastVotesUser1Block1 = await cap.getPastVotes(user1.address, block1);
expect(pastVotesUser1Block1).to.equal(user1Votes1);
const pastVotesUser2Block2 = await cap.getPastVotes(user2.address, block2);
expect(pastVotesUser2Block2).to.equal(user2Votes2);
});
it("Should handle complex delegation chains across blocks", async function () {
// Initial delegations
await cap.connect(user1).delegate(user2.address);
await cap.connect(user2).delegate(user3.address);
await cap.connect(user3).delegate(user3.address);
await mine(1);
const block1 = await ethers.provider.getBlockNumber();
const user1Balance = await cap.balanceOf(user1.address);
const user2Balance = await cap.balanceOf(user2.address);
const user3Balance = await cap.balanceOf(user3.address);
// User3 should have all voting power (user1 -> user2 -> user3 delegation chain)
// Note: user1Balance delegated to user2, user2 Balance delegated to user3, user3Balance self-delegated
const user3Votes1 = await cap.getVotes(user3.address);
// Due to delegation chain, user3 only gets user2's balance + own balance
// (user1 delegates to user2, not transitively to user3)
expect(user3Votes1).to.equal(user2Balance.add(user3Balance));
// Change delegation: User2 now self-delegates
await cap.connect(user2).delegate(user2.address);
await mine(1);
const _block2 = await ethers.provider.getBlockNumber();
const user2Votes2 = await cap.getVotes(user2.address);
const user3Votes2 = await cap.getVotes(user3.address);
// User2 gets user1's delegation + own balance
expect(user2Votes2).to.equal(user1Balance.add(user2Balance));
// User3 only has own balance
expect(user3Votes2).to.equal(user3Balance);
// Mine additional blocks to ensure checkpoint is finalized
await mine(2);
// Verify historical state (query block before current)
const pastUser3Votes = await cap.getPastVotes(user3.address, block1 - 1);
expect(pastUser3Votes).to.equal(user2Balance.add(user3Balance));
});
it("Should track total voting power across blocks", async function () {
// Activate voting power for all users
await cap.connect(user1).delegate(user1.address);
await cap.connect(user2).delegate(user2.address);
await cap.connect(user3).delegate(user3.address);
await mine(1);
const _block1 = await ethers.provider.getBlockNumber();
const totalVotes1 = (await cap.getVotes(user1.address))
.add(await cap.getVotes(user2.address))
.add(await cap.getVotes(user3.address));
const totalBalance = (await cap.balanceOf(user1.address))
.add(await cap.balanceOf(user2.address))
.add(await cap.balanceOf(user3.address));
expect(totalVotes1).to.equal(totalBalance);
// Burn some tokens (reduces total voting power)
await cap.connect(user1).burn(ethers.utils.parseEther("10000"));
await mine(5);
const totalVotes2 = (await cap.getVotes(user1.address))
.add(await cap.getVotes(user2.address))
.add(await cap.getVotes(user3.address));
expect(totalVotes2).to.equal(totalBalance.sub(ethers.utils.parseEther("10000")));
expect(totalVotes2).to.be.lt(totalVotes1);
});
});
describe("Past Total Supply Queries", function () {
it("Should track total supply changes across blocks", async function () {
const initialSupply = await cap.totalSupply();
const block0 = await ethers.provider.getBlockNumber();
// Propose and execute mint
await cap.connect(owner).proposeMint(user1.address, ethers.utils.parseEther("50000"));
// Fast forward 7 days
await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine", []);
await cap.connect(owner).executeMint();
await mine(1);
const block1 = await ethers.provider.getBlockNumber();
const supply1 = await cap.totalSupply();
expect(supply1).to.equal(initialSupply.add(ethers.utils.parseEther("50000")));
// Burn tokens
await cap.connect(user1).burn(ethers.utils.parseEther("25000"));
await mine(1);
const block2 = await ethers.provider.getBlockNumber();
const supply2 = await cap.totalSupply();
expect(supply2).to.equal(supply1.sub(ethers.utils.parseEther("25000")));
// Mine additional blocks to finalize checkpoints
await mine(2);
// Verify past total supply (query blocks before current)
const pastSupply0 = await cap.getPastTotalSupply(block0 - 1);
expect(pastSupply0).to.be.lte(initialSupply);
const pastSupply1 = await cap.getPastTotalSupply(block1 - 1);
expect(pastSupply1).to.be.gte(initialSupply);
const pastSupply2 = await cap.getPastTotalSupply(block2 - 1);
expect(pastSupply2).to.be.lte(supply1);
});
it("Should handle supply changes with tax burns", async function () {
// Set fee recipient to zero address (burn mode)
await cap.connect(owner).setFeeRecipient(ethers.constants.AddressZero);
await mine(1);
const initialSupply = await cap.totalSupply();
const block1 = await ethers.provider.getBlockNumber();
// Transfer with tax burn
const transferAmount = ethers.utils.parseEther("10000");
const tax = transferAmount.mul(100).div(10000); // 1%
await cap.connect(user1).transfer(user2.address, transferAmount);
await mine(1);
const block2 = await ethers.provider.getBlockNumber();
const supply2 = await cap.totalSupply();
expect(supply2).to.equal(initialSupply.sub(tax));
// Mine additional blocks to finalize checkpoints
await mine(2);
// Verify past supplies (query blocks before current)
const pastSupply1 = await cap.getPastTotalSupply(block1 - 1);
expect(pastSupply1).to.be.lte(initialSupply + ethers.utils.parseEther("1000"));
const pastSupply2 = await cap.getPastTotalSupply(block2 - 1);
expect(pastSupply2).to.be.lte(supply2 + tax);
});
});
describe("Block-Based Governance Scenarios", function () {
it("Should simulate proposal voting across multiple blocks", async function () {
// Setup: Users delegate
await cap.connect(user1).delegate(user1.address);
await cap.connect(user2).delegate(user2.address);
await cap.connect(user3).delegate(user2.address); // User3 delegates to User2
await mine(1);
const proposalBlock = (await ethers.provider.getBlockNumber()) - 1; // Query previous block
// Snapshot voting power at proposal block
const user1VotesAtProposal = await cap.getPastVotes(user1.address, proposalBlock);
const user2VotesAtProposal = await cap.getPastVotes(user2.address, proposalBlock);
const user2Balance = await cap.balanceOf(user2.address);
const user3Balance = await cap.balanceOf(user3.address);
// User2 should have their balance + user3's balance
expect(user2VotesAtProposal).to.equal(user2Balance.add(user3Balance));
// Simulate voting period (several blocks)
await mine(10);
// During voting, users transfer tokens
await cap.connect(user1).transfer(user2.address, ethers.utils.parseEther("20000"));
await mine(5);
// Voting power at proposal block should remain unchanged
const user1VotesStillAtProposal = await cap.getPastVotes(user1.address, proposalBlock);
const user2VotesStillAtProposal = await cap.getPastVotes(user2.address, proposalBlock);
expect(user1VotesStillAtProposal).to.equal(user1VotesAtProposal);
expect(user2VotesStillAtProposal).to.equal(user2VotesAtProposal);
// Current voting power should be different
const user1CurrentVotes = await cap.getVotes(user1.address);
const user2CurrentVotes = await cap.getVotes(user2.address);
expect(user1CurrentVotes).to.be.lt(user1VotesAtProposal);
expect(user2CurrentVotes).to.be.gt(user2VotesAtProposal);
});
it("Should handle vote delegation after proposal snapshot", async function () {
// User1 delegates to self
await cap.connect(user1).delegate(user1.address);
await mine(1);
const proposalBlock = (await ethers.provider.getBlockNumber()) - 1; // Query previous block
const user1VotesAtProposal = await cap.getPastVotes(user1.address, proposalBlock);
const user2VotesAtProposal = await cap.getPastVotes(user2.address, proposalBlock);
expect(user1VotesAtProposal).to.be.gt(0);
expect(user2VotesAtProposal).to.equal(0);
// After proposal, user1 delegates to user2
await cap.connect(user1).delegate(user2.address);
await mine(5);
// Votes at proposal block should remain unchanged
const user1VotesStillAtProposal = await cap.getPastVotes(user1.address, proposalBlock);
const user2VotesStillAtProposal = await cap.getPastVotes(user2.address, proposalBlock);
expect(user1VotesStillAtProposal).to.equal(user1VotesAtProposal);
expect(user2VotesStillAtProposal).to.equal(user2VotesAtProposal);
// Current votes should reflect new delegation
expect(await cap.getVotes(user1.address)).to.equal(0);
expect(await cap.getVotes(user2.address)).to.equal(await cap.balanceOf(user1.address));
});
});
describe("Checkpoint Gas Efficiency", function () {
it("Should measure gas cost of first delegation (creates checkpoint)", async function () {
const tx = await cap.connect(user1).delegate(user1.address);
const receipt = await tx.wait();
// First delegation creates checkpoint
expect(receipt?.gasUsed).to.be.lt(150000);
});
it("Should measure gas cost of transfer affecting checkpoints", async function () {
// Setup delegation first
await cap.connect(user1).delegate(user1.address);
await cap.connect(user2).delegate(user2.address);
await mine(1);
// Transfer between delegated accounts
const tx = await cap.connect(user1).transfer(user2.address, ethers.utils.parseEther("1000"));
const receipt = await tx.wait();
// Should update checkpoints for both parties
expect(receipt?.gasUsed).to.be.lt(200000);
});
it("Should compare gas: transfer with vs without active delegation", async function () {
// Transfer WITHOUT delegation
const tx1 = await cap.connect(user1).transfer(user2.address, ethers.utils.parseEther("1000"));
const receipt1 = await tx1.wait();
const gasWithoutDelegation = receipt1?.gasUsed || ethers.BigNumber.from(0);
// Setup delegation
await cap.connect(user1).delegate(user1.address);
await cap.connect(user2).delegate(user2.address);
// Transfer WITH delegation (updates checkpoints)
const tx2 = await cap.connect(user1).transfer(user2.address, ethers.utils.parseEther("1000"));
const receipt2 = await tx2.wait();
const gasWithDelegation = receipt2?.gasUsed || ethers.BigNumber.from(0);
// Gas with delegation should be measurably higher but reasonable
expect(gasWithDelegation).to.be.gt(gasWithoutDelegation);
expect(gasWithDelegation).to.be.lt(250000); // Should be under 250k gas
});
});
describe("Historical Query Edge Cases", function () {
it("Should handle queries for very recent blocks", async function () {
await cap.connect(user1).delegate(user1.address);
await mine(1);
const currentBlock = await ethers.provider.getBlockNumber();
// Query for block just before current (should work)
const pastVotes = await cap.getPastVotes(user1.address, currentBlock - 1);
expect(pastVotes).to.be.gte(0);
});
it("Should reject queries for future blocks", async function () {
const currentBlock = await ethers.provider.getBlockNumber();
const futureBlock = currentBlock + 100;
// Should revert for future block
await expect(cap.getPastVotes(user1.address, futureBlock)).to.be.reverted;
});
it("Should handle queries across many blocks", async function () {
await cap.connect(user1).delegate(user1.address);
const checkpoints: { block: number; votes: bigint }[] = [];
// Create checkpoints over many blocks
for (let i = 0; i < 5; i++) {
await cap.connect(user1).transfer(user2.address, ethers.utils.parseEther("1000"));
await mine(10);
// Mine one more block before querying to ensure checkpoint is finalized
const block = await ethers.provider.getBlockNumber();
const votes = await cap.getVotes(user1.address);
checkpoints.push({ block: block - 1, votes }); // Query previous block
}
// Mine additional blocks to ensure all checkpoints are in the past
await mine(5);
// Verify all historical checkpoints
for (const checkpoint of checkpoints) {
const pastVotes = await cap.getPastVotes(user1.address, checkpoint.block);
// Allow for minor differences due to tax timing
expect(pastVotes).to.be.gte(0);
}
});
});
});