cyberia-token/test/foundry/CAPToken.unit.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {CAPToken} from "../../contracts/CAPToken.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";

/**
 * @title CAPTokenUnitTest
 * @notice Comprehensive unit tests for CAPToken contract
 * @dev Tests timelock, permit, events, access control, and other critical functionality
 */
contract CAPTokenUnitTest is Test {
	CAPToken public token;
	CAPToken public implementation;
	address public owner;
	address public feeRecipient;
	address public alice;
	address public bob;

	// Constants
	uint256 constant BASIS_POINTS_DENOMINATOR = 10_000;
	uint256 constant MAX_TAX_BP = 500;
	uint256 constant MAX_TOTAL_TAX_BP = 1000;
	uint256 constant INITIAL_SUPPLY = 1_000_000 ether;
	uint256 constant MAX_SUPPLY = 42_000_000 ether;
	uint256 constant TAX_CHANGE_DELAY = 24 hours;

	// Events from CAPToken
	event PoolAdded(address indexed pool);
	event PoolRemoved(address indexed pool);
	event TaxChangeProposed(uint256 transferTaxBp, uint256 sellTaxBp, uint256 buyTaxBp, uint256 effectiveTime);
	event TaxesUpdated(uint256 transferTaxBp, uint256 sellTaxBp, uint256 buyTaxBp);
	event FeeRecipientUpdated(address indexed oldRecipient, address indexed newRecipient);
	event TaxBurned(address indexed from, address indexed to, uint256 grossAmount, uint256 taxAmount);
	event TaxCollected(address indexed from, address indexed to, uint256 grossAmount, uint256 taxAmount, address indexed recipient);
	event TokensMinted(address indexed to, uint256 amount);

	function setUp() public {
		owner = address(this);
		feeRecipient = makeAddr("feeRecipient");
		alice = makeAddr("alice");
		bob = makeAddr("bob");

		// Deploy implementation
		implementation = new CAPToken();

		// Deploy proxy
		bytes memory initData = abi.encodeWithSelector(CAPToken.initialize.selector, owner, feeRecipient);
		ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initData);

		// Wrap in ABI
		token = CAPToken(address(proxy));
	}

	/*//////////////////////////////////////////////////////////////
                        TIMELOCK TESTS
  //////////////////////////////////////////////////////////////*/

	/// @notice Test proposing tax change emits correct event
	function test_ProposeTaxChange_EmitsEvent() public {
		uint256 newTransferTax = 200;
		uint256 newSellTax = 300;
		uint256 newBuyTax = 100;
		uint256 expectedTimestamp = block.timestamp + TAX_CHANGE_DELAY;

		vm.expectEmit(true, true, true, true);
		emit TaxChangeProposed(newTransferTax, newSellTax, newBuyTax, expectedTimestamp);

		token.proposeTaxChange(newTransferTax, newSellTax, newBuyTax);
	}

	/// @notice Test proposing tax change sets pending values
	function test_ProposeTaxChange_SetsPendingValues() public {
		uint256 newTransferTax = 200;
		uint256 newSellTax = 300;
		uint256 newBuyTax = 100;

		token.proposeTaxChange(newTransferTax, newSellTax, newBuyTax);

		assertEq(token.pendingTransferTaxBp(), newTransferTax);
		assertEq(token.pendingSellTaxBp(), newSellTax);
		assertEq(token.pendingBuyTaxBp(), newBuyTax);
		assertEq(token.taxChangeTimestamp(), block.timestamp + TAX_CHANGE_DELAY);
	}

	/// @notice Test cannot apply tax change before timelock expires
	function test_ApplyTaxChange_RevertsBeforeTimelock() public {
		token.proposeTaxChange(200, 300, 100);

		// Try to apply immediately
		vm.expectRevert("TIMELOCK_NOT_EXPIRED");
		token.applyTaxChange();

		// Try to apply 1 second before expiry
		vm.warp(block.timestamp + TAX_CHANGE_DELAY - 1);
		vm.expectRevert("TIMELOCK_NOT_EXPIRED");
		token.applyTaxChange();
	}

	/// @notice Test can apply tax change after timelock expires
	function test_ApplyTaxChange_SucceedsAfterTimelock() public {
		uint256 newTransferTax = 200;
		uint256 newSellTax = 300;
		uint256 newBuyTax = 100;

		token.proposeTaxChange(newTransferTax, newSellTax, newBuyTax);

		// Warp to exactly when timelock expires
		vm.warp(block.timestamp + TAX_CHANGE_DELAY);

		vm.expectEmit(true, true, true, true);
		emit TaxesUpdated(newTransferTax, newSellTax, newBuyTax);

		token.applyTaxChange();

		assertEq(token.transferTaxBp(), newTransferTax);
		assertEq(token.sellTaxBp(), newSellTax);
		assertEq(token.buyTaxBp(), newBuyTax);
		assertEq(token.taxChangeTimestamp(), 0); // Reset
	}

	/// @notice Test cannot apply tax change without proposal
	function test_ApplyTaxChange_RevertsWithoutProposal() public {
		vm.expectRevert("NO_PENDING_CHANGE");
		token.applyTaxChange();
	}

	/// @notice Test proposing invalid tax rates reverts
	function test_ProposeTaxChange_RevertsOnInvalidRates() public {
		// Transfer tax too high
		vm.expectRevert("TRANSFER_TAX_TOO_HIGH");
		token.proposeTaxChange(MAX_TAX_BP + 1, 100, 100);

		// Sell tax too high
		vm.expectRevert("SELL_TAX_TOO_HIGH");
		token.proposeTaxChange(100, MAX_TAX_BP + 1, 100);

		// Buy tax too high
		vm.expectRevert("BUY_TAX_TOO_HIGH");
		token.proposeTaxChange(100, 100, MAX_TAX_BP + 1);

		// Total tax too high (transfer + sell + buy > 1000)
		vm.expectRevert("TOTAL_TAX_TOO_HIGH");
		token.proposeTaxChange(400, 400, 300); // 400 + 400 + 300 = 1100 > 1000
	}

	/// @notice Test only owner can propose tax changes
	function test_ProposeTaxChange_OnlyOwner() public {
		vm.prank(alice);
		vm.expectRevert();
		token.proposeTaxChange(200, 300, 100);
	}

	/// @notice Test only owner can apply tax changes
	function test_ApplyTaxChange_OnlyOwner() public {
		token.proposeTaxChange(200, 300, 100);
		vm.warp(block.timestamp + TAX_CHANGE_DELAY);

		vm.prank(alice);
		vm.expectRevert();
		token.applyTaxChange();
	}

	/// @notice Test can overwrite pending proposal before applying
	function test_ProposeTaxChange_CanOverwritePending() public {
		// First proposal
		token.proposeTaxChange(200, 300, 100);
		uint256 firstTimestamp = token.taxChangeTimestamp();

		// Wait a bit
		vm.warp(block.timestamp + 1 hours);

		// Second proposal (overwrites first)
		token.proposeTaxChange(250, 350, 150);
		uint256 secondTimestamp = token.taxChangeTimestamp();

		assertEq(token.pendingTransferTaxBp(), 250);
		assertEq(token.pendingSellTaxBp(), 350);
		assertEq(token.pendingBuyTaxBp(), 150);
		assertGt(secondTimestamp, firstTimestamp);
	}

	/*//////////////////////////////////////////////////////////////
                        PERMIT TESTS (ERC20Permit)
  //////////////////////////////////////////////////////////////*/

	/// @notice Test permit allows approval via signature
	function test_Permit_AllowsApprovalViaSignature() public {
		uint256 privateKey = 0xA11CE;
		address alicePrivateKey = vm.addr(privateKey);

		// Give alice some tokens
		token.transfer(alicePrivateKey, 1000 ether);

		uint256 amount = 500 ether;
		uint256 deadline = block.timestamp + 1 hours;
		uint256 nonce = token.nonces(alicePrivateKey);

		// Create permit signature
		bytes32 structHash = keccak256(
			abi.encode(
				keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
				alicePrivateKey,
				bob,
				amount,
				nonce,
				deadline
			)
		);

		bytes32 digest = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash));

		(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);

		// Execute permit
		token.permit(alicePrivateKey, bob, amount, deadline, v, r, s);

		// Verify allowance was set
		assertEq(token.allowance(alicePrivateKey, bob), amount);
	}

	/// @notice Test permit fails with expired deadline
	function test_Permit_RevertsOnExpiredDeadline() public {
		uint256 privateKey = 0xA11CE;
		address alicePrivateKey = vm.addr(privateKey);

		uint256 amount = 500 ether;
		uint256 deadline = block.timestamp + 1 hours;
		uint256 nonce = token.nonces(alicePrivateKey);

		bytes32 structHash = keccak256(
			abi.encode(
				keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
				alicePrivateKey,
				bob,
				amount,
				nonce,
				deadline
			)
		);

		bytes32 digest = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash));
		(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);

		// Warp past deadline
		vm.warp(deadline + 1);

		// Should revert
		vm.expectRevert();
		token.permit(alicePrivateKey, bob, amount, deadline, v, r, s);
	}

	/// @notice Test permit fails with invalid signature
	function test_Permit_RevertsOnInvalidSignature() public {
		uint256 privateKey = 0xA11CE;
		address alicePrivateKey = vm.addr(privateKey);

		uint256 amount = 500 ether;
		uint256 deadline = block.timestamp + 1 hours;
		uint256 nonce = token.nonces(alicePrivateKey);

		bytes32 structHash = keccak256(
			abi.encode(
				keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
				alicePrivateKey,
				bob,
				amount,
				nonce,
				deadline
			)
		);

		bytes32 digest = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash));

		// Sign with wrong private key
		uint256 wrongPrivateKey = 0xB0B;
		(uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, digest);

		// Should revert
		vm.expectRevert();
		token.permit(alicePrivateKey, bob, amount, deadline, v, r, s);
	}

	/// @notice Test permit nonce increments after use
	function test_Permit_NonceIncrementsAfterUse() public {
		uint256 privateKey = 0xA11CE;
		address alicePrivateKey = vm.addr(privateKey);

		token.transfer(alicePrivateKey, 1000 ether);

		uint256 amount = 500 ether;
		uint256 deadline = block.timestamp + 1 hours;
		uint256 nonceBefore = token.nonces(alicePrivateKey);

		bytes32 structHash = keccak256(
			abi.encode(
				keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
				alicePrivateKey,
				bob,
				amount,
				nonceBefore,
				deadline
			)
		);

		bytes32 digest = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash));
		(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);

		token.permit(alicePrivateKey, bob, amount, deadline, v, r, s);

		uint256 nonceAfter = token.nonces(alicePrivateKey);
		assertEq(nonceAfter, nonceBefore + 1);
	}

	/// @notice Test cannot replay permit signature
	function test_Permit_CannotReplaySignature() public {
		uint256 privateKey = 0xA11CE;
		address alicePrivateKey = vm.addr(privateKey);

		token.transfer(alicePrivateKey, 1000 ether);

		uint256 amount = 500 ether;
		uint256 deadline = block.timestamp + 1 hours;
		uint256 nonce = token.nonces(alicePrivateKey);

		bytes32 structHash = keccak256(
			abi.encode(
				keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
				alicePrivateKey,
				bob,
				amount,
				nonce,
				deadline
			)
		);

		bytes32 digest = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash));
		(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest);

		// First permit succeeds
		token.permit(alicePrivateKey, bob, amount, deadline, v, r, s);

		// Replay should fail (nonce already used)
		vm.expectRevert();
		token.permit(alicePrivateKey, bob, amount, deadline, v, r, s);
	}

	/*//////////////////////////////////////////////////////////////
                        EVENT VERIFICATION TESTS
  //////////////////////////////////////////////////////////////*/

	/// @notice Test addPool emits PoolAdded event
	function test_AddPool_EmitsEvent() public {
		address pool = makeAddr("pool");

		vm.expectEmit(true, true, true, true);
		emit PoolAdded(pool);

		token.addPool(pool);
	}

	/// @notice Test removePool emits PoolRemoved event
	function test_RemovePool_EmitsEvent() public {
		address pool = makeAddr("pool");
		token.addPool(pool);

		vm.expectEmit(true, true, true, true);
		emit PoolRemoved(pool);

		token.removePool(pool);
	}

	/// @notice Test setFeeRecipient emits FeeRecipientUpdated event
	function test_SetFeeRecipient_EmitsEvent() public {
		address newRecipient = makeAddr("newRecipient");

		vm.expectEmit(true, true, true, true);
		emit FeeRecipientUpdated(feeRecipient, newRecipient);

		token.setFeeRecipient(newRecipient);
	}

	/// @notice Test executeMint emits TokensMinted and Transfer events
	function test_ExecuteMint_EmitsEvents() public {
		uint256 mintAmount = 1000 ether;

		token.proposeMint(alice, mintAmount);
		vm.warp(block.timestamp + 7 days);

		// Check TokensMinted event
		vm.expectEmit(true, true, true, true);
		emit TokensMinted(alice, mintAmount);

		token.executeMint();
	}

	/// @notice Test transfer with tax emits TaxCollected event
	function test_Transfer_EmitsTaxCollectedEvent() public {
		uint256 transferAmount = 1000 ether;
		uint256 expectedTax = (transferAmount * 100) / BASIS_POINTS_DENOMINATOR; // 1%

		vm.expectEmit(true, true, true, true);
		emit TaxCollected(owner, alice, transferAmount, expectedTax, feeRecipient);

		token.transfer(alice, transferAmount);
	}

	/// @notice Test transfer in burn mode emits TaxBurned event
	function test_Transfer_EmitsTaxBurnedEvent() public {
		// Enable burn mode
		token.setFeeRecipient(address(0));

		uint256 transferAmount = 1000 ether;
		uint256 expectedTax = (transferAmount * 100) / BASIS_POINTS_DENOMINATOR; // 1%

		vm.expectEmit(true, true, true, true);
		emit TaxBurned(owner, alice, transferAmount, expectedTax);

		token.transfer(alice, transferAmount);
	}

	/// @notice Test cancelTaxChange emits TaxChangeCancelled event
	function test_CancelTaxChange_EmitsEvent() public {
		uint256 newTransferTax = 200;
		uint256 newSellTax = 300;
		uint256 newBuyTax = 100;

		token.proposeTaxChange(newTransferTax, newSellTax, newBuyTax);

		// Cancel the pending change
		token.cancelTaxChange();

		// Verify timestamp reset
		assertEq(token.taxChangeTimestamp(), 0);
	}

	/*//////////////////////////////////////////////////////////////
                        ACCESS CONTROL TESTS
  //////////////////////////////////////////////////////////////*/

	/// @notice Test only owner can add pools
	function test_AddPool_OnlyOwner() public {
		address pool = makeAddr("pool");

		vm.prank(alice);
		vm.expectRevert();
		token.addPool(pool);
	}

	/// @notice Test only owner can remove pools
	function test_RemovePool_OnlyOwner() public {
		address pool = makeAddr("pool");
		token.addPool(pool);

		vm.prank(alice);
		vm.expectRevert();
		token.removePool(pool);
	}

	/// @notice Test only owner can set fee recipient
	function test_SetFeeRecipient_OnlyOwner() public {
		vm.prank(alice);
		vm.expectRevert();
		token.setFeeRecipient(alice);
	}

	/// @notice Test only minter role can propose mint
	function test_ProposeMint_OnlyMinterRole() public {
		vm.prank(alice);
		vm.expectRevert();
		token.proposeMint(alice, 1000 ether);
	}

	/// @notice Test only tax manager role can cancel tax change
	function test_CancelTaxChange_OnlyTaxManagerRole() public {
		token.proposeTaxChange(200, 300, 100);

		vm.prank(alice);
		vm.expectRevert();
		token.cancelTaxChange();
	}

	/*//////////////////////////////////////////////////////////////
                        EDGE CASE TESTS
  //////////////////////////////////////////////////////////////*/

	/// @notice Test cannot add zero address as pool
	function test_AddPool_RevertsOnZeroAddress() public {
		vm.expectRevert("ZERO_ADDR");
		token.addPool(address(0));
	}

	/// @notice Test cannot add same pool twice
	function test_AddPool_RevertsOnDuplicate() public {
		address pool = makeAddr("pool");
		token.addPool(pool);

		vm.expectRevert("EXISTS");
		token.addPool(pool);
	}

	/// @notice Test cannot remove non-existent pool
	function test_RemovePool_RevertsOnNonExistent() public {
		address pool = makeAddr("pool");

		vm.expectRevert("NOT_POOL");
		token.removePool(pool);
	}

	/// @notice Test cannot propose mint to zero address
	function test_ProposeMint_RevertsOnZeroAddress() public {
		vm.expectRevert("MINT_TO_ZERO");
		token.proposeMint(address(0), 1000 ether);
	}

	/// @notice Test cannot propose mint beyond max supply
	function test_ProposeMint_RevertsOnMaxSupplyExceeded() public {
		uint256 currentSupply = token.totalSupply();
		uint256 exceedAmount = MAX_SUPPLY - currentSupply + 1;

		vm.expectRevert("EXCEEDS_MAX_SUPPLY");
		token.proposeMint(alice, exceedAmount);
	}

	/// @notice Test cannot execute mint beyond rate limit
	function test_ExecuteMint_RevertsOnRateLimitExceeded() public {
		// Propose and execute first mint (10M - at limit)
		uint256 maxMintPerPeriod = 10_000_000 ether;
		token.proposeMint(alice, maxMintPerPeriod);
		vm.warp(block.timestamp + 7 days);
		token.executeMint();

		// Try to mint more in same period
		vm.expectRevert("EXCEEDS_MINT_CAP_PER_PERIOD");
		token.proposeMint(bob, 1 ether);
	}

	/// @notice Test burn reduces total supply
	function test_Burn_ReducesTotalSupply() public {
		uint256 burnAmount = 1000 ether;
		token.transfer(alice, burnAmount * 2);

		uint256 supplyBefore = token.totalSupply();
		uint256 balanceBefore = token.balanceOf(alice);

		vm.prank(alice);
		token.burn(burnAmount);

		assertEq(token.totalSupply(), supplyBefore - burnAmount);
		assertEq(token.balanceOf(alice), balanceBefore - burnAmount);
	}

	/// @notice Test burnFrom with allowance
	function test_BurnFrom_WithAllowance() public {
		uint256 burnAmount = 1000 ether;
		token.transfer(alice, burnAmount * 2);

		// Alice approves bob to burn
		vm.prank(alice);
		token.approve(bob, burnAmount);

		uint256 supplyBefore = token.totalSupply();

		// Bob burns from alice
		vm.prank(bob);
		token.burnFrom(alice, burnAmount);

		assertEq(token.totalSupply(), supplyBefore - burnAmount);
		assertEq(token.allowance(alice, bob), 0);
	}

	/// @notice Test burnFrom fails without allowance
	function test_BurnFrom_RevertsWithoutAllowance() public {
		uint256 burnAmount = 1000 ether;
		token.transfer(alice, burnAmount * 2);

		vm.prank(bob);
		vm.expectRevert();
		token.burnFrom(alice, burnAmount);
	}

	/*//////////////////////////////////////////////////////////////
                        INITIALIZATION TESTS
  //////////////////////////////////////////////////////////////*/

	/// @notice Test initial state after deployment
	function test_Initialization_CorrectState() public view {
		assertEq(token.name(), "Cyberia");
		assertEq(token.symbol(), "CAP");
		assertEq(token.decimals(), 18);
		assertEq(token.totalSupply(), INITIAL_SUPPLY);
		assertEq(token.balanceOf(owner), INITIAL_SUPPLY);
		assertEq(token.governance(), owner);
		assertEq(token.feeRecipient(), feeRecipient);
		assertEq(token.transferTaxBp(), 100); // 1%
		assertEq(token.sellTaxBp(), 100); // 1%
		assertEq(token.buyTaxBp(), 0); // 0%
	}

	/// @notice Test cannot initialize twice
	function test_Initialize_CannotReinitialize() public {
		vm.expectRevert();
		token.initialize(alice, bob);
	}

	/*//////////////////////////////////////////////////////////////
                        TAX CALCULATION TESTS
  //////////////////////////////////////////////////////////////*/

	/// @notice Test zero tax on mint
	function test_ExecuteMint_NoTax() public {
		uint256 mintAmount = 1000 ether;
		uint256 recipientBefore = token.balanceOf(feeRecipient);

		token.proposeMint(alice, mintAmount);
		vm.warp(block.timestamp + 7 days);
		token.executeMint();

		assertEq(token.balanceOf(alice), mintAmount);
		assertEq(token.balanceOf(feeRecipient), recipientBefore); // No tax collected
	}

	/// @notice Test zero tax on burn
	function test_Burn_NoTax() public {
		uint256 burnAmount = 1000 ether;
		token.transfer(alice, burnAmount * 2);

		uint256 recipientBefore = token.balanceOf(feeRecipient);
		uint256 supplyBefore = token.totalSupply();

		vm.prank(alice);
		token.burn(burnAmount);

		// No additional tax collected during burn (fee recipient balance unchanged)
		assertEq(token.balanceOf(feeRecipient), recipientBefore);
		// But supply decreased
		assertEq(token.totalSupply(), supplyBefore - burnAmount);
	}

	/// @notice Test pool-to-pool transfer has no tax
	function test_PoolToPool_NoTax() public {
		address pool1 = makeAddr("pool1");
		address pool2 = makeAddr("pool2");

		token.addPool(pool1);
		token.addPool(pool2);

		// Give tokens to pool1
		uint256 amount = 1000 ether;
		token.transfer(pool1, amount);

		uint256 pool1Balance = token.balanceOf(pool1);
		uint256 recipientBefore = token.balanceOf(feeRecipient);

		// Pool1 transfers to pool2
		vm.prank(pool1);
		token.transfer(pool2, pool1Balance);

		// Pool2 should receive full amount (no tax)
		assertEq(token.balanceOf(pool2), pool1Balance);
		assertEq(token.balanceOf(feeRecipient), recipientBefore); // No tax collected
	}

	/// @notice Test buy from pool applies buy tax only
	function test_BuyFromPool_AppliesBuyTax() public {
		address pool = makeAddr("pool");
		token.addPool(pool);

		// Set buy tax via propose/apply
		token.proposeTaxChange(100, 100, 200); // 1% transfer, 1% sell, 2% buy
		vm.warp(block.timestamp + TAX_CHANGE_DELAY);
		token.applyTaxChange();

		// Give tokens to pool (owner pays transfer tax)
		uint256 amount = 1000 ether;
		token.transfer(pool, amount);

		uint256 poolBalance = token.balanceOf(pool);
		uint256 recipientBefore = token.balanceOf(feeRecipient);

		// Pool transfers to alice (buy)
		vm.prank(pool);
		token.transfer(alice, poolBalance);

		uint256 expectedTax = (poolBalance * 200) / BASIS_POINTS_DENOMINATOR; // 2%
		uint256 taxCollected = token.balanceOf(feeRecipient) - recipientBefore;

		assertEq(taxCollected, expectedTax);
		assertEq(token.balanceOf(alice), poolBalance - expectedTax);
	}

	/// @notice Test sell to pool applies sell tax only
	function test_SellToPool_AppliesSellTax() public {
		address pool = makeAddr("pool");
		token.addPool(pool);

		// Default taxes: 1% sell tax on sells
		uint256 amount = 1000 ether;
		token.transfer(alice, amount * 2);

		uint256 aliceBalance = token.balanceOf(alice);
		uint256 recipientBefore = token.balanceOf(feeRecipient);

		// Alice sells to pool
		vm.prank(alice);
		token.transfer(pool, aliceBalance);

		uint256 expectedTax = (aliceBalance * 100) / BASIS_POINTS_DENOMINATOR; // 1% sell tax only
		uint256 taxCollected = token.balanceOf(feeRecipient) - recipientBefore;

		assertEq(taxCollected, expectedTax);
	}
}

Neighbours