// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "forge-std/console.sol"; import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; import "../src/Harberg.sol"; contract HarbergTest is Test { TwabController tc; Harberg harberg; address stakingPool; address liquidityPool; address liquidityManager; address taxPool; function setUp() public { tc = new TwabController(60 * 60, uint32(block.timestamp)); harberg = new Harberg("HARB", "HARB", tc); taxPool = harberg.TAX_POOL(); stakingPool = makeAddr("stakingPool"); harberg.setStakingPool(stakingPool); liquidityPool = makeAddr("liquidityPool"); harberg.setLiquidityPool(liquidityPool); liquidityManager = makeAddr("liquidityManager"); harberg.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 harberg.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 harberg.transfer(address(this), amount); } function testHarbergConstructor() public view { // Check if the token details are set as expected assertEq(harberg.name(), "HARB"); assertEq(harberg.symbol(), "HARB"); // Confirm that the TwabController address is correctly set (address _tc, address _lm, address _sp, address _lp) = harberg.peripheryContracts(); assertEq(_tc, address(tc)); assertEq(_lm, liquidityManager); assertEq(_sp, stakingPool); assertEq(_lp, liquidityPool); } function testMintWithEmptyStakingPool() public { uint256 initialSupply = harberg.totalSupply(); uint256 mintAmount = 1000 * 1e18; // 1000 HARB tokens vm.prank(address(liquidityManager)); harberg.mint(mintAmount); // Check if the total supply has increased correctly assertEq(harberg.totalSupply(), initialSupply + mintAmount); // Check if the staking pool balance is still 0, as before assertEq(harberg.balanceOf(stakingPool), 0); } function testBurnWithEmptyStakingPool() public { uint256 initialSupply = harberg.totalSupply(); uint256 burnAmount = 500 * 1e18; // 500 HARB tokens // First, mint some tokens to burn vm.prank(address(liquidityManager)); harberg.mint(burnAmount); vm.prank(address(liquidityManager)); harberg.burn(burnAmount); // Check if the total supply has decreased correctly assertEq(harberg.totalSupply(), initialSupply); // Check if the staking pool balance has decreased correctly assertEq(harberg.balanceOf(stakingPool), 0); } function testMintImpactOnSimulatedStaking() public { uint256 initialStakingPoolBalance = harberg.balanceOf(stakingPool); uint256 mintAmount = 1000 * 1e18; // 1000 HARB tokens // Ensure the test contract has enough tokens to simulate staking vm.prank(address(liquidityManager)); harberg.mint(mintAmount); vm.prank(address(liquidityManager)); harberg.transfer(address(this), mintAmount); // Simulate staking of the minted amount simulateStake(mintAmount); // Check balances after simulated staking assertEq(harberg.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)); harberg.mint(stakeAmount); vm.prank(address(liquidityManager)); harberg.transfer(address(this), stakeAmount); uint256 initialTotalSupply = harberg.totalSupply(); // Simulate staking and then unstaking simulateStake(stakeAmount); simulateUnstake(stakeAmount); // Check total supply remains unchanged after unstake assertEq(harberg.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)); harberg.mint(initialAmount); vm.prank(address(liquidityManager)); harberg.transfer(address(this), initialAmount); // Limit fuzzing input to 0% - 20% uint8 effectiveStakePercentage = _stakePercentage % 21; uint256 stakeAmount = (initialAmount * effectiveStakePercentage) / 100; simulateStake(stakeAmount); uint256 initialTotalSupply = harberg.totalSupply(); uint256 initialStakingPoolBalance = harberg.balanceOf(stakingPool); mintAmount = bound(mintAmount, 0, 500 * 1e18); uint256 expectedNewStake = initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance); vm.prank(address(liquidityManager)); harberg.mint(mintAmount); uint256 expectedStakingPoolBalance = initialStakingPoolBalance + expectedNewStake; uint256 expectedTotalSupply = initialTotalSupply + mintAmount + expectedNewStake; assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply did not match expected after mint."); assertEq(harberg.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly 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)); harberg.mint(mintAmount); // Limit fuzzing input to 0% - 20% uint8 effectiveStakePercentage = _stakePercentage % 21; uint256 stakeAmount = (mintAmount * effectiveStakePercentage) / 100; vm.prank(address(liquidityManager)); harberg.transfer(address(this), stakeAmount); simulateStake(stakeAmount); burnAmount = bound(burnAmount, 0, 200 * 1e18); uint256 initialTotalSupply = harberg.totalSupply(); uint256 initialStakingPoolBalance = harberg.balanceOf(stakingPool); uint256 expectedExcessStake = initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance); vm.prank(address(liquidityManager)); harberg.burn(burnAmount); uint256 expectedStakingPoolBalance = initialStakingPoolBalance - expectedExcessStake; uint256 expectedTotalSupply = initialTotalSupply - burnAmount - expectedExcessStake; assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply did not match expected after burn."); assertEq(harberg.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after burn."); } function testTaxAccumulation() public { uint256 taxAmount = 100 * 1e18; // 100 HARB tokens vm.prank(address(liquidityManager)); harberg.mint(taxAmount); // Initial tax collected should be zero assertEq(harberg.sumTaxCollected(), 0, "Initial tax collected should be zero."); // Simulate sending tokens to the taxPool vm.prank(address(liquidityManager)); harberg.transfer(taxPool, taxAmount); // Check that sumTaxCollected has been updated correctly assertEq(harberg.sumTaxCollected(), taxAmount, "Tax collected not updated correctly after transfer to taxPool."); } function testUBIClaimBySingleAccountOverTime() public { uint256 initialSupply = 1000 * 1e18; // 1000 HARB tokens uint256 taxAmount = 200 * 1e18; // 200 HARB tokens to be collected as tax address user = makeAddr("alice"); // Setup initial supply and distribute to user vm.prank(address(liquidityManager)); harberg.mint(initialSupply + taxAmount); vm.prank(address(liquidityManager)); harberg.transfer(user, initialSupply); // Simulate tax collection vm.prank(address(liquidityManager)); harberg.transfer(taxPool, taxAmount); // Simulate time passage to ensure TWAB is recorded over time vm.warp(block.timestamp + 30 days); // Assert initial user balance and sumTaxCollected before claiming UBI assertEq(harberg.balanceOf(user), initialSupply, "User should hold the entire initial supply."); assertEq(harberg.sumTaxCollected(), taxAmount, "Tax collected should match the tax amount transferred."); // User claims UBI vm.prank(user); harberg.claimUbi(user); // Compute expected UBI // Assume the user is the only one holding tokens, they get all the collected taxes. uint256 expectedUbiAmount = taxAmount; // Verify UBI claim uint256 postClaimBalance = harberg.balanceOf(user); assertEq(postClaimBalance, initialSupply + expectedUbiAmount, "User's balance after claiming UBI is incorrect."); // Ensure that claiming doesn't affect the total supply uint256 expectedTotalSupply = initialSupply + taxAmount; // Include minted tax assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply should be unchanged after UBI claim."); } function testUBIClaimBySingleAccountWithWraparound() public { uint256 initialSupply = 1000 * 1e18; // 1000 HARB tokens uint256 taxAmount = 200 * 1e18; // 200 HARB tokens to be collected as tax uint256 nearMaxUint = type(uint256).max - 10; address user = makeAddr("alice"); // Set sumTaxCollected to near max value to simulate wrap-around vm.store( address(harberg), bytes32(uint256(9)), bytes32(nearMaxUint) ); // Read the value back to confirm it's set correctly assertEq(harberg.sumTaxCollected(), nearMaxUint, "Initial sumTaxCollected should be near max uint256"); // Setup initial supply and distribute to user vm.prank(address(liquidityManager)); harberg.mint(initialSupply + taxAmount); vm.prank(address(liquidityManager)); harberg.transfer(user, initialSupply); // Simulate tax collection to cause overflow vm.prank(address(liquidityManager)); harberg.transfer(taxPool, taxAmount); // Simulate time passage to ensure TWAB is recorded over time vm.warp(block.timestamp + 30 days); // Verify the new value of sumTaxCollected after overflow uint256 newSumTaxCollected = harberg.sumTaxCollected(); assertGt(taxAmount, newSumTaxCollected, "sumTaxCollected should have wrapped around and be less than taxAmount"); // User claims UBI vm.prank(user); harberg.claimUbi(user); // Compute expected UBI // Assume the user is the only one holding tokens, they get all the collected taxes. uint256 expectedUbiAmount = taxAmount; // Verify UBI claim uint256 postClaimBalance = harberg.balanceOf(user); assertApproxEqRel(postClaimBalance, initialSupply + expectedUbiAmount, 1 * 1e14, "User's balance after claiming UBI is incorrect."); // Ensure that claiming doesn't affect the total supply uint256 expectedTotalSupply = initialSupply + taxAmount; // Include minted tax assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply should be unchanged after UBI claim."); } function testUBIClaimByMultipleAccountsWithDifferentHoldingPeriods() public { uint256 initialSupply = 1000 * 1e18; // 1000 HARB tokens uint256 taxAmount = 300 * 1e18; // 300 HARB tokens to be collected as tax address account1 = makeAddr("alice"); address account2 = makeAddr("bob"); address account3 = makeAddr("charly"); // Setup initial supply and distribute to users vm.startPrank(address(liquidityManager)); harberg.mint(initialSupply); harberg.transfer(account1, 400 * 1e18); // Account 1 gets 400 tokens harberg.transfer(account2, 300 * 1e18); // Account 2 gets 300 tokens harberg.transfer(account3, 300 * 1e18); // Account 3 gets 300 tokens vm.stopPrank(); uint256 startTime = block.timestamp; // Simulate different holding periods vm.warp(block.timestamp + 10 days); // Fast forward 10 days vm.prank(account1); harberg.transfer(account2, 100 * 1e18); // Account 1 transfers 100 tokens to Account 2 vm.warp(block.timestamp + 20 days); // Fast forward another 20 days vm.prank(account2); harberg.transfer(account3, 100 * 1e18); // Account 2 transfers 100 tokens to Account 3 // Simulate tax collection after the transactions vm.startPrank(address(liquidityManager)); harberg.mint(taxAmount); harberg.transfer(taxPool, taxAmount); vm.stopPrank(); // Assert sumTaxCollected before claiming UBI assertEq(harberg.sumTaxCollected(), taxAmount, "Tax collected should match the tax amount transferred."); // Each account claims UBI vm.prank(account1); harberg.claimUbi(account1); vm.prank(account2); harberg.claimUbi(account2); vm.prank(account3); harberg.claimUbi(account3); // Assert the post-claim balances reflect the TWAB calculations { uint256 totalDistributed = harberg.balanceOf(account1) + harberg.balanceOf(account2) + harberg.balanceOf(account3) - initialSupply; // Tolerance setup: 0.01% of the total tax amount uint256 lowerBound = taxAmount - (taxAmount / 10000); assertTrue(totalDistributed >= lowerBound && totalDistributed <= totalDistributed, "Total distributed UBI does not match the total tax collected within an acceptable tolerance range."); } // Calculate expected UBI amounts based on simplified TWAB assumptions // These should be replaced with actual TWAB calculations from your contract uint256 totalPeriod = block.timestamp - startTime; uint256 account1TWAB = (400 * 1e18 * 10 days + 300 * 1e18 * (totalPeriod - 10 days)) / totalPeriod; uint256 account2TWAB = (300 * 1e18 * 10 days + 400 * 1e18 * 20 days + 300 * 1e18 * (totalPeriod - 30 days)) / totalPeriod; uint256 account3TWAB = (300 * 1e18 * 30 days + 400 * 1e18 * (totalPeriod - 30 days)) / totalPeriod; uint256 totalTWAB = account1TWAB + account2TWAB + account3TWAB; // Calculate exact expected UBI payouts uint256 expectedBalance1 = (taxAmount * account1TWAB) / totalTWAB; uint256 expectedBalance2 = (taxAmount * account2TWAB) / totalTWAB; uint256 expectedBalance3 = (taxAmount * account3TWAB) / totalTWAB; // Assert the post-claim balances reflect the TWAB calculations with a smaller rounding tolerance // 1 * 1e14; // 0.0001 HARB token tolerance for rounding errors assertApproxEqRel(harberg.balanceOf(account1) - 300 * 1e18, expectedBalance1, 1 * 1e14, "Account 1's balance after claiming UBI is incorrect."); assertApproxEqRel(harberg.balanceOf(account2) - 300 * 1e18, expectedBalance2, 1 * 1e14, "Account 2's balance after claiming UBI is incorrect."); assertApproxEqRel(harberg.balanceOf(account3) - 400 * 1e18, expectedBalance3, 1 * 1e14, "Account 3's balance after claiming UBI is incorrect."); } function testUBIClaimWithoutAnyTaxCollected() public { uint256 initialSupply = 1000 * 1e18; // 1000 HARB tokens address user = makeAddr("alice"); // Setup initial supply and allocate to user vm.startPrank(address(liquidityManager)); harberg.mint(initialSupply); harberg.transfer(user, initialSupply); vm.stopPrank(); // Ensure no tax has been collected yet assertEq(harberg.sumTaxCollected(), 0, "Initial tax collected should be zero."); // Simulate time passage to ensure TWAB is recorded vm.warp(block.timestamp + 30 days); // User attempts to claim UBI vm.prank(user); vm.expectRevert("No UBI to claim."); // Assuming your contract reverts with a message when there's no UBI to claim harberg.claimUbi(user); // Ensure the user's balance remains unchanged as no UBI should be distributed assertEq(harberg.balanceOf(user), initialSupply, "User's balance should not change after attempting to claim UBI without any taxes collected."); // Check if sumTaxCollected remains zero after the claim attempt assertEq(harberg.sumTaxCollected(), 0, "No tax should be collected, and sumTaxCollected should remain zero after the claim attempt."); } function testEdgeCaseWithMaximumTaxCollection() public { uint256 initialSupply = 1e24; // Large number of HARB tokens to simulate realistic large-scale deployment uint256 maxTaxAmount = type(uint96).max - initialSupply; // Setting max tax just below overflow threshold when added to total supply address account1 = makeAddr("alice"); // Setup initial supply and allocate to user vm.startPrank(address(liquidityManager)); harberg.mint(initialSupply + maxTaxAmount); harberg.transfer(account1, initialSupply); harberg.transfer(taxPool, maxTaxAmount); // Simulate tax collection at the theoretical maximum vm.stopPrank(); // Assert that maximum tax was collected assertEq(harberg.sumTaxCollected(), maxTaxAmount, "Max tax collected should match the max tax amount transferred."); // Simulate time passage and UBI claim vm.warp(block.timestamp + 30 days); // Account 1 claims UBI vm.prank(account1); harberg.claimUbi(account1); // Check if the account's balance increased correctly uint256 expectedBalance = initialSupply + maxTaxAmount; // This assumes the entire tax pool goes to one account, simplify as needed assertEq(harberg.balanceOf(account1), expectedBalance, "Account 1's balance after claiming UBI with max tax collection is incorrect."); // Verify that no taxes are left unclaimed assertEq(harberg.balanceOf(taxPool), 0, "All taxes should be claimed after the UBI claim."); } // TODO: why is this test passing even though it exceeds MAX_CARDINALITY? function testTwabBeyondBuffer() public { uint256 initialSupply = 1000 * 1e18; // 1000 HARB tokens uint256 taxAmount = 300 * 1e18; // 300 HARB tokens to be collected as tax address account1 = makeAddr("alice"); // Setup initial supply and allocate to user vm.startPrank(address(liquidityManager)); harberg.mint(initialSupply + taxAmount); harberg.transfer(account1, initialSupply / 800); harberg.transfer(taxPool, taxAmount); // Simulate tax collection at the theoretical maximum harberg.transfer(liquidityPool, harberg.balanceOf(address(liquidityManager))); vm.stopPrank(); vm.warp(block.timestamp + 1 hours); // Simulate updates over a longer period, e.g., enough to potentially wrap the buffer. uint numHours = 399; // More than 365 to potentially test buffer wrapping (MAX_CARDINALITY) for (uint i = 0; i < numHours; i++) { vm.prank(liquidityPool); harberg.transfer(account1, initialSupply / 800); vm.warp(block.timestamp + 1 hours); // Fast-forward time by one hour. } // Account 1 claims UBI vm.prank(account1); uint256 ubiCollected = harberg.claimUbi(account1); // Check if the account's balance increased correctly uint256 expectedBalance = (initialSupply / 2) + ubiCollected; // This assumes the entire tax pool goes to one account, simplify as needed assertApproxEqRel(harberg.balanceOf(account1), expectedBalance, 1 * 1e18, "Account 1's balance after claiming UBI with max tax collection is incorrect."); // Verify that no taxes are left unclaimed assertEq(harberg.balanceOf(taxPool), 0, "All taxes should be claimed after the UBI claim."); } }