cyberia-token/test/security/AdvancedSecurity.test.ts

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("Advanced Security Tests", function () {
  let cap: CAPToken;
  let owner: Signer;
  let treasury: Signer;
  let attacker: Signer;
  let user1: Signer;
  let user2: Signer;

  beforeEach(async function () {
    [owner, treasury, attacker, user1, user2] = 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 for testing
    await cap.connect(owner).transfer(user1.address, ethers.utils.parseEther("100000"));
    await cap.connect(owner).transfer(attacker.address, ethers.utils.parseEther("10000"));
  });

  describe("Reentrancy Attack Simulation", function () {
    it("Should prevent reentrancy via nonReentrant modifier", async function () {
      // Note: ERC20 transfers don't have receiver hooks, but the nonReentrant modifier
      // protects against any potential reentrancy in the _update function

      const transferAmount = ethers.utils.parseEther("100");
      const user1BalanceBefore = await cap.balanceOf(user1.address);

      // Attempt transfer (should succeed without reentrancy)
      await cap.connect(user1).transfer(user2.address, transferAmount);

      const user1BalanceAfter = await cap.balanceOf(user1.address);

      // Verify balance changed exactly once
      expect(user1BalanceBefore.sub(user1BalanceAfter)).to.equal(transferAmount);

      // The nonReentrant modifier ensures that if any future upgrade adds
      // hooks or callbacks, reentrancy will still be prevented
    });

    it("Should handle nested transfer attempts safely", async function () {
      // Test that multiple transfers in sequence work correctly
      const amount = ethers.utils.parseEther("10");

      // Start balance
      const startBalance = await cap.balanceOf(user1.address);

      // Multiple transfers
      await cap.connect(user1).transfer(user2.address, amount);
      await cap.connect(user1).transfer(user2.address, amount);
      await cap.connect(user1).transfer(user2.address, amount);

      const endBalance = await cap.balanceOf(user1.address);

      // Should have transferred exactly 3x amount (plus taxes)
      const expectedDeduction = amount.mul(3);
      expect(startBalance.sub(endBalance)).to.equal(expectedDeduction);
    });
  });

  describe("EIP-2612 Permit Functionality", function () {
    it("Should support full EIP-2612 permit signature flow", async function () {
      // Get current block timestamp from hardhat network
      const currentBlock = await ethers.provider.getBlock("latest");
      const currentTime = currentBlock!.timestamp;
      const deadline = currentTime + 3600; // 1 hour from current block time
      const value = ethers.utils.parseEther("1000");

      // Get domain separator
      const domain = {
        name: await cap.name(),
        version: "1",
        chainId: (await ethers.provider.getNetwork()).chainId,
        verifyingContract: cap.address,
      };

      // Define permit type
      const types = {
        Permit: [
          { name: "owner", type: "address" },
          { name: "spender", type: "address" },
          { name: "value", type: "uint256" },
          { name: "nonce", type: "uint256" },
          { name: "deadline", type: "uint256" },
        ],
      };

      // Get current nonce
      const nonce = await cap.nonces(user1.address);

      // Create permit message
      const message = {
        owner: user1.address,
        spender: user2.address,
        value: value,
        nonce: nonce,
        deadline: deadline,
      };

      // Sign the permit (ethers v5 uses _signTypedData)
      const signature = await user1._signTypedData(domain, types, message);
      const { v, r, s } = ethers.utils.splitSignature(signature);

      // Verify allowance is 0 before permit
      expect(await cap.allowance(user1.address, user2.address)).to.equal(0);

      // Execute permit
      await cap.permit(user1.address, user2.address, value, deadline, v, r, s);

      // Verify allowance is now set
      expect(await cap.allowance(user1.address, user2.address)).to.equal(value);

      // Verify nonce was incremented
      expect(await cap.nonces(user1.address)).to.equal(nonce.add(1));

      // User2 can now spend user1's tokens
      const user1BalanceBefore = await cap.balanceOf(user1.address);
      const user2BalanceBefore = await cap.balanceOf(user2.address);

      await cap.connect(user2).transferFrom(user1.address, user2.address, value);

      // Verify transfer occurred with tax
      const tax = value.mul(100).div(10000); // 1%
      const netAmount = value.sub(tax);

      expect(await cap.balanceOf(user1.address)).to.equal(user1BalanceBefore.sub(value));
      expect(await cap.balanceOf(user2.address)).to.equal(user2BalanceBefore.add(netAmount));
    });

    it("Should reject permit with invalid signature", async function () {
      const deadline = Math.floor(Date.now() / 1000) + 3600;
      const value = ethers.utils.parseEther("1000");
      const _nonce = await cap.nonces(user1.address);

      // Create invalid signature
      const invalidR = "0x1234567890123456789012345678901234567890123456789012345678901234";
      const invalidS = "0x1234567890123456789012345678901234567890123456789012345678901234";
      const v = 27;

      await expect(cap.permit(user1.address, user2.address, value, deadline, v, invalidR, invalidS)).to.be.reverted;
    });

    it("Should reject expired permit", async function () {
      // Get current block timestamp from hardhat network
      const currentBlock = await ethers.provider.getBlock("latest");
      const currentTime = currentBlock!.timestamp;
      const expiredDeadline = currentTime - 3600; // 1 hour ago from current block time
      const value = ethers.utils.parseEther("1000");

      const domain = {
        name: await cap.name(),
        version: "1",
        chainId: (await ethers.provider.getNetwork()).chainId,
        verifyingContract: cap.address,
      };

      const types = {
        Permit: [
          { name: "owner", type: "address" },
          { name: "spender", type: "address" },
          { name: "value", type: "uint256" },
          { name: "nonce", type: "uint256" },
          { name: "deadline", type: "uint256" },
        ],
      };

      const nonce = await cap.nonces(user1.address);

      const message = {
        owner: user1.address,
        spender: user2.address,
        value: value,
        nonce: nonce,
        deadline: expiredDeadline,
      };

      const signature = await user1._signTypedData(domain, types, message);
      const { v, r, s } = ethers.utils.splitSignature(signature);

      await expect(cap.permit(user1.address, user2.address, value, expiredDeadline, v, r, s)).to.be.reverted;
    });
  });

  describe("Storage Collision and Upgrade Safety", function () {
    it("Should preserve storage layout after upgrade", async function () {
      // Set up initial state
      await cap.connect(owner).addPool(user1.address);
      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 cap.connect(owner).applyTaxChange();
      await cap.connect(owner).transfer(user2.address, ethers.utils.parseEther("1000"));

      // Record all state
      const stateBefore = {
        name: await cap.name(),
        symbol: await cap.symbol(),
        totalSupply: await cap.totalSupply(),
        decimals: await cap.decimals(),
        owner: await cap.governance(),
        feeRecipient: await cap.feeRecipient(),
        transferTaxBp: await cap.transferTaxBp(),
        sellTaxBp: await cap.sellTaxBp(),
        buyTaxBp: await cap.buyTaxBp(),
        isPoolUser1: await cap.isPool(user1.address),
        balanceOwner: await cap.balanceOf(owner.address),
        balanceUser2: await cap.balanceOf(user2.address),
      };

      // Deploy new implementation (same code)
      const CAPv2 = await ethers.getContractFactory("CAPToken");
      const newImpl = await CAPv2.deploy();

      // Upgrade
      await cap.connect(owner).upgradeToAndCall(newImpl.address, "0x");

      // Verify all state preserved
      expect(await cap.name()).to.equal(stateBefore.name);
      expect(await cap.symbol()).to.equal(stateBefore.symbol);
      expect(await cap.totalSupply()).to.equal(stateBefore.totalSupply);
      expect(await cap.decimals()).to.equal(stateBefore.decimals);
      expect(await cap.governance()).to.equal(stateBefore.owner);
      expect(await cap.feeRecipient()).to.equal(stateBefore.feeRecipient);
      expect(await cap.transferTaxBp()).to.equal(stateBefore.transferTaxBp);
      expect(await cap.sellTaxBp()).to.equal(stateBefore.sellTaxBp);
      expect(await cap.buyTaxBp()).to.equal(stateBefore.buyTaxBp);
      expect(await cap.isPool(user1.address)).to.equal(stateBefore.isPoolUser1);
      expect(await cap.balanceOf(owner.address)).to.equal(stateBefore.balanceOwner);
      expect(await cap.balanceOf(user2.address)).to.equal(stateBefore.balanceUser2);
    });

    it("Should handle upgrade with pending tax change", async function () {
      // Propose tax change
      await cap.connect(owner).proposeTaxChange(300, 400, 50);

      const timestampBefore = await cap.taxChangeTimestamp();
      const pendingTransferBefore = await cap.pendingTransferTaxBp();
      const pendingSellBefore = await cap.pendingSellTaxBp();
      const pendingBuyBefore = await cap.pendingBuyTaxBp();

      // Upgrade contract
      const CAPv2 = await ethers.getContractFactory("CAPToken");
      const newImpl = await CAPv2.deploy();
      await cap.connect(owner).upgradeToAndCall(newImpl.address, "0x");

      // Verify pending state preserved
      expect(await cap.taxChangeTimestamp()).to.equal(timestampBefore);
      expect(await cap.pendingTransferTaxBp()).to.equal(pendingTransferBefore);
      expect(await cap.pendingSellTaxBp()).to.equal(pendingSellBefore);
      expect(await cap.pendingBuyTaxBp()).to.equal(pendingBuyBefore);

      // Verify can still apply after timelock
      await time.increase(24 * 60 * 60 + 1); // Move forward 24 hours
      await cap.connect(owner).applyTaxChange();

      expect(await cap.transferTaxBp()).to.equal(300);
      expect(await cap.sellTaxBp()).to.equal(400);
      expect(await cap.buyTaxBp()).to.equal(50);
    });

    it("Should maintain correct storage slots for critical variables", async function () {
      // This test verifies that storage layout hasn't been corrupted
      // by checking that values remain correct after various operations

      // Set multiple values
      await cap.connect(owner).addPool(user1.address);
      await cap.connect(owner).addPool(user2.address);
      await cap.connect(owner).setFeeRecipient(attacker.address);
      await cap.connect(owner).proposeTaxChange(150, 200, 50);

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

      await cap.connect(owner).applyTaxChange();

      // Perform transfers
      await cap.connect(owner).transfer(user1.address, ethers.utils.parseEther("500"));
      await cap.connect(user1).transfer(user2.address, ethers.utils.parseEther("100"));

      // Verify all storage slots
      expect(await cap.isPool(user1.address)).to.be.true;
      expect(await cap.isPool(user2.address)).to.be.true;
      expect(await cap.feeRecipient()).to.equal(attacker.address);
      expect(await cap.transferTaxBp()).to.equal(150);
      expect(await cap.sellTaxBp()).to.equal(200);
      expect(await cap.buyTaxBp()).to.equal(50);

      // Verify balances are correct
      expect(await cap.balanceOf(user1.address)).to.be.gt(0);
      expect(await cap.balanceOf(user2.address)).to.be.gt(0);
    });
  });

  describe("Max Supply Protection", function () {
    it("Should enforce max supply cap on minting", async function () {
      const maxSupply = await cap.MAX_SUPPLY();
      const currentSupply = await cap.totalSupply();

      // Calculate available supply within rate limit (10M per 30 days)
      const rateLimitCap = ethers.utils.parseEther("10000000");
      const mintAmount = rateLimitCap.lt(maxSupply.sub(currentSupply)) ? rateLimitCap : maxSupply.sub(currentSupply);

      // Should succeed: mint within rate limit and supply cap
      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.not.be.reverted;

      // Should fail: try to mint over rate limit
      await expect(cap.connect(owner).proposeMint(user1.address, 1)).to.be.revertedWith("EXCEEDS_MINT_CAP_PER_PERIOD");
    });

    it("Should handle minting close to max supply", async function () {
      const _maxSupply = await cap.MAX_SUPPLY();
      const _currentSupply = await cap.totalSupply();
      const _rateLimitCap = ethers.utils.parseEther("10000000");

      // Mint within rate limit
      const mintAmount = ethers.utils.parseEther("5000000"); // 5M
      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();

      // Should still be able to mint remaining (within rate limit)
      const remaining = ethers.utils.parseEther("5000000"); // Another 5M
      await cap.connect(owner).proposeMint(user2.address, remaining);

      // 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.not.be.reverted;

      // Now at rate limit for this period
      await expect(cap.connect(owner).proposeMint(user1.address, 1)).to.be.revertedWith("EXCEEDS_MINT_CAP_PER_PERIOD");
    });

    it("Should allow minting after burning brings supply below cap", async function () {
      const mintAmount = ethers.utils.parseEther("5000000"); // 5M

      // Mint within rate limit
      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();

      // Burn some tokens
      await cap.connect(user1).burn(ethers.utils.parseEther("1000"));

      // Fast forward 30 days to reset rate limit period
      await ethers.provider.send("evm_increaseTime", [30 * 24 * 60 * 60]);
      await ethers.provider.send("evm_mine", []);

      // Mint more (within new period's rate limit)
      await cap.connect(owner).proposeMint(user2.address, ethers.utils.parseEther("5000000")); // 5M

      // Fast forward 7 days for timelock
      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;
    });
  });
});

Neighbours