From 092f88a6683fd09f28288b077b4f9c6962c2a2d1 Mon Sep 17 00:00:00 2001 From: giteadmin Date: Thu, 23 Jan 2025 13:21:49 +0100 Subject: [PATCH] took out UBI and cleaned up --- onchain/script/DeployScript.sol | 19 +- onchain/src/Harberg.sol | 256 +++-------------------- onchain/src/LiquidityManager.sol | 18 +- onchain/src/Stake.sol | 8 +- onchain/test/Harberg.t.sol | 302 +--------------------------- onchain/test/LiquidityManager.t.sol | 8 +- onchain/test/Sentimenter.t.sol | 22 +- onchain/test/Simulations.t.sol | 42 ++-- onchain/test/Stake.t.sol | 11 +- 9 files changed, 88 insertions(+), 598 deletions(-) diff --git a/onchain/script/DeployScript.sol b/onchain/script/DeployScript.sol index d0da04d..693a855 100644 --- a/onchain/script/DeployScript.sol +++ b/onchain/script/DeployScript.sol @@ -1,7 +1,6 @@ pragma solidity ^0.8.19; import "forge-std/Script.sol"; -import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "../src/Harberg.sol"; @@ -28,24 +27,16 @@ contract DeployScript is Script { vm.startBroadcast(privateKey); //address sender = vm.addr(privateKey); - TwabController tc; - if (twabc == address(0)) { - tc = TwabController(twabc); - } else { - // in case you want to deploy an new TwabController - tc = new TwabController(60 * 60, uint32(block.timestamp)); - } - Harberg harb = new Harberg("Harbergerger Tax", "HARB", tc); + Harberg harb = new Harberg("Harbergerger Tax", "HARB"); token0isWeth = address(weth) < address(harb); - Stake stake = new Stake(address(harb)); + Stake stake = new Stake(address(harb), feeDest); harb.setStakingPool(address(stake)); IUniswapV3Factory factory = IUniswapV3Factory(v3Factory); address liquidityPool = factory.createPool(weth, address(harb), FEE); IUniswapV3Pool(liquidityPool).initializePoolFor1Cent(token0isWeth); - harb.setLiquidityPool(liquidityPool); - Sentimenter sentimenter = new Sentimenter(); - bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(harb),address(stake)); - ERC1967Proxy proxy = new ERC1967Proxy(address(sentimenter), params); + Sentimenter sentimenter = new Sentimenter(); + bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(harb),address(stake)); + ERC1967Proxy proxy = new ERC1967Proxy(address(sentimenter), params); LiquidityManager liquidityManager = new LiquidityManager(v3Factory, weth, address(harb), address(proxy)); liquidityManager.setFeeDestination(feeDest); // note: this delayed initialization is not a security issue. diff --git a/onchain/src/Harberg.sol b/onchain/src/Harberg.sol index 189d6f9..ed2452d 100644 --- a/onchain/src/Harberg.sol +++ b/onchain/src/Harberg.sol @@ -3,59 +3,25 @@ pragma solidity ^0.8.19; import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol"; import {ERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; -import {SafeCast} from "@openzeppelin/utils/math/SafeCast.sol"; import {Math} from "@openzeppelin/utils/math/Math.sol"; -import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; /** - * @title Harberg ERC20 Token - * @notice This contract implements an ERC20 token with mechanisms for minting, burning, and transferring tokens, - * integrated with a TWAB controller for time-weighted balance tracking. This token supports a novel economic model - * that finances Universal Basic Income (UBI) through a Harberger tax applied to staked tokens. The liquidity manager - * manages token supply to stabilize market liquidity. + * @title stakeable ERC20 Token + * @notice This contract implements an ERC20 token with mechanisms for minting and burning in which a single account (staking Pool) is proportionally receiving a share. Only the liquidity manager has permission to manage token supply. */ contract Harberg is ERC20, ERC20Permit { using Math for uint256; - // Total tax collected so far - uint256 public sumTaxCollected; - // Constant for UNI V3 1% fee tier pools - uint24 private constant FEE = uint24(10_000); // Minimum fraction of the total supply required for staking to prevent fragmentation of staking positions uint256 private constant MIN_STAKE_FRACTION = 3000; - // Maximum fraction of the total supply allowed for staking to manage gas costs - uint256 private constant MAX_STAKE_FRACTION = 100000; - // Period offset for TWAB calculations - uint256 private immutable PERIOD_OFFSET; - // Minimum period length for TWAB observations - uint256 private immutable PERIOD_LENGTH; - - // Immutable reference to the TWAB controller - TwabController private immutable twabController; // Address of the liquidity manager address private liquidityManager; // Address of the staking pool address private stakingPool; - // Address of the liquidity pool - address private liquidityPool; - // Address of the tax pool - address public constant TAX_POOL = address(2); // Previous total supply for staking calculations uint256 public previousTotalSupply; - // Minimum fraction of the total supply required for staking - uint256 public minStakeSupplyFraction; - - // Structure to hold UBI title information for each account - struct UbiTitle { - uint256 sumTaxCollected; - uint256 time; - } - - // Mapping to store UBI titles for each account - mapping(address => UbiTitle) public ubiTitles; // Custom errors - error ZeroAddressInConstructor(); error ZeroAddressInSetter(); error AddressAlreadySet(); @@ -69,30 +35,10 @@ contract Harberg is ERC20, ERC20Permit { * @notice Constructor for the Harberg token * @param name_ The name of the token * @param symbol_ The symbol of the token - * @param twabController_ The TWAB controller contract */ - constructor(string memory name_, string memory symbol_, TwabController twabController_) + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) - ERC20Permit(name_) - { - if (address(0) == address(twabController_)) revert ZeroAddressInConstructor(); - twabController = twabController_; - PERIOD_OFFSET = twabController.PERIOD_OFFSET(); - PERIOD_LENGTH = twabController.PERIOD_LENGTH(); - minStakeSupplyFraction = MIN_STAKE_FRACTION; - } - - /** - * @notice Sets the address for the liquidityPool. Used once post-deployment to initialize the contract. - * @dev Should be called only once right after the contract deployment to set the liquidity pool address. - * Throws AddressAlreadySet if called more than once. - * @param liquidityPool_ The address of the liquidity pool. - */ - function setLiquidityPool(address liquidityPool_) external { - if (address(0) == liquidityPool_) revert ZeroAddressInSetter(); - if (liquidityPool != address(0)) revert AddressAlreadySet(); - liquidityPool = liquidityPool_; - } + ERC20Permit(name_){} /** * @notice Sets the address for the liquidityManager. Used once post-deployment to initialize the contract. @@ -122,8 +68,8 @@ contract Harberg is ERC20, ERC20Permit { * @notice Returns the addresses of the periphery contracts * @return The addresses of the TWAB controller, liquidity manager, staking pool, and liquidity pool */ - function peripheryContracts() external view returns (address, address, address, address) { - return (address(twabController), liquidityManager, stakingPool, liquidityPool); + function peripheryContracts() external view returns (address, address) { + return (liquidityManager, stakingPool); } /** @@ -131,7 +77,7 @@ contract Harberg is ERC20, ERC20Permit { * @return The minimum stake amount */ function minStake() external view returns (uint256) { - return previousTotalSupply / minStakeSupplyFraction; + return previousTotalSupply / MIN_STAKE_FRACTION; } /** @@ -141,9 +87,17 @@ contract Harberg is ERC20, ERC20Permit { * @param _amount The number of tokens to mint. */ function mint(uint256 _amount) external onlyLiquidityManager { - _mint(address(liquidityManager), _amount); + if (_amount > 0) { + // make sure staking pool grows proportional to economy + uint256 stakingPoolBalance = balanceOf(stakingPool); + if (stakingPoolBalance > 0) { + uint256 newStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance); + _mint(stakingPool, newStake); + } + _mint(address(liquidityManager), _amount); + } if (previousTotalSupply == 0) { - previousTotalSupply = twabController.totalSupply(address(this)); + previousTotalSupply = totalSupply(); } } @@ -154,7 +108,15 @@ contract Harberg is ERC20, ERC20Permit { * @param _amount The number of tokens to burn. */ function burn(uint256 _amount) external onlyLiquidityManager { - _burn(address(liquidityManager), _amount); + if (_amount > 0) { + // shrink staking pool proportional to economy + uint256 stakingPoolBalance = balanceOf(stakingPool); + if (stakingPoolBalance > 0) { + uint256 excessStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance); + _burn(stakingPool, excessStake); + } + _burn(address(liquidityManager), _amount); + } } /** @@ -165,32 +127,6 @@ contract Harberg is ERC20, ERC20Permit { previousTotalSupply = _ts; } - /** - * @notice Sets the minimum stake supply fraction - * @param _mssf The minimum stake supply fraction value - */ - function setMinStakeSupplyFraction(uint256 _mssf) external onlyLiquidityManager { - require(_mssf >= MIN_STAKE_FRACTION, "minStakeSupplyFraction below allowed min"); - require(_mssf <= MAX_STAKE_FRACTION, "minStakeSupplyFraction above allow max"); - minStakeSupplyFraction = _mssf; - } - - /* ============ Public ERC20 Overrides ============ */ - - /** - * @inheritdoc ERC20 - */ - function balanceOf(address _account) public view override(ERC20) returns (uint256) { - return twabController.balanceOf(address(this), _account); - } - - /** - * @inheritdoc ERC20 - */ - function totalSupply() public view override(ERC20) returns (uint256) { - return twabController.totalSupply(address(this)); - } - /** * @notice Returns the outstanding supply, excluding the balances of the liquidity pool and liquidity manager * @return The outstanding supply @@ -199,144 +135,4 @@ contract Harberg is ERC20, ERC20Permit { return totalSupply() - balanceOf(liquidityManager); } - /* ============ Internal ERC20 Overrides ============ */ - - /** - * @notice Mints tokens to `_receiver` and increases the total supply. - * @dev Emits a {Transfer} event with `from` set to the zero address. - * @dev `receiver` cannot be the zero address. - * @param receiver Address that will receive the minted tokens - * @param amount Tokens to mint - */ - function _mint(address receiver, uint256 amount) internal override { - if (amount > 0) { - // make sure staking pool grows proportional to economy - uint256 stakingPoolBalance = balanceOf(stakingPool); - if (stakingPoolBalance > 0) { - uint256 newStake = stakingPoolBalance * amount / (totalSupply() - stakingPoolBalance); - twabController.mint(stakingPool, SafeCast.toUint96(newStake)); - emit Transfer(address(0), stakingPool, newStake); - } - twabController.mint(receiver, SafeCast.toUint96(amount)); - emit Transfer(address(0), receiver, amount); - if (ubiTitles[receiver].time == 0 && amount > 0) { - // new account, start UBI title - ubiTitles[receiver].sumTaxCollected = sumTaxCollected; - ubiTitles[receiver].time = block.timestamp; - } - } - } - - /** - * @notice Burns tokens from `_owner` and decreases the total supply. - * @dev Emits a {Transfer} event with `to` set to the zero address. - * @dev `_owner` must have at least `_amount` tokens. - * @param owner Address that will have tokens burnt - * @param amount Tokens to burn - */ - function _burn(address owner, uint256 amount) internal override { - if (amount > 0) { - // shrink staking pool proportional to economy - uint256 stakingPoolBalance = balanceOf(stakingPool); - if (stakingPoolBalance > 0) { - uint256 excessStake = stakingPoolBalance * amount / (totalSupply() - stakingPoolBalance); - twabController.burn(stakingPool, SafeCast.toUint96(excessStake)); - emit Transfer(stakingPool, address(0), excessStake); - } - twabController.burn(owner, SafeCast.toUint96(amount)); - emit Transfer(owner, address(0), amount); - } - } - - /** - * @notice Transfers tokens from one account to another. - * @dev Emits a {Transfer} event. - * @dev `_from` cannot be the zero address. - * @dev `_to` cannot be the zero address. - * @dev `_from` must have a balance of at least `_amount`. - * @param _from Address to transfer from - * @param _to Address to transfer to - * @param _amount The amount of tokens to transfer - */ - function _transfer(address _from, address _to, uint256 _amount) internal override { - if (_to == TAX_POOL) { - unchecked { - sumTaxCollected += _amount; - } - } else if (ubiTitles[_to].time == 0 && _amount > 0) { - // new account, start UBI title - ubiTitles[_to].sumTaxCollected = sumTaxCollected; - ubiTitles[_to].time = block.timestamp; - } - twabController.transfer(_from, _to, SafeCast.toUint96(_amount)); - emit Transfer(_from, _to, _amount); - } - - /* ============ UBI stuff ============ */ - - /** - * @notice Calculates the UBI due to an account based on time-weighted average balances. - * @dev Uses historic TWAB data to determine an account's proportionate share of collected taxes since last claim. - * @param _account The account whose UBI is being calculated. - * @param lastTaxClaimed The timestamp of the last UBI claim. - * @param _sumTaxCollected The tax collected up to the last claim. - * @return amountDue The amount of UBI due to the account. - * @return lastPeriodEndAt The timestamp marking the end of the last period considered for UBI calculation. - */ - 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), liquidityPool, lastTaxClaimed, lastPeriodEndAt); - uint256 taxTwab = twabController.getTwabBetween(address(this), TAX_POOL, lastTaxClaimed, lastPeriodEndAt); - uint256 totalSupplyTwab = twabController.getTotalSupplyTwabBetween(address(this), lastTaxClaimed, lastPeriodEndAt); - - //uint256 taxCollectedSinceLastClaim = sumTaxCollected - _sumTaxCollected; - uint256 taxCollectedSinceLastClaim; - if (sumTaxCollected >= _sumTaxCollected) { - taxCollectedSinceLastClaim = sumTaxCollected - _sumTaxCollected; - } else { - // Handle the wrap-around case - taxCollectedSinceLastClaim = type(uint256).max - _sumTaxCollected + sumTaxCollected + 1; - } - amountDue = taxCollectedSinceLastClaim.mulDiv(accountTwab, (totalSupplyTwab - stakeTwab - poolTwab - taxTwab), Math.Rounding.Down); - } - - /** - * @notice Calculates the UBI due to an account based on time-weighted average balances. - * @dev Uses historic TWAB data to determine an account's proportionate share of collected taxes since last claim. - * @param _account The account whose UBI is being calculated. - * @return amountDue The amount of UBI due to the account. - * @return lastPeriodEndAt The timestamp marking the end of the last period considered for UBI calculation. - */ - function getUbiDue(address _account) public view returns (uint256 amountDue, uint256 lastPeriodEndAt) { - UbiTitle storage lastUbiTitle = ubiTitles[_account]; - return ubiDue(_account, lastUbiTitle.time, lastUbiTitle.sumTaxCollected); - } - - /** - * @notice Claims the calculated UBI amount for the caller. - * @dev Transfers the due UBI from the tax pool to the account, updating the UBI title. - * Emits Transfer event on successful transfer. - * @param _account The account claiming the UBI. - * @return ubiAmountDue The amount of UBI claimed. - */ - function claimUbi(address _account) external returns (uint256 ubiAmountDue) { - UbiTitle storage lastUbiTitle = ubiTitles[_account]; - uint256 lastPeriodEndAt; - (ubiAmountDue, lastPeriodEndAt) = ubiDue(_account, lastUbiTitle.time, lastUbiTitle.sumTaxCollected); - - if (ubiAmountDue > 0) { - - ubiTitles[_account].sumTaxCollected = sumTaxCollected; - ubiTitles[_account].time = lastPeriodEndAt; - twabController.transfer(TAX_POOL, _account, SafeCast.toUint96(ubiAmountDue)); - emit Transfer(TAX_POOL, _account, ubiAmountDue); - } else { - revert("No UBI to claim."); - } - } } diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 2c4732b..1654490 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -94,7 +94,7 @@ contract LiquidityManager { pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); harb = Harberg(_harb); token0isWeth = _WETH9 < _harb; - sentimenter = Sentimenter(_sentimenter); + sentimenter = Sentimenter(_sentimenter); } /// @notice Callback function that Uniswap V3 calls for liquidity actions requiring minting or burning of tokens. @@ -125,10 +125,6 @@ contract LiquidityManager { feeDestination = feeDestination_; } - function setMinStakeSupplyFraction(uint256 mssf_) external onlyFeeDestination { - harb.setMinStakeSupplyFraction(mssf_); - } - function setRecenterAccess(address addr) external onlyFeeDestination { recenterAccess = addr; } @@ -433,13 +429,15 @@ contract LiquidityManager { /// @notice Adjusts liquidity positions in response to an increase or decrease in the Harberg token's price. /// @dev This function should be called when significant price movement is detected. It recalibrates the liquidity ranges to align with the new market conditions. function recenter() external returns (bool isUp, uint256 sentiment) { - if (recenterAccess != address(0)) { - require(msg.sender == recenterAccess, "access denied"); - } // Fetch the current tick from the Uniswap V3 pool (, int24 currentTick, , , , , ) = pool.slot0(); - // check slippage with oracle - require(_isPriceStable(currentTick), "price deviated from oracle"); + + if (recenterAccess != address(0)) { + require(msg.sender == recenterAccess, "access denied"); + } else { + // check slippage with oracle + require(_isPriceStable(currentTick), "price deviated from oracle"); + } isUp = false; // check how price moved diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index c0da882..d6fe27a 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -64,7 +64,7 @@ contract Stake { } Harberg private immutable harberg; - address private immutable taxPool; + address private immutable taxReceiver; uint256 public immutable totalSupply; uint256 public outstandingStake; @@ -78,10 +78,10 @@ contract Stake { /// @notice Initializes the stake contract with references to the Harberg contract and sets the initial position ID. /// @param _harberg Address of the Harberg contract which this Stake contract interacts with. /// @dev Sets up the total supply based on the decimals of the Harberg token plus a fixed offset. - constructor(address _harberg) { + constructor(address _harberg, address _taxReceiver) { harberg = Harberg(_harberg); + taxReceiver = _taxReceiver; totalSupply = 10 ** (harberg.decimals() + DECIMAL_OFFSET); - taxPool = Harberg(_harberg).TAX_POOL(); // start counting somewhere nextPositionId = 654321; // Initialize totalSharesAtTaxRate array @@ -125,7 +125,7 @@ contract Stake { delete pos.creationTime; delete pos.share; } - SafeERC20.safeTransfer(harberg, taxPool, taxAmountDue); + SafeERC20.safeTransfer(harberg, taxReceiver, taxAmountDue); } /// @dev Internal function to close a staking position, transferring the remaining Harberg tokens back to the owner after tax payment. diff --git a/onchain/test/Harberg.t.sol b/onchain/test/Harberg.t.sol index ada211d..c0111f5 100644 --- a/onchain/test/Harberg.t.sol +++ b/onchain/test/Harberg.t.sol @@ -3,25 +3,19 @@ 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 {IERC20} from "@openzeppelin/token/ERC20/IERC20.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(); + harberg = new Harberg("HARB", "HARB"); stakingPool = makeAddr("stakingPool"); harberg.setStakingPool(stakingPool); - liquidityPool = makeAddr("liquidityPool"); - harberg.setLiquidityPool(liquidityPool); liquidityManager = makeAddr("liquidityManager"); harberg.setLiquidityManager(liquidityManager); } @@ -46,11 +40,9 @@ contract HarbergTest is Test { 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)); + (address _lm, address _sp) = harberg.peripheryContracts(); assertEq(_lm, liquidityManager); assertEq(_sp, stakingPool); - assertEq(_lp, liquidityPool); } function testMintWithEmptyStakingPool() public { @@ -134,17 +126,20 @@ contract HarbergTest is Test { uint256 initialTotalSupply = harberg.totalSupply(); uint256 initialStakingPoolBalance = harberg.balanceOf(stakingPool); - mintAmount = bound(mintAmount, 0, 500 * 1e18); + mintAmount = bound(mintAmount, 1, 500 * 1e18); uint256 expectedNewStake = initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance); + // Expect Transfer events + vm.expectEmit(true, true, true, true, address(harberg)); + emit IERC20.Transfer(address(0), address(liquidityManager), mintAmount); + 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."); + assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply did not match expected after mint."); } // Fuzz test for burn function with varying stake amounts @@ -172,284 +167,7 @@ contract HarbergTest is Test { 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."); + assertEq(harberg.totalSupply(), expectedTotalSupply, "Total supply did not match expected 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."); - } - } diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index 698f553..d9163c0 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -5,7 +5,6 @@ import "forge-std/Test.sol"; import "@aperture/uni-v3-lib/TickMath.sol"; import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import {WETH} from "solmate/tokens/WETH.sol"; -import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; import {PoolAddress, PoolKey} from "@aperture/uni-v3-lib/PoolAddress.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; @@ -60,8 +59,6 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { function setUpCustomToken0(bool token0shouldBeWeth) public { factory = UniswapHelpers.deployUniswapFactory(); - TwabController tc = new TwabController(60 * 60 * 24, uint32(block.timestamp)); - bool setupComplete = false; uint retryCount = 0; while (!setupComplete && retryCount < 5) { @@ -71,7 +68,7 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { } weth = IWETH9(address(new WETH())); - harberg = new Harberg("HARB", "HARB", tc); + harberg = new Harberg("HARB", "HARB"); // Check if the setup meets the required condition if (token0shouldBeWeth == address(weth) < address(harberg)) { @@ -90,7 +87,7 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { token0isWeth = address(weth) < address(harberg); pool.initializePoolFor1Cent(token0isWeth); - stake = new Stake(address(harberg)); + stake = new Stake(address(harberg), feeDestination); harberg.setStakingPool(address(stake)); Sentimenter senti = Sentimenter(address(new MockSentimenter())); senti.initialize(address(harberg), address(stake)); @@ -98,7 +95,6 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { lm.setFeeDestination(feeDestination); vm.prank(feeDestination); harberg.setLiquidityManager(address(lm)); - harberg.setLiquidityPool(address(pool)); vm.deal(address(lm), 10 ether); initializePositionsCSV(); // Set up the CSV header } diff --git a/onchain/test/Sentimenter.t.sol b/onchain/test/Sentimenter.t.sol index 2470157..0c61458 100644 --- a/onchain/test/Sentimenter.t.sol +++ b/onchain/test/Sentimenter.t.sol @@ -3,7 +3,6 @@ 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"; import {TooMuchSnatch, Stake} from "../src/Stake.sol"; import "../src/Sentimenter.sol"; @@ -11,29 +10,22 @@ import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; import {MockSentimenter} from "./mocks/MockSentimenter.sol"; contract SentimenterTest is Test { - TwabController tc; Harberg harberg; Stake stake; Sentimenter sentimenter; - 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(); - stake = new Stake(address(harberg)); + harberg = new Harberg("HARB", "HARB"); + stake = new Stake(address(harberg), makeAddr("taxRecipient")); harberg.setStakingPool(address(stake)); - liquidityPool = makeAddr("liquidityPool"); - harberg.setLiquidityPool(liquidityPool); liquidityManager = makeAddr("liquidityManager"); harberg.setLiquidityManager(liquidityManager); - // deploy upgradeable tuner contract - Sentimenter _sentimenter = new Sentimenter(); - bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(harberg),address(stake)); - ERC1967Proxy proxy = new ERC1967Proxy(address(_sentimenter), params); - sentimenter = Sentimenter(address(proxy)); + // deploy upgradeable tuner contract + Sentimenter _sentimenter = new Sentimenter(); + bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(harberg),address(stake)); + ERC1967Proxy proxy = new ERC1967Proxy(address(_sentimenter), params); + sentimenter = Sentimenter(address(proxy)); } function doSnatch(address staker, uint256 amount, uint32 taxRate) private returns (uint256 positionId) { diff --git a/onchain/test/Simulations.t.sol b/onchain/test/Simulations.t.sol index d6b4eef..8a61675 100644 --- a/onchain/test/Simulations.t.sol +++ b/onchain/test/Simulations.t.sol @@ -6,7 +6,6 @@ import "@aperture/uni-v3-lib/TickMath.sol"; import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import "../src/interfaces/IWETH9.sol"; import {WETH} from "solmate/tokens/WETH.sol"; -import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; import {PoolAddress, PoolKey} from "@aperture/uni-v3-lib/PoolAddress.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; @@ -18,11 +17,16 @@ import {UniswapTestBase} from "./helpers/UniswapTestBase.sol"; import {CSVHelper} from "./helpers/CSVHelper.sol"; import {CSVManager} from "./helpers/CSVManager.sol"; import "../src/Sentimenter.sol"; +import "./mocks/MockSentimenter.sol"; -address constant TAX_POOL = address(2); // default fee of 1% uint24 constant FEE = uint24(10_000); +// Dummy.sol +contract Dummy { + // This contract can be empty as it is only used to affect the nonce +} + contract SimulationsTest is UniswapTestBase, CSVManager { using UniswapHelpers for IUniswapV3Pool; using CSVHelper for *; @@ -71,27 +75,33 @@ contract SimulationsTest is UniswapTestBase, CSVManager { Action[] txns; } + + // Utility to deploy dummy contracts + function deployDummies(uint count) internal { + for (uint i = 0; i < count; i++) { + new Dummy(); // Just increment the nonce + } + } + function setUp() public { factory = UniswapHelpers.deployUniswapFactory(); weth = IWETH9(address(new WETH())); - TwabController tc = new TwabController(60 * 60, uint32(block.timestamp)); - harberg = new Harberg("Harberg", "HRB", tc); + harberg = new Harberg("Harberg", "HRB"); pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE)); poolKey = PoolAddress.getPoolKey(address(weth), address(harberg), FEE); token0isWeth = address(weth) < address(harberg); //pool.initializePoolFor1Cent(token0isWeth); - stakingPool = new Stake(address(harberg)); + stakingPool = new Stake(address(harberg), feeDestination); harberg.setStakingPool(address(stakingPool)); - Sentimenter senti = new Sentimenter(); - senti.initialize(address(harberg), address(stakingPool)); + Sentimenter senti = new Sentimenter(); + senti.initialize(address(harberg), address(stakingPool)); lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(senti)); lm.setFeeDestination(feeDestination); vm.prank(feeDestination); harberg.setLiquidityManager(address(lm)); - harberg.setLiquidityPool(address(pool)); vm.deal(address(lm), 1 ether); timeOnRecenter = block.timestamp; initializeTimeSeriesCSV(); @@ -100,8 +110,6 @@ contract SimulationsTest is UniswapTestBase, CSVManager { function setUpCustomToken0(bool token0shouldBeWeth) public { factory = UniswapHelpers.deployUniswapFactory(); - TwabController tc = new TwabController(60 * 60 * 24, uint32(block.timestamp)); - bool setupComplete = false; uint retryCount = 0; while (!setupComplete && retryCount < 5) { @@ -111,7 +119,7 @@ contract SimulationsTest is UniswapTestBase, CSVManager { } weth = IWETH9(address(new WETH())); - harberg = new Harberg("HARB", "HARB", tc); + harberg = new Harberg("HARB", "HARB"); // Check if the setup meets the required condition if (token0shouldBeWeth == address(weth) < address(harberg)) { @@ -128,15 +136,14 @@ contract SimulationsTest is UniswapTestBase, CSVManager { pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE)); token0isWeth = address(weth) < address(harberg); - stake = new Stake(address(harberg)); - harberg.setStakingPool(address(stake)); - Sentimenter senti = Sentimenter(address(new MockSentimenter())); - senti.initialize(address(harberg), address(stake)); + stakingPool = new Stake(address(harberg), feeDestination); + harberg.setStakingPool(address(stakingPool)); + Sentimenter senti = Sentimenter(address(new MockSentimenter())); + senti.initialize(address(harberg), address(stakingPool)); lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(senti)); lm.setFeeDestination(feeDestination); vm.prank(feeDestination); harberg.setLiquidityManager(address(lm)); - harberg.setLiquidityPool(address(pool)); vm.deal(address(lm), 10 ether); initializePositionsCSV(); // Set up the CSV header } @@ -215,8 +222,7 @@ contract SimulationsTest is UniswapTestBase, CSVManager { //(sentiment, avgTaxRate) = stakingPool.getSentiment(); newRow = string.concat(newRow, ",", CSVHelper.uintToStr(avgTaxRate), - ",", CSVHelper.uintToStr(sentiment), - ",", CSVHelper.uintToStr(harberg.sumTaxCollected() / 1e18) + ",", CSVHelper.uintToStr(sentiment) ); } else { newRow = string.concat(newRow, ", 0, 100, 95, 25, 0"); diff --git a/onchain/test/Stake.t.sol b/onchain/test/Stake.t.sol index 9dcfccc..dab9376 100644 --- a/onchain/test/Stake.t.sol +++ b/onchain/test/Stake.t.sol @@ -3,30 +3,23 @@ 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"; import {TooMuchSnatch, Stake} from "../src/Stake.sol"; contract StakeTest is Test { - TwabController tc; Harberg harberg; Stake stakingPool; address liquidityPool; address liquidityManager; - address taxPool; event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 harbergDeposit, uint256 share, uint32 taxRate); event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 harbergPayout); function setUp() public { - tc = new TwabController(60 * 60, uint32(block.timestamp)); - harberg = new Harberg("HARB", "HARB", tc); - taxPool = harberg.TAX_POOL(); - stakingPool = new Stake(address(harberg)); + harberg = new Harberg("HARB", "HARB"); + stakingPool = new Stake(address(harberg), makeAddr("taxRecipient")); harberg.setStakingPool(address(stakingPool)); - liquidityPool = makeAddr("liquidityPool"); - harberg.setLiquidityPool(liquidityPool); liquidityManager = makeAddr("liquidityManager"); harberg.setLiquidityManager(liquidityManager); }