cyberia-token/test/foundry/CAPToken.stateful.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";

/**
 * @title CAPTokenStatefulTest
 * @notice Stateful fuzz testing for complex multi-step scenarios
 * @dev Tests sequences of operations that could expose edge cases
 */
contract CAPTokenStatefulTest is Test {
	CAPToken public token;
	CAPToken public implementation;
	address public owner;
	address public feeRecipient;

	// Test actors
	address public alice;
	address public bob;
	address public carol;
	address public pool1;
	address public pool2;

	// Constants
	uint256 constant BASIS_POINTS_DENOMINATOR = 10_000;
	uint256 constant INITIAL_SUPPLY = 1_000_000 ether;
	uint256 constant MAX_SUPPLY = 42_000_000 ether;

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

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

		// Distribute initial tokens
		token.transfer(alice, INITIAL_SUPPLY / 10);
		token.transfer(bob, INITIAL_SUPPLY / 10);
		token.transfer(carol, INITIAL_SUPPLY / 10);
	}

	/*//////////////////////////////////////////////////////////////
                        MULTI-TRANSFER SCENARIOS
  //////////////////////////////////////////////////////////////*/

	/// @notice Stateful test: Multiple sequential transfers
	function testStateful_SequentialTransfers(uint8 numTransfers, uint256 baseAmount) public {
		vm.assume(numTransfers > 0 && numTransfers <= 20);
		vm.assume(baseAmount > 1000 && baseAmount <= token.balanceOf(alice) / numTransfers);

		uint256 initialBalance = token.balanceOf(alice);
		uint256 initialTreasury = token.balanceOf(feeRecipient);

		// Execute multiple transfers
		for (uint256 i = 0; i < numTransfers; i++) {
			vm.prank(alice);
			token.transfer(bob, baseAmount);
		}

		// Verify total transferred
		uint256 totalTransferred = baseAmount * numTransfers;
		uint256 expectedTax = (totalTransferred * 100) / BASIS_POINTS_DENOMINATOR; // 1%

		assertEq(token.balanceOf(alice), initialBalance - totalTransferred, "Alice balance incorrect");
		assertApproxEqAbs(token.balanceOf(feeRecipient) - initialTreasury, expectedTax, numTransfers, "Treasury didn't receive correct tax");
	}

	/// @notice Stateful test: Circular transfers
	function testStateful_CircularTransfers(uint256 amount) public {
		vm.assume(amount > 1000 && amount <= token.balanceOf(alice) / 10);

		uint256 initialAlice = token.balanceOf(alice);
		uint256 initialBob = token.balanceOf(bob);
		uint256 initialCarol = token.balanceOf(carol);

		// Alice -> Bob -> Carol -> Alice
		vm.prank(alice);
		token.transfer(bob, amount);

		uint256 bobReceived = token.balanceOf(bob) - initialBob;

		vm.prank(bob);
		token.transfer(carol, bobReceived);

		uint256 carolReceived = token.balanceOf(carol) - initialCarol;

		vm.prank(carol);
		token.transfer(alice, carolReceived);

		// Alice should have less than initial due to 3 rounds of tax
		assertLt(token.balanceOf(alice), initialAlice, "Circular transfer should reduce total due to tax");
	}

	/*//////////////////////////////////////////////////////////////
                        POOL INTERACTION SCENARIOS
  //////////////////////////////////////////////////////////////*/

	/// @notice Stateful test: Pool addition/removal with transfers
	function testStateful_PoolManagement(uint256 transferAmount) public {
		vm.assume(transferAmount > 1000 && transferAmount <= token.balanceOf(alice) / 4);

		// Add pool1
		token.addPool(pool1);

		uint256 treasuryBefore = token.balanceOf(feeRecipient);

		// Sell to pool (should have 1% sell tax only)
		vm.prank(alice);
		token.transfer(pool1, transferAmount);

		uint256 sellTax = token.balanceOf(feeRecipient) - treasuryBefore;
		uint256 expectedSellTax = (transferAmount * 100) / BASIS_POINTS_DENOMINATOR; // 1% sell tax only
		assertEq(sellTax, expectedSellTax, "Sell tax incorrect");

		// Remove pool1
		token.removePool(pool1);

		treasuryBefore = token.balanceOf(feeRecipient);

		// Transfer to former pool (should have 1% tax now)
		vm.prank(bob);
		token.transfer(pool1, transferAmount);

		uint256 regularTax = token.balanceOf(feeRecipient) - treasuryBefore;
		uint256 expectedRegularTax = (transferAmount * 100) / BASIS_POINTS_DENOMINATOR; // 1%
		assertEq(regularTax, expectedRegularTax, "Regular tax incorrect after pool removal");
	}

	/// @notice Stateful test: Multiple pool interactions
	function testStateful_MultiplePoolSwaps(uint256 amount) public {
		vm.assume(amount > 1000 && amount <= token.balanceOf(alice) / 10);

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

		// Give tokens to pools (owner pays 1% tax)
		token.transfer(pool1, amount * 3);
		token.transfer(pool2, amount * 3);

		uint256 initialAlice = token.balanceOf(alice);

		// Alice sells to pool1 (1% sell tax only)
		vm.prank(alice);
		token.transfer(pool1, amount);

		// Pool1 received (amount - 1% tax)
		uint256 pool1Received = (amount * (BASIS_POINTS_DENOMINATOR - 100)) / BASIS_POINTS_DENOMINATOR;

		// Pool1 transfers to pool2 (pool-to-pool, no tax)
		uint256 pool2BalanceBefore = token.balanceOf(pool2);
		vm.prank(pool1);
		token.transfer(pool2, pool1Received);

		assertEq(token.balanceOf(pool2) - pool2BalanceBefore, pool1Received, "Pool-to-pool transfer should have no tax");

		// Pool2 transfers back to alice (buy, 0% tax by default)
		vm.prank(pool2);
		token.transfer(alice, pool1Received);

		// Alice should have net loss of 1% of the amount she initially sent (allow for rounding)
		uint256 expectedLoss = (amount * 100) / BASIS_POINTS_DENOMINATOR;
		assertApproxEqAbs(initialAlice - token.balanceOf(alice), expectedLoss, 1, "Alice should have net loss equal to sell tax");
	}

	/*//////////////////////////////////////////////////////////////
                        BURN AND MINT SCENARIOS
  //////////////////////////////////////////////////////////////*/

	/// @notice Stateful test: Burn and mint cycle
	function testStateful_BurnMintCycle(uint256 burnAmount, uint256 mintAmount) public {
		vm.assume(burnAmount > 0 && burnAmount <= token.balanceOf(alice) / 2);
		uint256 maxMintPerPeriod = 10_000_000 ether; // Rate limit
		vm.assume(mintAmount > 0 && mintAmount <= MAX_SUPPLY - token.totalSupply());
		vm.assume(mintAmount <= maxMintPerPeriod); // Respect rate limit

		uint256 initialSupply = token.totalSupply();

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

		assertEq(token.totalSupply(), initialSupply - burnAmount, "Supply should decrease by burn amount");

		// Mint with timelock
		token.proposeMint(bob, mintAmount);
		vm.warp(block.timestamp + 7 days);
		token.executeMint();

		assertEq(token.totalSupply(), initialSupply - burnAmount + mintAmount, "Supply should reflect burn and mint");
	}

	/// @notice Stateful test: Multiple burns from different users
	function testStateful_MultipleBurns(uint256 amount1, uint256 amount2, uint256 amount3) public {
		vm.assume(amount1 > 0 && amount1 <= token.balanceOf(alice));
		vm.assume(amount2 > 0 && amount2 <= token.balanceOf(bob));
		vm.assume(amount3 > 0 && amount3 <= token.balanceOf(carol));

		uint256 initialSupply = token.totalSupply();
		uint256 totalBurned = amount1 + amount2 + amount3;

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

		vm.prank(bob);
		token.burn(amount2);

		vm.prank(carol);
		token.burn(amount3);

		assertEq(token.totalSupply(), initialSupply - totalBurned, "Total burned amount should match supply decrease");
	}

	/*//////////////////////////////////////////////////////////////
                        DELEGATION SCENARIOS
  //////////////////////////////////////////////////////////////*/

	/// @notice Stateful test: Delegation chain with transfers
	function testStateful_DelegationWithTransfers(uint256 transferAmount) public {
		vm.assume(transferAmount > 0 && transferAmount <= token.balanceOf(alice) / 4);

		// Setup delegation: alice -> bob, bob -> carol, carol -> carol
		vm.prank(alice);
		token.delegate(bob);

		vm.prank(bob);
		token.delegate(carol);

		vm.prank(carol);
		token.delegate(carol);

		vm.roll(block.number + 1);

		// Carol should have voting power from bob's balance + own balance
		uint256 carolInitialVotes = token.getVotes(carol);
		uint256 expectedVotes = token.balanceOf(bob) + token.balanceOf(carol);
		assertEq(carolInitialVotes, expectedVotes, "Carol should have bob + carol voting power");

		// Alice transfers to bob
		vm.prank(alice);
		token.transfer(bob, transferAmount);

		vm.roll(block.number + 1);

		// Bob's balance increased, so carol's voting power should increase
		uint256 carolNewVotes = token.getVotes(carol);
		assertGt(carolNewVotes, carolInitialVotes, "Carol's voting power should increase after bob receives tokens");
	}

	/// @notice Stateful test: Delegation changes during transfers
	function testStateful_DelegationChanges(uint256 amount) public {
		vm.assume(amount > 1000 && amount <= token.balanceOf(alice) / 2);

		// Alice delegates to bob
		vm.prank(alice);
		token.delegate(bob);

		vm.roll(block.number + 1);

		uint256 bobInitialVotes = token.getVotes(bob);
		assertEq(bobInitialVotes, token.balanceOf(alice), "Bob should have alice's voting power");

		// Alice transfers half
		vm.prank(alice);
		token.transfer(carol, amount);

		vm.roll(block.number + 1);

		// Bob's voting power should decrease
		uint256 bobNewVotes = token.getVotes(bob);
		assertLt(bobNewVotes, bobInitialVotes, "Bob's voting power should decrease after alice transfers");

		// Alice changes delegation to carol
		vm.prank(alice);
		token.delegate(carol);

		vm.roll(block.number + 1);

		// Bob should have no voting power from alice now
		assertEq(token.getVotes(bob), 0, "Bob should have no voting power after delegation change");

		// Carol should have voting power from alice's remaining balance
		assertGt(token.getVotes(carol), 0, "Carol should have voting power from alice");
	}

	/*//////////////////////////////////////////////////////////////
                        TAX CHANGE SCENARIOS
  //////////////////////////////////////////////////////////////*/

	/// @notice Stateful test: Tax changes with ongoing transfers
	function testStateful_TaxChangeDuringTransfers(uint256 amount) public {
		vm.assume(amount > 1000 && amount <= token.balanceOf(alice) / 10);

		// Transfer with initial tax (1%)
		uint256 treasuryBefore = token.balanceOf(feeRecipient);
		vm.prank(alice);
		token.transfer(bob, amount);
		uint256 tax1 = token.balanceOf(feeRecipient) - treasuryBefore;

		// Change taxes with timelock
		token.proposeTaxChange(200, 300, 50); // 2%, 3%, 0.5%
		vm.warp(block.timestamp + 24 hours);
		token.applyTaxChange();

		// Transfer with new tax (2%)
		treasuryBefore = token.balanceOf(feeRecipient);
		vm.prank(bob);
		token.transfer(carol, amount);
		uint256 tax2 = token.balanceOf(feeRecipient) - treasuryBefore;

		// Second tax should be approximately 2x first tax (allow for rounding)
		assertApproxEqAbs(tax2, tax1 * 2, 1, "Tax should scale with new rate");
	}

	/*//////////////////////////////////////////////////////////////
                        ALLOWANCE SCENARIOS
  //////////////////////////////////////////////////////////////*/

	/// @notice Stateful test: Multiple transferFrom operations
	function testStateful_MultipleTransferFrom(uint256 allowanceAmount, uint8 numTransfers) public {
		vm.assume(numTransfers > 0 && numTransfers <= 10);
		vm.assume(allowanceAmount > numTransfers * 1000 && allowanceAmount <= token.balanceOf(alice));

		uint256 transferAmount = allowanceAmount / numTransfers;

		// Alice approves bob
		vm.prank(alice);
		token.approve(bob, allowanceAmount);

		uint256 initialAllowance = token.allowance(alice, bob);

		// Bob transfers from alice multiple times
		for (uint256 i = 0; i < numTransfers; i++) {
			vm.prank(bob);
			token.transferFrom(alice, carol, transferAmount);
		}

		uint256 finalAllowance = token.allowance(alice, bob);
		uint256 totalTransferred = transferAmount * numTransfers;

		assertApproxEqAbs(finalAllowance, initialAllowance - totalTransferred, numTransfers, "Allowance should decrease by total transferred");
	}

	/*//////////////////////////////////////////////////////////////
                        BURN MODE SCENARIOS
  //////////////////////////////////////////////////////////////*/

	/// @notice Stateful test: Switch between burn mode and normal mode
	function testStateful_BurnModeSwitch(uint256 amount) public {
		vm.assume(amount > 1000 && amount <= token.balanceOf(alice) / 10);

		uint256 initialSupply = token.totalSupply();

		// Enable burn mode
		token.setFeeRecipient(address(0));

		// Transfer in burn mode (tax should be burned)
		vm.prank(alice);
		token.transfer(bob, amount);

		uint256 supplyAfterBurn = token.totalSupply();
		assertLt(supplyAfterBurn, initialSupply, "Supply should decrease in burn mode");

		// Disable burn mode
		token.setFeeRecipient(feeRecipient);

		// Transfer in normal mode (tax should go to treasury)
		uint256 treasuryBefore = token.balanceOf(feeRecipient);
		vm.prank(bob);
		token.transfer(carol, amount);

		assertGt(token.balanceOf(feeRecipient), treasuryBefore, "Treasury should receive tax after disabling burn mode");
		assertEq(token.totalSupply(), supplyAfterBurn, "Supply should not change in normal mode");
	}

	/*//////////////////////////////////////////////////////////////
                        EDGE CASE SCENARIOS
  //////////////////////////////////////////////////////////////*/

	/// @notice Stateful test: Maximum tax with various transfer patterns
	function testStateful_MaxTaxScenarios(uint256 amount) public {
		vm.assume(amount > 1000 && amount <= token.balanceOf(alice) / 20);

		// Set maximum taxes (within total cap: 4% + 4% + 2% = 10%)
		token.proposeTaxChange(400, 400, 200); // 4% transfer, 4% sell, 2% buy
		vm.warp(block.timestamp + 24 hours);
		token.applyTaxChange();

		token.addPool(pool1);

		// Regular transfer (4%)
		uint256 treasuryBefore = token.balanceOf(feeRecipient);
		vm.prank(alice);
		token.transfer(bob, amount);
		uint256 regularTax = token.balanceOf(feeRecipient) - treasuryBefore;
		assertEq(regularTax, (amount * 400) / BASIS_POINTS_DENOMINATOR, "Regular tax should be 4%");

		// Sell to pool (4% sell tax only)
		treasuryBefore = token.balanceOf(feeRecipient);
		vm.prank(bob);
		token.transfer(pool1, amount);
		uint256 sellTax = token.balanceOf(feeRecipient) - treasuryBefore;
		assertEq(sellTax, (amount * 400) / BASIS_POINTS_DENOMINATOR, "Sell tax should be 4%");

		// Give tokens to pool
		token.transfer(pool1, amount * 2);

		// Buy from pool (2%)
		treasuryBefore = token.balanceOf(feeRecipient);
		vm.prank(pool1);
		token.transfer(carol, amount);
		uint256 buyTax = token.balanceOf(feeRecipient) - treasuryBefore;
		assertEq(buyTax, (amount * 200) / BASIS_POINTS_DENOMINATOR, "Buy tax should be 2%");
	}

	/// @notice Stateful test: Rapid pool status changes
	function testStateful_RapidPoolChanges(uint256 amount) public {
		vm.assume(amount > 1000 && amount <= token.balanceOf(alice) / 20);

		for (uint256 i = 0; i < 5; i++) {
			// Add pool
			token.addPool(pool1);

			// Transfer to pool
			vm.prank(alice);
			token.transfer(pool1, amount);

			// Remove pool
			token.removePool(pool1);

			// Transfer from former pool
			vm.prank(pool1);
			token.transfer(bob, amount / 2);
		}

		// Verify balances are consistent
		uint256 totalSupply = token.totalSupply();
		uint256 sumBalances = token.balanceOf(alice) +
			token.balanceOf(bob) +
			token.balanceOf(carol) +
			token.balanceOf(pool1) +
			token.balanceOf(feeRecipient) +
			token.balanceOf(owner);

		assertEq(sumBalances, totalSupply, "Sum of balances should equal total supply after rapid changes");
	}

	/*//////////////////////////////////////////////////////////////
                        STRESS TEST SCENARIOS
  //////////////////////////////////////////////////////////////*/

	/// @notice Stateful test: Complex multi-actor scenario
	function testStateful_ComplexMultiActorScenario(uint256 seed) public {
		uint256 amount = bound(seed, 1000, token.balanceOf(alice) / 50);

		// Setup
		token.addPool(pool1);
		vm.prank(alice);
		token.delegate(alice);
		vm.prank(bob);
		token.delegate(bob);

		uint256 initialSupply = token.totalSupply();

		// Execute complex sequence
		vm.prank(alice);
		token.transfer(bob, amount); // Transfer

		vm.prank(bob);
		token.approve(carol, amount); // Approve

		vm.prank(carol);
		token.transferFrom(bob, pool1, amount / 2); // TransferFrom to pool (sell)

		token.transfer(pool1, amount * 2); // Give more tokens to pool

		vm.prank(pool1);
		token.transfer(alice, amount); // Buy

		vm.prank(alice);
		token.burn(amount / 4); // Burn

		// Mint with timelock
		token.proposeMint(carol, amount / 2);
		vm.warp(block.timestamp + 7 days);
		token.executeMint();

		// Verify final state is consistent
		uint256 finalSupply = token.totalSupply();

		// Supply should have decreased by burn and increased by mint
		assertApproxEqAbs(finalSupply, initialSupply - amount / 4 + amount / 2, 1e18, "Supply changes should match operations");

		// Sum of all balances should equal total supply
		uint256 sumBalances = token.balanceOf(alice) +
			token.balanceOf(bob) +
			token.balanceOf(carol) +
			token.balanceOf(pool1) +
			token.balanceOf(feeRecipient) +
			token.balanceOf(owner);

		assertEq(sumBalances, finalSupply, "Sum of balances should equal total supply");
	}
}

Neighbours