diff --git a/onchain/src/Harb.sol b/onchain/src/Harb.sol index 5acda30..7bf6b20 100644 --- a/onchain/src/Harb.sol +++ b/onchain/src/Harb.sol @@ -203,19 +203,17 @@ contract Harb is ERC20, ERC20Permit { function ubiDue(address _account, uint256 lastTaxClaimed, uint256 _sumTaxCollected) internal view returns (uint256 amountDue, uint256 lastPeriodEndAt) { lastPeriodEndAt = ((block.timestamp - PERIOD_OFFSET) / uint256(PERIOD_LENGTH)) * PERIOD_LENGTH + PERIOD_OFFSET - 1; - if (lastTaxClaimed == 0 || lastTaxClaimed > lastPeriodEndAt || lastPeriodEndAt - lastTaxClaimed < PERIOD_LENGTH) { return (0, lastPeriodEndAt); } uint256 accountTwab = twabController.getTwabBetween(address(this), _account, lastTaxClaimed, lastPeriodEndAt); uint256 stakeTwab = twabController.getTwabBetween(address(this), stakingPool, lastTaxClaimed, lastPeriodEndAt); - - uint256 poolTwab = twabController.getTwabBetween(address(this), address(pool), lastTaxClaimed, lastPeriodEndAt); + uint256 taxTwab = twabController.getTwabBetween(address(this), TAX_POOL, lastTaxClaimed, lastPeriodEndAt); uint256 totalSupplyTwab = twabController.getTotalSupplyTwabBetween(address(this), lastTaxClaimed, lastPeriodEndAt); uint256 taxCollectedSinceLastClaim = sumTaxCollected - _sumTaxCollected; - amountDue = taxCollectedSinceLastClaim.mulDiv(accountTwab, (totalSupplyTwab - stakeTwab - poolTwab), Math.Rounding.Down); + amountDue = taxCollectedSinceLastClaim.mulDiv(accountTwab, (totalSupplyTwab - stakeTwab - poolTwab - taxTwab), Math.Rounding.Down); } function claimUbi(address _account) external { @@ -229,6 +227,8 @@ contract Harb is ERC20, ERC20Permit { ubiTitles[_account].time = lastPeriodEndAt; twabController.transfer(TAX_POOL, _account, SafeCast.toUint96(ubiAmountDue)); emit UbiClaimed(_account, ubiAmountDue); + } else { + revert("No UBI to claim."); } } diff --git a/onchain/test/Harb.t.sol b/onchain/test/Harb.t.sol index 15975e3..fbd8f7b 100644 --- a/onchain/test/Harb.t.sol +++ b/onchain/test/Harb.t.sol @@ -11,7 +11,6 @@ import "../src/interfaces/IWETH9.sol"; import {WETH} from "solmate/tokens/WETH.sol"; import "../src/Harb.sol"; import {BaseLineLP} from "../src/BaseLineLP.sol"; -import {Stake, ExceededAvailableStake} from "../src/Stake.sol"; address constant TAX_POOL = address(2); // default fee of 1% @@ -22,8 +21,9 @@ contract HarbTest is Test { IWETH9 weth; Harb harb; IUniswapV3Factory factory; - Stake stake; + address stakingPool; BaseLineLP liquidityManager; + TwabController tc; function deployContract(bytes memory bytecode, bytes memory constructorArgs) internal returns (address addr) { bytes memory deploymentData = abi.encodePacked(bytecode, constructorArgs); @@ -39,116 +39,323 @@ contract HarbTest is Test { factory = IUniswapV3Factory(factoryAddress); weth = IWETH9(address(new WETH())); - TwabController tc = new TwabController(60 * 60 * 24, uint32(block.timestamp)); + tc = new TwabController(60 * 60 * 24, uint32(block.timestamp)); harb = new Harb("HARB", "HARB", factoryAddress, address(weth), tc); factory = IUniswapV3Factory(factoryAddress); factory.createPool(address(weth), address(harb), FEE); - stake = new Stake(address(harb)); - harb.setStakingPool(address(stake)); + stakingPool = makeAddr("stakingPool"); // This represents the staking pool + harb.setStakingPool(stakingPool); liquidityManager = new BaseLineLP(factoryAddress, address(weth), address(harb)); harb.setLiquidityManager(address(liquidityManager)); } - function test_MintStakeUnstake(address account, uint256 amount) public { - vm.assume(amount > 10000); - vm.assume(amount < 2 ** 93); // TWAB limit = 2**96 - vm.assume(account != address(0)); - vm.assume(account != address(1)); // TWAB sponsorship address - vm.assume(account != address(2)); // tax pool address - vm.assume(account != address(harb)); - vm.assume(account != address(stake)); - address alice = makeAddr("alice"); - - // test mint - uint256 totalSupplyBefore = harb.totalSupply(); - uint256 balanceBefore = harb.balanceOf(account); - harb.setLiquidityManager(account); - vm.prank(account); - harb.mint(amount); - uint256 totalAfter = harb.totalSupply(); - assertEq(totalAfter, totalSupplyBefore + amount, "total supply should match"); - assertEq(harb.balanceOf(account), balanceBefore + amount, "balance should match"); - - // test UBI title - { - // prepare UBI title - vm.prank(account); - harb.mint(amount * 4); - vm.prank(account); - harb.transfer(alice, amount); - vm.prank(alice); - harb.transfer(account, amount); - // check ubi title - (uint256 titleSumTax, uint256 titleTime) = harb.ubiTitles(account); - assertEq(titleSumTax, 0, "no taxes paid yet"); - assertEq(block.timestamp, titleTime, "title start time should match"); - } - - // test stake - { - // get some stake - assertEq(stake.outstandingStake(), 0, "init failure"); - vm.prank(account); - harb.approve(address(stake), amount); - uint256[] memory empty; - vm.prank(account); - stake.snatch(amount, account, 1, empty); - assertEq(harb.totalSupply(), totalAfter * 5, "total supply should match after stake"); - assertEq(harb.balanceOf(account), amount * 4, "balance should match after stake"); - assertEq(harb.balanceOf(address(stake)), amount, "balance should match after stake"); - // check stake position - (uint256 share, address owner, uint32 creationTime, uint32 lastTaxTime, uint32 taxRate) = stake.positions(654321); - assertEq(share, stake.totalSupply() / 5, "share should match"); - assertEq(owner, account, "owners should match"); - assertEq(creationTime, block.timestamp, "time should match"); - assertEq(lastTaxTime, block.timestamp, "tax time should match"); - assertEq(taxRate, 1, "tax rate should match"); - } - - // test stake when stake full - { - uint256[] memory empty; - vm.expectRevert(); - //vm.expectRevert(abi.encodeWithSelector(ExceededAvailableStake.selector, account, amount, 0)); - vm.prank(account); - stake.snatch(amount, account, 2, empty); - } - - - // test unstake - { - // advance the time - uint256 timeBefore = block.timestamp; - vm.warp(timeBefore + (60 * 60 * 24 * 4)); - uint256 taxDue = stake.taxDue(654321, 60 * 60 * 24 * 3); - - uint256 sumTaxCollectedBefore = harb.sumTaxCollected(); - vm.prank(account); - stake.exitPosition(654321); - assertApproxEqRel(harb.balanceOf(account), amount * 5 - taxDue, 1e14, "account balance should match"); - assertEq(harb.balanceOf(TAX_POOL), taxDue, "tax pool balance should match"); - assertEq(sumTaxCollectedBefore + taxDue, harb.sumTaxCollected(), "collected tax should have increased"); - } - - // claim tax - { - balanceBefore = harb.balanceOf(account); - (uint256 ubiDue, ) = harb.getUbiDue(account); - vm.prank(account); - harb.claimUbi(account); - assertFalse(ubiDue == 0, "No UBI paid"); - assertEq(balanceBefore + ubiDue, harb.balanceOf(account), "ubi should match"); - } - - - // test UBIdue - { - uint256 timeBefore = block.timestamp; - vm.warp(timeBefore + (60 * 60 * 24 * 7)); - harb.getUbiDue(account); - harb.getUbiDue(alice); - } + // 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 + harb.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 + harb.transfer(address(this), amount); + } + + function testHarbConstructor() view public { + // Check if the token details are set as expected + assertEq(harb.name(), "HARB"); + assertEq(harb.symbol(), "HARB"); + + // Confirm that the TwabController address is correctly set + assertEq(address(harb.twabController()), address(tc)); + + // Check that the initial staking pool and liquidity manager are correctly set + assertEq(address(harb.stakingPool()), stakingPool); + assertEq(address(harb.liquidityManager()), address(liquidityManager)); + } + + function testMintWithEmptyStakingPool() public { + uint256 initialSupply = harb.totalSupply(); + uint256 mintAmount = 1000 * 1e18; // 1000 HARB tokens + + vm.prank(address(liquidityManager)); + harb.mint(mintAmount); + + // Check if the total supply has increased correctly + assertEq(harb.totalSupply(), initialSupply + mintAmount); + // Check if the staking pool balance is still 0, as before + assertEq(harb.balanceOf(stakingPool), 0); + } + + function testBurnWithEmptyStakingPool() public { + uint256 initialSupply = harb.totalSupply(); + uint256 burnAmount = 500 * 1e18; // 500 HARB tokens + + // First, mint some tokens to burn + vm.prank(address(liquidityManager)); + harb.mint(burnAmount); + + vm.prank(address(liquidityManager)); + harb.burn(burnAmount); + + // Check if the total supply has decreased correctly + assertEq(harb.totalSupply(), initialSupply); + // Check if the staking pool balance has decreased correctly + assertEq(harb.balanceOf(stakingPool), 0); + } + + function testMintImpactOnSimulatedStaking() public { + uint256 initialStakingPoolBalance = harb.balanceOf(stakingPool); + uint256 mintAmount = 1000 * 1e18; // 1000 HARB tokens + // Ensure the test contract has enough tokens to simulate staking + vm.prank(address(liquidityManager)); + harb.mint(mintAmount); + vm.prank(address(liquidityManager)); + harb.transfer(address(this), mintAmount); + + // Simulate staking of the minted amount + simulateStake(mintAmount); + + // Check balances after simulated staking + assertEq(harb.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)); + harb.mint(stakeAmount); + vm.prank(address(liquidityManager)); + harb.transfer(address(this), stakeAmount); + + uint256 initialTotalSupply = harb.totalSupply(); + + // Simulate staking and then unstaking + simulateStake(stakeAmount); + simulateUnstake(stakeAmount); + + // Check total supply remains unchanged after unstake + assertEq(harb.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)); + harb.mint(initialAmount); + vm.prank(address(liquidityManager)); + harb.transfer(address(this), initialAmount); + + // Limit fuzzing input to 0% - 20% + uint8 effectiveStakePercentage = _stakePercentage % 21; + uint256 stakeAmount = (initialAmount * effectiveStakePercentage) / 100; + simulateStake(stakeAmount); + + uint256 initialTotalSupply = harb.totalSupply(); + uint256 initialStakingPoolBalance = harb.balanceOf(stakingPool); + + mintAmount = bound(mintAmount, 0, 500 * 1e18); + uint256 expectedNewStake = initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance); + + vm.prank(address(liquidityManager)); + harb.mint(mintAmount); + + uint256 expectedStakingPoolBalance = initialStakingPoolBalance + expectedNewStake; + uint256 expectedTotalSupply = initialTotalSupply + mintAmount + expectedNewStake; + + assertEq(harb.totalSupply(), expectedTotalSupply, "Total supply did not match expected after mint."); + assertEq(harb.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)); + harb.mint(mintAmount); + + // Limit fuzzing input to 0% - 20% + uint8 effectiveStakePercentage = _stakePercentage % 21; + uint256 stakeAmount = (mintAmount * effectiveStakePercentage) / 100; + vm.prank(address(liquidityManager)); + harb.transfer(address(this), stakeAmount); + simulateStake(stakeAmount); + + burnAmount = bound(burnAmount, 0, 200 * 1e18); + uint256 initialTotalSupply = harb.totalSupply(); + uint256 initialStakingPoolBalance = harb.balanceOf(stakingPool); + uint256 expectedExcessStake = initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance); + + vm.prank(address(liquidityManager)); + harb.burn(burnAmount); + + uint256 expectedStakingPoolBalance = initialStakingPoolBalance - expectedExcessStake; + uint256 expectedTotalSupply = initialTotalSupply - burnAmount - expectedExcessStake; + + assertEq(harb.totalSupply(), expectedTotalSupply, "Total supply did not match expected after burn."); + assertEq(harb.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)); + harb.mint(taxAmount); + + // Initial tax collected should be zero + assertEq(harb.sumTaxCollected(), 0, "Initial tax collected should be zero."); + + // Simulate sending tokens to the TAX_POOL + vm.prank(address(liquidityManager)); + harb.transfer(TAX_POOL, taxAmount); + + // Check that sumTaxCollected has been updated correctly + assertEq(harb.sumTaxCollected(), taxAmount, "Tax collected not updated correctly after transfer to TAX_POOL."); + } + + 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)); + harb.mint(initialSupply + taxAmount); + vm.prank(address(liquidityManager)); + harb.transfer(user, initialSupply); + + // Simulate tax collection + vm.prank(address(liquidityManager)); + harb.transfer(TAX_POOL, 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(harb.balanceOf(user), initialSupply, "User should hold the entire initial supply."); + assertEq(harb.sumTaxCollected(), taxAmount, "Tax collected should match the tax amount transferred."); + + // User claims UBI + vm.prank(user); + harb.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 = harb.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(harb.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)); + harb.mint(initialSupply); + harb.transfer(account1, 400 * 1e18); // Account 1 gets 400 tokens + harb.transfer(account2, 300 * 1e18); // Account 2 gets 300 tokens + harb.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); + harb.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); + harb.transfer(account3, 100 * 1e18); // Account 2 transfers 100 tokens to Account 3 + + // Simulate tax collection after the transactions + vm.startPrank(address(liquidityManager)); + harb.mint(taxAmount); + harb.transfer(TAX_POOL, taxAmount); + vm.stopPrank(); + + // Assert sumTaxCollected before claiming UBI + assertEq(harb.sumTaxCollected(), taxAmount, "Tax collected should match the tax amount transferred."); + + // Each account claims UBI + vm.prank(account1); + harb.claimUbi(account1); + vm.prank(account2); + harb.claimUbi(account2); + vm.prank(account3); + harb.claimUbi(account3); + + + // Assert the post-claim balances reflect the TWAB calculations + { + uint256 totalDistributed = harb.balanceOf(account1) + harb.balanceOf(account2) + harb.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(harb.balanceOf(account1) - 300 * 1e18, expectedBalance1, 1 * 1e14, "Account 1's balance after claiming UBI is incorrect."); + assertApproxEqRel(harb.balanceOf(account2) - 300 * 1e18, expectedBalance2, 1 * 1e14, "Account 2's balance after claiming UBI is incorrect."); + assertApproxEqRel(harb.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)); + harb.mint(initialSupply); + harb.transfer(user, initialSupply); + vm.stopPrank(); + + // Ensure no tax has been collected yet + assertEq(harb.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 + harb.claimUbi(user); + + // Ensure the user's balance remains unchanged as no UBI should be distributed + assertEq(harb.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(harb.sumTaxCollected(), 0, "No tax should be collected, and sumTaxCollected should remain zero after the claim attempt."); + } + }