import { expect } from "chai";
import { ethers, upgrades } from "hardhat";
import { CAPToken } from "../typechain-types";
import { Signer } from "ethers";
import { time } from "@nomicfoundation/hardhat-network-helpers";
describe("Timelock Boundary Tests", function () {
let cap: CAPToken;
let owner: Signer;
let treasury: Signer;
let user1: Signer;
const TIMELOCK_DELAY = 24 * 60 * 60; // 24 hours in seconds
beforeEach(async function () {
[owner, treasury, user1] = 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;
});
describe("Timelock Delay Boundaries", function () {
it("Should reject application exactly 1 second before timelock expiry", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
const targetTime = await cap.taxChangeTimestamp();
// Set time to exactly 1 second before the target time
await time.setNextBlockTimestamp(targetTime.sub(1));
await expect(cap.connect(owner).applyTaxChange()).to.be.revertedWith("TIMELOCK_NOT_EXPIRED");
});
it("Should allow application exactly at timelock expiry", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
// Move to exactly the timelock delay
await time.increase(TIMELOCK_DELAY);
await expect(cap.connect(owner).applyTaxChange()).to.not.be.reverted;
expect(await cap.transferTaxBp()).to.equal(200);
});
it("Should allow application 1 second after timelock expiry", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
// Move to 1 second after timelock delay
await time.increase(TIMELOCK_DELAY + 1);
await expect(cap.connect(owner).applyTaxChange()).to.not.be.reverted;
expect(await cap.transferTaxBp()).to.equal(200);
});
it("Should handle application hours after timelock expiry", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
// Move to 48 hours (well past timelock)
await time.increase(TIMELOCK_DELAY * 2);
await expect(cap.connect(owner).applyTaxChange()).to.not.be.reverted;
expect(await cap.transferTaxBp()).to.equal(200);
});
it("Should handle application days after timelock expiry", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
// Move to 7 days after proposal
await time.increase(TIMELOCK_DELAY * 7);
await expect(cap.connect(owner).applyTaxChange()).to.not.be.reverted;
expect(await cap.transferTaxBp()).to.equal(200);
});
});
describe("Multiple Proposal Scenarios", function () {
it("Should handle rapid proposal replacements", async function () {
// First proposal
await cap.connect(owner).proposeTaxChange(100, 150, 25);
const timestamp1 = await cap.taxChangeTimestamp();
// Wait 1 hour
await time.increase(3600);
// Second proposal (overwrites first)
await cap.connect(owner).proposeTaxChange(200, 250, 50);
const timestamp2 = await cap.taxChangeTimestamp();
expect(timestamp2).to.be.gt(timestamp1);
// Wait 23 hours (total 24 hours from second proposal)
await time.increase(23 * 3600);
// Should NOT be able to apply yet (only 23 hours from second proposal)
await expect(cap.connect(owner).applyTaxChange()).to.be.revertedWith("TIMELOCK_NOT_EXPIRED");
// Wait 1 more hour
await time.increase(3600);
// Now should work with SECOND proposal values
await cap.connect(owner).applyTaxChange();
expect(await cap.transferTaxBp()).to.equal(200);
expect(await cap.sellTaxBp()).to.equal(250);
expect(await cap.buyTaxBp()).to.equal(50);
});
it("Should allow proposing same values multiple times", async function () {
const values = { transfer: 200, sell: 300, buy: 50 };
// First proposal
await cap.connect(owner).proposeTaxChange(values.transfer, values.sell, values.buy);
const timestamp1 = await cap.taxChangeTimestamp();
// Wait a bit
await time.increase(3600);
// Propose same values again
await cap.connect(owner).proposeTaxChange(values.transfer, values.sell, values.buy);
const timestamp2 = await cap.taxChangeTimestamp();
expect(timestamp2).to.be.gt(timestamp1);
expect(await cap.pendingTransferTaxBp()).to.equal(values.transfer);
});
it("Should handle proposal at timestamp boundaries", async function () {
// Get current block timestamp before proposal
const tx = await cap.connect(owner).proposeTaxChange(200, 300, 50);
const receipt = await tx.wait();
const block = await ethers.provider.getBlock(receipt!.blockNumber);
const blockTime = block!.timestamp;
const proposalTime = await cap.taxChangeTimestamp();
// Proposal timestamp should be block timestamp + delay
expect(proposalTime).to.equal(blockTime + TIMELOCK_DELAY);
});
});
describe("Timestamp Edge Cases", function () {
it("Should handle timelock at maximum safe timestamp", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
// Move to far future (but within safe bounds)
const farFuture = TIMELOCK_DELAY + 365 * 24 * 60 * 60; // 1 year from now
await time.increase(farFuture);
await expect(cap.connect(owner).applyTaxChange()).to.not.be.reverted;
});
it("Should prevent double application of same proposal", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
await time.increase(TIMELOCK_DELAY + 1);
// First application
await cap.connect(owner).applyTaxChange();
// Second application should fail (no pending change)
await expect(cap.connect(owner).applyTaxChange()).to.be.revertedWith("NO_PENDING_CHANGE");
});
it("Should handle proposal-apply-proposal cycle", async function () {
// First cycle
await cap.connect(owner).proposeTaxChange(200, 300, 50);
await time.increase(TIMELOCK_DELAY + 1);
await cap.connect(owner).applyTaxChange();
expect(await cap.transferTaxBp()).to.equal(200);
// Second cycle immediately after first
await cap.connect(owner).proposeTaxChange(150, 200, 25);
await time.increase(TIMELOCK_DELAY + 1);
await cap.connect(owner).applyTaxChange();
expect(await cap.transferTaxBp()).to.equal(150);
});
});
describe("Tax Change Cancellation", function () {
it("Should allow owner to cancel pending tax change", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
expect(await cap.taxChangeTimestamp()).to.not.equal(0);
expect(await cap.pendingTransferTaxBp()).to.equal(200);
// Cancel the pending change
const tx = await cap.connect(owner).cancelTaxChange();
await expect(tx).to.emit(cap, "TaxChangeCancelled").withArgs(200, 300, 50);
// Verify state is reset
expect(await cap.taxChangeTimestamp()).to.equal(0);
expect(await cap.pendingTransferTaxBp()).to.equal(0);
expect(await cap.pendingSellTaxBp()).to.equal(0);
expect(await cap.pendingBuyTaxBp()).to.equal(0);
// Original taxes unchanged
expect(await cap.transferTaxBp()).to.equal(100);
expect(await cap.sellTaxBp()).to.equal(100);
expect(await cap.buyTaxBp()).to.equal(0);
});
it("Should revert when cancelling with no pending change", async function () {
await expect(cap.connect(owner).cancelTaxChange()).to.be.revertedWith("NO_PENDING_CHANGE");
});
it("Should allow cancel even after timelock expired", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
// Wait past timelock
await time.increase(TIMELOCK_DELAY + 3600);
// Should still be able to cancel (governance decided not to apply)
await cap.connect(owner).cancelTaxChange();
expect(await cap.taxChangeTimestamp()).to.equal(0);
});
it("Should allow new proposal after cancellation", async function () {
// First proposal
await cap.connect(owner).proposeTaxChange(200, 300, 50);
// Cancel it
await cap.connect(owner).cancelTaxChange();
// Should be able to propose new values
await cap.connect(owner).proposeTaxChange(150, 250, 25);
expect(await cap.pendingTransferTaxBp()).to.equal(150);
expect(await cap.pendingSellTaxBp()).to.equal(250);
expect(await cap.pendingBuyTaxBp()).to.equal(25);
// And apply the new proposal
await time.increase(TIMELOCK_DELAY + 1);
await cap.connect(owner).applyTaxChange();
expect(await cap.transferTaxBp()).to.equal(150);
});
it("Should prevent non-owner from cancelling", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
await expect(cap.connect(user1).cancelTaxChange()).to.be.revertedWith("ONLY_GOVERNANCE");
});
it("Should handle cancel-propose-cancel cycle", async function () {
// First proposal
await cap.connect(owner).proposeTaxChange(200, 300, 50);
await cap.connect(owner).cancelTaxChange();
// Second proposal
await cap.connect(owner).proposeTaxChange(150, 250, 25);
await cap.connect(owner).cancelTaxChange();
// Third proposal - should work fine
await cap.connect(owner).proposeTaxChange(100, 200, 0);
expect(await cap.pendingTransferTaxBp()).to.equal(100);
expect(await cap.pendingSellTaxBp()).to.equal(200);
});
});
describe("Timelock Interaction with Other Operations", function () {
it("Should allow overwriting pending proposal with new proposal", async function () {
// Propose first timelock change
await cap.connect(owner).proposeTaxChange(200, 300, 50);
// Owner can propose another change (overwrites first)
await cap.connect(owner).proposeTaxChange(150, 250, 25);
expect(await cap.pendingTransferTaxBp()).to.equal(150);
// Can apply the new (overwritten) proposal after timelock
await time.increase(TIMELOCK_DELAY + 1);
await cap.connect(owner).applyTaxChange();
expect(await cap.transferTaxBp()).to.equal(150);
});
it("Should handle transfers during pending timelock", async function () {
await cap.connect(owner).transfer(user1.address, ethers.utils.parseEther("10000"));
// Propose tax change
await cap.connect(owner).proposeTaxChange(400, 400, 200);
// Transfers should use current tax rates
const transferAmount = ethers.utils.parseEther("1000");
const currentTax = transferAmount.mul(100).div(10000); // 1%
const treasuryBefore = await cap.balanceOf(treasury.address);
await cap.connect(user1).transfer(owner.address, transferAmount);
const treasuryAfter = await cap.balanceOf(treasury.address);
expect(treasuryAfter.sub(treasuryBefore)).to.equal(currentTax);
// Apply new taxes
await time.increase(TIMELOCK_DELAY + 1);
await cap.connect(owner).applyTaxChange();
// New transfers should use new tax rates
const newTax = transferAmount.mul(400).div(10000); // 4%
const treasuryBefore2 = await cap.balanceOf(treasury.address);
await cap.connect(user1).transfer(owner.address, transferAmount);
const treasuryAfter2 = await cap.balanceOf(treasury.address);
expect(treasuryAfter2.sub(treasuryBefore2)).to.equal(newTax);
});
it("Should preserve pending proposal through governance transfer", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
const timestampBefore = await cap.taxChangeTimestamp();
// Transfer governance to user1
await cap.connect(owner).setGovernance(user1.address);
// Pending proposal should still exist
expect(await cap.taxChangeTimestamp()).to.equal(timestampBefore);
expect(await cap.pendingTransferTaxBp()).to.equal(200);
// New governance can apply it
await time.increase(TIMELOCK_DELAY + 1);
await cap.connect(user1).applyTaxChange();
expect(await cap.transferTaxBp()).to.equal(200);
});
});
describe("Gas Efficiency During Timelock", function () {
it("Should measure gas cost of proposing tax change", async function () {
const tx = await cap.connect(owner).proposeTaxChange(200, 300, 50);
const receipt = await tx.wait();
// Should be relatively cheap (mostly storage writes)
expect(receipt?.gasUsed).to.be.lt(150000);
});
it("Should measure gas cost of applying tax change", async function () {
await cap.connect(owner).proposeTaxChange(200, 300, 50);
await time.increase(TIMELOCK_DELAY + 1);
const tx = await cap.connect(owner).applyTaxChange();
const receipt = await tx.wait();
// Should be relatively cheap
expect(receipt?.gasUsed).to.be.lt(100000);
});
it("Should measure gas cost of propose + apply cycle", async function () {
// Timelock change (propose + apply)
const tx1 = await cap.connect(owner).proposeTaxChange(200, 300, 50);
const receipt1 = await tx1.wait();
await time.increase(TIMELOCK_DELAY + 1);
const tx2 = await cap.connect(owner).applyTaxChange();
const receipt2 = await tx2.wait();
const gas1 = receipt1?.gasUsed || ethers.BigNumber.from(0);
const gas2 = receipt2?.gasUsed || ethers.BigNumber.from(0);
const totalGas = gas1.add(gas2);
// Two-step process should be reasonable
expect(totalGas).to.be.lt(250000);
});
});
});