cyberia-token/test/unit/CAPToken.test.ts

import { expect } from "chai";
import { ethers, upgrades } from "hardhat";
import { CAPToken, MockDEXPair } from "../typechain-types";
import { Signer } from "ethers";

describe("CAPToken", function () {
  let cap: CAPToken;
  let mockPool: MockDEXPair;
  let owner: Signer;
  let treasury: Signer;
  let user1: Signer;
  let user2: Signer;

  const INITIAL_SUPPLY = ethers.utils.parseEther("1000000"); // 1M tokens
  const _BASIS_POINTS_DENOMINATOR = 10000;

  beforeEach(async function () {
    [owner, treasury, user1, user2] = await ethers.getSigners();

    // Deploy CAP Token
    const CAP = await ethers.getContractFactory("CAPToken");
    cap = (await upgrades.deployProxy(CAP, [owner.address, treasury.address], {
      kind: "uups",
      initializer: "initialize",
    })) as unknown as CAPToken;

    // Deploy Mock DEX Pair for pool testing
    const MockDEXPair = await ethers.getContractFactory("MockDEXPair");
    const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; // Mainnet WETH
    mockPool = await MockDEXPair.deploy(cap.address, WETH);
  });

  describe("Deployment", function () {
    it("Should set the correct name and symbol", async function () {
      expect(await cap.name()).to.equal("Cyberia");
      expect(await cap.symbol()).to.equal("CAP");
      expect(await cap.decimals()).to.equal(18);
    });

    it("Should mint initial supply to owner", async function () {
      expect(await cap.totalSupply()).to.equal(INITIAL_SUPPLY);
      expect(await cap.balanceOf(owner.address)).to.equal(INITIAL_SUPPLY);
    });

    it("Should set correct initial tax rates", async function () {
      expect(await cap.transferTaxBp()).to.equal(100); // 1%
      expect(await cap.sellTaxBp()).to.equal(100); // 1%
      expect(await cap.buyTaxBp()).to.equal(0); // 0%
    });

    it("Should set fee recipient", async function () {
      expect(await cap.feeRecipient()).to.equal(treasury.address);
    });

    it("Should set owner", async function () {
      expect(await cap.governance()).to.equal(owner.address);
    });
  });

  describe("Tax System", function () {
    beforeEach(async function () {
      // Distribute tokens for testing
      await cap.connect(owner).transfer(user1.address, ethers.utils.parseEther("100000"));
      await cap.connect(owner).transfer(user2.address, ethers.utils.parseEther("100000"));
    });

    it("Should apply transfer tax on user-to-user transfers", async function () {
      const transferAmount = ethers.utils.parseEther("1000");
      const expectedTax = transferAmount.mul(100).div(10000); // 1%
      const expectedNet = transferAmount.sub(expectedTax);

      const user1InitialBalance = await cap.balanceOf(user1.address);
      const user2InitialBalance = await cap.balanceOf(user2.address);
      const treasuryInitialBalance = await cap.balanceOf(treasury.address);

      await cap.connect(user1).transfer(user2.address, transferAmount);

      expect(await cap.balanceOf(user1.address)).to.equal(user1InitialBalance.sub(transferAmount));
      expect(await cap.balanceOf(user2.address)).to.equal(user2InitialBalance.add(expectedNet));
      expect(await cap.balanceOf(treasury.address)).to.equal(treasuryInitialBalance.add(expectedTax));
    });

    it("Should apply sell tax when transferring to pool", async function () {
      // Add pool
      const poolAddress = mockPool.address;
      await cap.connect(owner).addPool(poolAddress);

      const transferAmount = ethers.utils.parseEther("1000");
      const sellTax = transferAmount.mul(100).div(10000); // 1% sell tax only
      const expectedNet = transferAmount.sub(sellTax);

      const user1InitialBalance = await cap.balanceOf(user1.address);
      const poolInitialBalance = await cap.balanceOf(poolAddress);
      const treasuryInitialBalance = await cap.balanceOf(treasury.address);

      await cap.connect(user1).transfer(poolAddress, transferAmount);

      expect(await cap.balanceOf(user1.address)).to.equal(user1InitialBalance.sub(transferAmount));
      expect(await cap.balanceOf(poolAddress)).to.equal(poolInitialBalance.add(expectedNet));
      expect(await cap.balanceOf(treasury.address)).to.equal(treasuryInitialBalance.add(sellTax));
    });

    it("Should apply no tax when transferring from pool (buy)", async function () {
      // Add pool and give it tokens
      const poolAddress = mockPool.address;
      await cap.connect(owner).addPool(poolAddress);
      await cap.connect(owner).transfer(poolAddress, ethers.utils.parseEther("10000"));

      const transferAmount = ethers.utils.parseEther("1000");

      const poolInitialBalance = await cap.balanceOf(poolAddress);
      const user1InitialBalance = await cap.balanceOf(user1.address);
      const treasuryInitialBalance = await cap.balanceOf(treasury.address);

      // Impersonate the pool contract to send tokens
      await ethers.provider.send("hardhat_impersonateAccount", [poolAddress]);
      await ethers.provider.send("hardhat_setBalance", [poolAddress, "0x1000000000000000000"]); // Give pool ETH for gas
      const poolSigner = await ethers.getSigner(poolAddress);

      await cap.connect(poolSigner).transfer(user1.address, transferAmount);

      await ethers.provider.send("hardhat_stopImpersonatingAccount", [poolAddress]);

      expect(await cap.balanceOf(poolAddress)).to.equal(poolInitialBalance.sub(transferAmount));
      expect(await cap.balanceOf(user1.address)).to.equal(user1InitialBalance.add(transferAmount));
      expect(await cap.balanceOf(treasury.address)).to.equal(treasuryInitialBalance); // No change
    });

    it("Should burn taxes when fee recipient is zero address", async function () {
      // Set fee recipient to zero address
      await cap.connect(owner).setFeeRecipient(ethers.constants.AddressZero);

      const transferAmount = ethers.utils.parseEther("1000");
      const expectedTax = transferAmount.mul(100).div(10000); // 1%

      const initialSupply = await cap.totalSupply();

      await cap.connect(user1).transfer(user2.address, transferAmount);

      expect(await cap.totalSupply()).to.equal(initialSupply.sub(expectedTax));
    });
  });

  describe("Pool Management", function () {
    it("Should allow owner to add and remove pools", async function () {
      const poolAddress = mockPool.address;

      // Add pool
      await expect(cap.connect(owner).addPool(poolAddress)).to.emit(cap, "PoolAdded").withArgs(poolAddress);
      expect(await cap.isPool(poolAddress)).to.be.true;

      // Remove pool
      await expect(cap.connect(owner).removePool(poolAddress)).to.emit(cap, "PoolRemoved").withArgs(poolAddress);
      expect(await cap.isPool(poolAddress)).to.be.false;
    });
  });

  describe("Tax Configuration", function () {
    it("Should allow owner to propose and apply tax rates with timelock", async function () {
      await cap.connect(owner).proposeTaxChange(200, 300, 100);

      // Fast forward 24 hours
      await ethers.provider.send("evm_increaseTime", [24 * 60 * 60]);
      await ethers.provider.send("evm_mine", []);

      await expect(cap.connect(owner).applyTaxChange()).to.emit(cap, "TaxesUpdated").withArgs(200, 300, 100);

      expect(await cap.transferTaxBp()).to.equal(200);
      expect(await cap.sellTaxBp()).to.equal(300);
      expect(await cap.buyTaxBp()).to.equal(100);
    });

    it("Should enforce total tax cap", async function () {
      // Total cap is 1000 bp (10%), so transfer + sell + buy must be <= 1000
      // Individual cap is 500 bp (5%)

      // Test at total limit (400 + 400 + 200 = 1000)
      await expect(cap.connect(owner).proposeTaxChange(400, 400, 200)).to.not.be.reverted;

      // Test exceeding total limit (400 + 400 + 300 = 1100, exceeds 1000 total cap)
      await expect(cap.connect(owner).proposeTaxChange(400, 400, 300)).to.be.revertedWith("TOTAL_TAX_TOO_HIGH");

      // Test just under total limit (400 + 400 + 199 = 999)
      await expect(cap.connect(owner).proposeTaxChange(400, 400, 199)).to.not.be.reverted;

      // Test with all three at max individual (500 + 500 + 0 = 1000)
      await expect(cap.connect(owner).proposeTaxChange(500, 500, 0)).to.not.be.reverted;
    });
  });

  describe("Fee Recipient Management", function () {
    it("Should allow owner to update fee recipient", async function () {
      await expect(cap.connect(owner).setFeeRecipient(user1.address))
        .to.emit(cap, "FeeRecipientUpdated")
        .withArgs(treasury.address, user1.address);

      expect(await cap.feeRecipient()).to.equal(user1.address);
    });

    it("Should allow setting fee recipient to zero address", async function () {
      await cap.connect(owner).setFeeRecipient(ethers.constants.AddressZero);
      expect(await cap.feeRecipient()).to.equal(ethers.constants.AddressZero);
    });

    it("Should prevent setting contract itself as fee recipient", async function () {
      // Security check: prevent infinite loops or accidentally redirecting fees to contract
      await expect(cap.connect(owner).setFeeRecipient(cap.address)).to.be.revertedWith(
        "FEE_RECIPIENT_CANNOT_BE_CONTRACT"
      );
    });
  });

  describe("Burning", function () {
    it("Should allow users to burn their tokens", async function () {
      await cap.connect(owner).transfer(user1.address, ethers.utils.parseEther("10000"));

      const burnAmount = ethers.utils.parseEther("1000");
      const initialBalance = await cap.balanceOf(user1.address);
      const initialSupply = await cap.totalSupply();

      await cap.connect(user1).burn(burnAmount);

      expect(await cap.balanceOf(user1.address)).to.equal(initialBalance.sub(burnAmount));
      expect(await cap.totalSupply()).to.equal(initialSupply.sub(burnAmount));
    });
  });

  describe("Minting", function () {
    it("Should allow owner to propose and execute mint", async function () {
      const mintAmount = ethers.utils.parseEther("1000000");
      const initialSupply = await cap.totalSupply();
      const initialBalance = await cap.balanceOf(user1.address);

      await cap.connect(owner).proposeMint(user1.address, mintAmount);

      // Fast forward 7 days
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]);
      await ethers.provider.send("evm_mine", []);

      await expect(cap.connect(owner).executeMint()).to.emit(cap, "TokensMinted").withArgs(user1.address, mintAmount);

      expect(await cap.totalSupply()).to.equal(initialSupply.add(mintAmount));
      expect(await cap.balanceOf(user1.address)).to.equal(initialBalance.add(mintAmount));
    });

    it("Should not allow proposing mint to zero address", async function () {
      const mintAmount = ethers.utils.parseEther("1000000");

      await expect(cap.connect(owner).proposeMint(ethers.constants.AddressZero, mintAmount)).to.be.revertedWith(
        "MINT_TO_ZERO"
      );
    });

    it("Should emit canonical Transfer event when minting", async function () {
      const mintAmount = ethers.utils.parseEther("1000000");

      await cap.connect(owner).proposeMint(user1.address, mintAmount);

      // Fast forward 7 days
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]);
      await ethers.provider.send("evm_mine", []);

      await expect(cap.connect(owner).executeMint())
        .to.emit(cap, "Transfer")
        .withArgs(ethers.constants.AddressZero, user1.address, mintAmount);
    });

    it("Should not apply tax when minting", async function () {
      const mintAmount = ethers.utils.parseEther("1000000");
      const initialTreasuryBalance = await cap.balanceOf(treasury.address);

      await cap.connect(owner).proposeMint(user1.address, mintAmount);

      // 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();

      // Treasury balance should not change (no tax on mint)
      expect(await cap.balanceOf(treasury.address)).to.equal(initialTreasuryBalance);
      // User should receive full mint amount
      expect(await cap.balanceOf(user1.address)).to.equal(mintAmount);
    });

    it("Should enforce rolling window mint cap (30-day period)", async function () {
      // Max mint per 30 days = 10M tokens

      // First mint: propose and execute 5M
      const firstMint = ethers.utils.parseEther("5000000");
      await cap.connect(owner).proposeMint(user1.address, firstMint);
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]); // 7 days
      await ethers.provider.send("evm_mine", []);
      await cap.connect(owner).executeMint();

      // Second mint: propose and execute another 5M (still in same period = 10M total)
      const secondMint = ethers.utils.parseEther("5000000");
      await cap.connect(owner).proposeMint(user2.address, secondMint);
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]); // 7 more days (14 days total)
      await ethers.provider.send("evm_mine", []);
      await cap.connect(owner).executeMint();

      // Third mint: should fail (exceeds 10M cap for 30-day period)
      const thirdMint = ethers.utils.parseEther("1");
      await expect(cap.connect(owner).proposeMint(user1.address, thirdMint)).to.be.revertedWith(
        "EXCEEDS_MINT_CAP_PER_PERIOD"
      );
    });

    it("Should reset mint cap after rolling 30-day period expires", async function () {
      // This test documents that the rolling window mint cap resets after 30 days
      // Start by minting up to the cap in the first period
      const fullCap = ethers.utils.parseEther("10000000"); // 10M cap per 30 days

      // Proposal 1: Mint 10M (uses the full cap)
      await cap.connect(owner).proposeMint(user1.address, fullCap);
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]); // 7 days
      await ethers.provider.send("evm_mine", []);
      await cap.connect(owner).executeMint(); // Actually increments mintedInCurrentPeriod

      // Verify the cap is now exhausted (trying to mint any amount should fail)
      const tinyAmount = ethers.utils.parseEther("1");
      await expect(cap.connect(owner).proposeMint(user2.address, tinyAmount)).to.be.revertedWith(
        "EXCEEDS_MINT_CAP_PER_PERIOD"
      );

      // Now move forward exactly 30 days to reset the period
      // Current time is 7 days, need to go to 7 + 30 = 37 days from initial start
      await ethers.provider.send("evm_increaseTime", [30 * 24 * 60 * 60]); // Advance 30 more days
      await ethers.provider.send("evm_mine", []);

      // Now the 30-day period should have reset, and we can mint again
      const secondPeriodMint = ethers.utils.parseEther("5000000");
      await cap.connect(owner).proposeMint(user2.address, secondPeriodMint);
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]);
      await ethers.provider.send("evm_mine", []);
      await expect(cap.connect(owner).executeMint()).to.not.be.reverted;
    });

    it("Should handle rolling window correctly (multiple periods)", async function () {
      const halfCap = ethers.utils.parseEther("5000000");

      // Period 1: mint 10M total (at the cap)
      await cap.connect(owner).proposeMint(user1.address, halfCap);
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]);
      await ethers.provider.send("evm_mine", []);
      await cap.connect(owner).executeMint();

      await cap.connect(owner).proposeMint(user2.address, halfCap);
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]);
      await ethers.provider.send("evm_mine", []);
      await cap.connect(owner).executeMint();

      // Period 2: skip ahead 30 days from first mint to reset
      await ethers.provider.send("evm_increaseTime", [16 * 24 * 60 * 60]); // More than 30 days from first mint
      await ethers.provider.send("evm_mine", []);

      // Should be able to mint MINT_CAP again (period has rolled)
      await cap.connect(owner).proposeMint(user1.address, halfCap);
      await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]);
      await ethers.provider.send("evm_mine", []);
      await expect(cap.connect(owner).executeMint()).to.not.be.reverted;
    });
  });

  describe("Governance Features", function () {
    it("Should track voting power correctly after delegation", async function () {
      await cap.connect(owner).transfer(user1.address, ethers.utils.parseEther("10000"));
      await cap.connect(user1).delegate(user1.address);

      const actualBalance = await cap.balanceOf(user1.address);
      const votingPower = await cap.getVotes(user1.address);
      expect(votingPower).to.equal(actualBalance);
    });

    it("Should support permit functionality", async function () {
      expect(cap.permit).to.be.a("function");
    });
  });

  describe("Upgrade Functionality", function () {
    it("Should allow owner to authorize upgrades", async function () {
      const CAPTokenV2 = await ethers.getContractFactory("CAPToken");
      const newImplementation = await CAPTokenV2.deploy();

      await expect(cap.connect(owner).upgradeToAndCall(newImplementation.address, "0x")).to.not.be.reverted;
    });
  });
});

Neighbours