// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "../src/Kraiken.sol"; import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; contract KraikenTest is Test { Kraiken internal kraiken; address internal stakingPool; address internal liquidityPool; address internal liquidityManager; function setUp() public { kraiken = new Kraiken("KRAIKEN", "KRK"); stakingPool = makeAddr("stakingPool"); kraiken.setStakingPool(stakingPool); liquidityManager = makeAddr("liquidityManager"); kraiken.setLiquidityManager(liquidityManager); } // Simulates staking by transferring tokens to the stakingPool address. function simulateStake(uint256 amount) internal { // the amount of token has to be available on the balance // of the test contract kraiken.transfer(stakingPool, amount); } // Simulates unstaking by transferring tokens from the stakingPool back to a given address. function simulateUnstake(uint256 amount) internal { // Direct transfer from the stakingPool to 'to' address to simulate unstaking vm.prank(stakingPool); // Assuming 'stake' contract would allow this in an actual scenario kraiken.transfer(address(this), amount); } function testKraikenConstructor() public view { // Check if the token details are set as expected assertEq(kraiken.name(), "KRAIKEN"); assertEq(kraiken.symbol(), "KRK"); // Confirm that the TwabController address is correctly set (address _lm, address _sp) = kraiken.peripheryContracts(); assertEq(_lm, liquidityManager); assertEq(_sp, stakingPool); } function testMintWithEmptyStakingPool() public { uint256 initialSupply = kraiken.totalSupply(); uint256 mintAmount = 1000 * 1e18; // 1000 HARB tokens vm.prank(address(liquidityManager)); kraiken.mint(mintAmount); // Check if the total supply has increased correctly assertEq(kraiken.totalSupply(), initialSupply + mintAmount); // Check if the staking pool balance is still 0, as before assertEq(kraiken.balanceOf(stakingPool), 0); } function testBurnWithEmptyStakingPool() public { uint256 initialSupply = kraiken.totalSupply(); uint256 burnAmount = 500 * 1e18; // 500 HARB tokens // First, mint some tokens to burn vm.prank(address(liquidityManager)); kraiken.mint(burnAmount); vm.prank(address(liquidityManager)); kraiken.burn(burnAmount); // Check if the total supply has decreased correctly assertEq(kraiken.totalSupply(), initialSupply); // Check if the staking pool balance has decreased correctly assertEq(kraiken.balanceOf(stakingPool), 0); } function testMintImpactOnSimulatedStaking() public { uint256 initialStakingPoolBalance = kraiken.balanceOf(stakingPool); uint256 mintAmount = 1000 * 1e18; // 1000 HARB tokens // Ensure the test contract has enough tokens to simulate staking vm.prank(address(liquidityManager)); kraiken.mint(mintAmount); vm.prank(address(liquidityManager)); kraiken.transfer(address(this), mintAmount); // Simulate staking of the minted amount simulateStake(mintAmount); // Check balances after simulated staking assertEq(kraiken.balanceOf(stakingPool), initialStakingPoolBalance + mintAmount); } function testUnstakeImpactOnTotalSupply() public { uint256 stakeAmount = 500 * 1e18; // 500 HARB tokens // Ensure the test contract has enough tokens to simulate staking vm.prank(address(liquidityManager)); kraiken.mint(stakeAmount); vm.prank(address(liquidityManager)); kraiken.transfer(address(this), stakeAmount); uint256 initialTotalSupply = kraiken.totalSupply(); // Simulate staking and then unstaking simulateStake(stakeAmount); simulateUnstake(stakeAmount); // Check total supply remains unchanged after unstake assertEq(kraiken.totalSupply(), initialTotalSupply); } // Fuzz test for mint function with varying stake amounts function testMintWithStake(uint8 _stakePercentage, uint256 mintAmount) public { uint256 initialAmount = 500 * 1e18; // Ensure the test contract has enough tokens to simulate staking vm.prank(address(liquidityManager)); kraiken.mint(initialAmount); vm.prank(address(liquidityManager)); kraiken.transfer(address(this), initialAmount); // Limit fuzzing input to 0% - 20% uint8 effectiveStakePercentage = _stakePercentage % 21; uint256 stakeAmount = (initialAmount * effectiveStakePercentage) / 100; simulateStake(stakeAmount); uint256 initialTotalSupply = kraiken.totalSupply(); uint256 initialStakingPoolBalance = kraiken.balanceOf(stakingPool); mintAmount = bound(mintAmount, 1, 500 * 1e18); uint256 expectedNewStake = initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance); // Expect Transfer events vm.expectEmit(true, true, true, true, address(kraiken)); emit IERC20.Transfer(address(0), address(liquidityManager), mintAmount); vm.prank(address(liquidityManager)); kraiken.mint(mintAmount); uint256 expectedStakingPoolBalance = initialStakingPoolBalance + expectedNewStake; uint256 expectedTotalSupply = initialTotalSupply + mintAmount + expectedNewStake; assertEq(kraiken.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after mint."); assertEq(kraiken.totalSupply(), expectedTotalSupply, "Total supply did not match expected after mint."); } // Fuzz test for burn function with varying stake amounts function testBurnWithStake(uint8 _stakePercentage, uint256 burnAmount) public { uint256 mintAmount = 500 * 1e18; // Ensure the test contract has enough tokens to simulate staking vm.prank(address(liquidityManager)); kraiken.mint(mintAmount); // Limit fuzzing input to 0% - 20% uint8 effectiveStakePercentage = _stakePercentage % 21; uint256 stakeAmount = (mintAmount * effectiveStakePercentage) / 100; vm.prank(address(liquidityManager)); kraiken.transfer(address(this), stakeAmount); simulateStake(stakeAmount); burnAmount = bound(burnAmount, 0, 200 * 1e18); uint256 initialTotalSupply = kraiken.totalSupply(); uint256 initialStakingPoolBalance = kraiken.balanceOf(stakingPool); uint256 expectedExcessStake = initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance); vm.prank(address(liquidityManager)); kraiken.burn(burnAmount); uint256 expectedStakingPoolBalance = initialStakingPoolBalance - expectedExcessStake; uint256 expectedTotalSupply = initialTotalSupply - burnAmount - expectedExcessStake; assertEq(kraiken.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after burn."); assertEq(kraiken.totalSupply(), expectedTotalSupply, "Total supply did not match expected after burn."); } // ======================================== // SETTER VALIDATION TESTS // ======================================== function testSetLiquidityManagerZeroAddress() public { Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK"); vm.expectRevert(Kraiken.ZeroAddressInSetter.selector); freshKraiken.setLiquidityManager(address(0)); } function testSetLiquidityManagerAlreadySet() public { // liquidityManager is already set in setUp — calling again must revert vm.expectRevert(Kraiken.AddressAlreadySet.selector); kraiken.setLiquidityManager(makeAddr("anotherLiquidityManager")); } function testSetLiquidityManagerRejectsStakingPool() public { Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK"); address sp = makeAddr("stakingPool"); freshKraiken.setStakingPool(sp); vm.expectRevert(Kraiken.InvalidAddress.selector); freshKraiken.setLiquidityManager(sp); } function testSetLiquidityManagerOnlyDeployer() public { Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK"); address nonDeployer = makeAddr("nonDeployer"); vm.prank(nonDeployer); vm.expectRevert("only deployer"); freshKraiken.setLiquidityManager(makeAddr("someManager")); } function testSetStakingPoolZeroAddress() public { Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK"); vm.expectRevert(Kraiken.ZeroAddressInSetter.selector); freshKraiken.setStakingPool(address(0)); } function testSetStakingPoolAlreadySet() public { // stakingPool is already set in setUp — calling again must revert vm.expectRevert(Kraiken.AddressAlreadySet.selector); kraiken.setStakingPool(makeAddr("anotherStakingPool")); } function testSetStakingPoolRejectsLiquidityManager() public { Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK"); address lm = makeAddr("liquidityManager"); freshKraiken.setLiquidityManager(lm); vm.expectRevert(Kraiken.InvalidAddress.selector); freshKraiken.setStakingPool(lm); } function testSetStakingPoolOnlyDeployer() public { Kraiken freshKraiken = new Kraiken("KRAIKEN", "KRK"); address nonDeployer = makeAddr("nonDeployer"); vm.prank(nonDeployer); vm.expectRevert("only deployer"); freshKraiken.setStakingPool(makeAddr("somePool")); } function testOnlyLiquidityManagerModifier() public { address nonLM = makeAddr("notLiquidityManager"); vm.prank(nonLM); vm.expectRevert("only liquidity manager"); kraiken.mint(1000 * 1e18); } // ======================================== // ZERO AMOUNT TESTS // ======================================== function testMintZeroAmount() public { // Mint a positive amount first so previousTotalSupply gets set vm.prank(address(liquidityManager)); kraiken.mint(1000 * 1e18); uint256 totalSupplyBefore = kraiken.totalSupply(); assertGt(kraiken.previousTotalSupply(), 0, "previousTotalSupply should be non-zero after first mint"); // Mint zero: should skip the entire amount > 0 block // AND should skip the previousTotalSupply == 0 block (it is already set) vm.prank(address(liquidityManager)); kraiken.mint(0); assertEq(kraiken.totalSupply(), totalSupplyBefore, "Total supply must not change when minting zero"); assertEq(kraiken.balanceOf(stakingPool), 0, "Staking pool balance must not change when minting zero"); } function testBurnZeroAmount() public { vm.prank(address(liquidityManager)); kraiken.mint(1000 * 1e18); uint256 totalSupplyBefore = kraiken.totalSupply(); // Burn zero: should skip the entire amount > 0 block vm.prank(address(liquidityManager)); kraiken.burn(0); assertEq(kraiken.totalSupply(), totalSupplyBefore, "Total supply must not change when burning zero"); } }