harb/onchain/test/Harb.t.sol
2024-06-19 10:33:28 +02:00

405 lines
18 KiB
Solidity

// 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/Harb.sol";
contract HarbTest is Test {
TwabController tc;
Harb harb;
address stakingPool;
address liquidityPool;
address liquidityManager;
address taxPool;
function setUp() public {
tc = new TwabController(60 * 60, uint32(block.timestamp));
harb = new Harb("HARB", "HARB", tc);
taxPool = harb.TAX_POOL();
stakingPool = makeAddr("stakingPool");
harb.setStakingPool(stakingPool);
liquidityPool = makeAddr("liquidityPool");
harb.setLiquidityPool(liquidityPool);
liquidityManager = makeAddr("liquidityManager");
harb.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
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() public view {
// 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
(address _tc, address _lm, address _sp, address _lp) = harb.getPeripheryContracts();
assertEq(_tc, address(tc));
assertEq(_lm, liquidityManager);
assertEq(_sp, stakingPool);
assertEq(_lp, liquidityPool);
}
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 taxPool
vm.prank(address(liquidityManager));
harb.transfer(taxPool, taxAmount);
// Check that sumTaxCollected has been updated correctly
assertEq(harb.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));
harb.mint(initialSupply + taxAmount);
vm.prank(address(liquidityManager));
harb.transfer(user, initialSupply);
// Simulate tax collection
vm.prank(address(liquidityManager));
harb.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(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(taxPool, 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.");
}
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));
harb.mint(initialSupply + maxTaxAmount);
harb.transfer(account1, initialSupply);
harb.transfer(taxPool, maxTaxAmount); // Simulate tax collection at the theoretical maximum
vm.stopPrank();
// Assert that maximum tax was collected
assertEq(harb.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);
harb.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(harb.balanceOf(account1), expectedBalance, "Account 1's balance after claiming UBI with max tax collection is incorrect.");
// Verify that no taxes are left unclaimed
assertEq(harb.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));
harb.mint(initialSupply + taxAmount);
harb.transfer(account1, initialSupply / 800);
harb.transfer(taxPool, taxAmount); // Simulate tax collection at the theoretical maximum
harb.transfer(liquidityPool, harb.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);
harb.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 = harb.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(harb.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(harb.balanceOf(taxPool), 0, "All taxes should be claimed after the UBI claim.");
}
}