From b93664431ff61238d6abfb5bc4eed730488121de Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Thu, 14 Mar 2024 12:40:57 +0100 Subject: [PATCH] got UBI to work --- .gitignore | 2 +- onchain/script/Deploy.sol | 5 +- onchain/src/Harb.sol | 75 ++++++++++++++++++++++++++---- onchain/src/LiquidityManager.sol | 18 ++++--- onchain/src/Stake.sol | 2 +- onchain/test/Harb.t.sol | 67 ++++++++++++++++++++++---- onchain/test/interfaces/IWETH9.sol | 13 ++++++ 7 files changed, 154 insertions(+), 28 deletions(-) create mode 100644 onchain/test/interfaces/IWETH9.sol diff --git a/.gitignore b/.gitignore index a44218c..1a37863 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ docs/ .infura .DS_Store -/onchain/lib/**/node-modules/ \ No newline at end of file +/onchain/lib/**/node-modules/ diff --git a/onchain/script/Deploy.sol b/onchain/script/Deploy.sol index 3909685..e4a7fc8 100644 --- a/onchain/script/Deploy.sol +++ b/onchain/script/Deploy.sol @@ -5,6 +5,9 @@ import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; import "../src/Harb.sol"; import "../src/Stake.sol"; +address constant WETH = 0xb16F35c0Ae2912430DAc15764477E179D9B9EbEa; //Sepolia +address constant V3_FACTORY = 0x0227628f3F023bb0B980b67D528571c95c6DaC1c; //Sepolia + contract SepoliaScript is Script { function setUp() public {} @@ -14,7 +17,7 @@ contract SepoliaScript is Script { vm.startBroadcast(privateKey); TwabController tc = new TwabController(60 * 60 * 24, uint32(block.timestamp)); - Harb harb = new Harb("Harberger Tax", "HARB", tc); + Harb harb = new Harb("Harberger Tax", "HARB", V3_FACTORY, WETH, tc); Stake stake = new Stake(address(harb)); harb.setStakingPool(address(stake)); diff --git a/onchain/src/Harb.sol b/onchain/src/Harb.sol index 6e6a4b0..7928ca3 100644 --- a/onchain/src/Harb.sol +++ b/onchain/src/Harb.sol @@ -6,7 +6,11 @@ import {ERC20, IERC20, IERC20Metadata} from "@openzeppelin/token/ERC20/ERC20.sol import {ERC20Permit, IERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; import {SafeCast} from "@openzeppelin/utils/math/SafeCast.sol"; import {IStake} from "./interfaces/IStake.sol"; +import {LiquidityManager} from "./LiquidityManager.sol"; import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; +import {Math} from "@openzeppelin/utils/math/Math.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; +import "@aperture/uni-v3-lib/PoolAddress.sol"; /** * @title TWAB ERC20 Token @@ -16,18 +20,31 @@ import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; * gas savings. Any mints that increase a balance past this limit will fail. */ contract Harb is ERC20, ERC20Permit { + using Math for uint256; + address public constant TAX_POOL = address(2); + uint24 constant FEE = uint24(10_000); /* ============ Public Variables ============ */ /// @notice Address of the TwabController used to keep track of balances. TwabController public immutable twabController; + IUniswapV3Pool immutable pool; /// @notice Address of the LiquidityManager contract that mints and burns supply address public liquidityManager; address public stakingPool; + uint256 public sumTaxCollected; + + struct UbiTitle { + uint256 sumTaxCollected; + uint256 time; + } + + mapping(address => UbiTitle) public ubiTitles; + /* ============ Errors ============ */ /// @notice Thrown if the some address is unexpectedly the zero address. @@ -35,7 +52,7 @@ contract Harb is ERC20, ERC20Permit { /// @dev Function modifier to ensure that the caller is the liquidityManager modifier onlyLiquidityManager() { - require(msg.sender == liquidityManager, "Harb/only-lm"); + require(msg.sender == address(liquidityManager), "Harb/only-lm"); _; } @@ -46,12 +63,14 @@ contract Harb is ERC20, ERC20Permit { * @param name_ The name of the token * @param symbol_ The token symbol */ - constructor(string memory name_, string memory symbol_, TwabController twabController_) + constructor(string memory name_, string memory symbol_, address _factory, address _WETH9, TwabController twabController_) ERC20(name_, symbol_) ERC20Permit(name_) { if (address(0) == address(twabController_)) revert ZeroAddressInConstructor(); twabController = twabController_; + PoolKey memory poolKey = PoolAddress.getPoolKey(_WETH9, address(this), FEE); + pool = IUniswapV3Pool(PoolAddress.computeAddress(_factory, poolKey)); } function setLiquidityManager(address liquidityManager_) external { @@ -72,25 +91,25 @@ contract Harb is ERC20, ERC20Permit { /// @dev May be overridden to provide more granular control over minting /// @param _amount Amount of tokens to mint function mint(uint256 _amount) external onlyLiquidityManager { - _mint(liquidityManager, _amount); + _mint(address(liquidityManager), _amount); } /// @notice Allows the liquidityManager to burn tokens from a its account /// @dev May be overridden to provide more granular control over burning /// @param _amount Amount of tokens to burn function burn(uint256 _amount) external onlyLiquidityManager { - _burn(liquidityManager, _amount); + _burn(address(liquidityManager), _amount); } /* ============ Public ERC20 Overrides ============ */ /// @inheritdoc ERC20 - function balanceOf(address _account) public view virtual override(ERC20) returns (uint256) { + function balanceOf(address _account) public view override(ERC20) returns (uint256) { return twabController.balanceOf(address(this), _account); } /// @inheritdoc ERC20 - function totalSupply() public view virtual override(ERC20) returns (uint256) { + function totalSupply() public view override(ERC20) returns (uint256) { return twabController.totalSupply(address(this)); } @@ -103,7 +122,7 @@ contract Harb is ERC20, ERC20Permit { * @param receiver Address that will receive the minted tokens * @param amount Tokens to mint */ - function _mint(address receiver, uint256 amount) internal virtual override { + function _mint(address receiver, uint256 amount) internal override { // make sure staking pool grows proportional to economy uint256 stakingPoolBalance = balanceOf(stakingPool); uint256 activeSupply = totalSupply() - stakingPoolBalance; @@ -112,7 +131,6 @@ contract Harb is ERC20, ERC20Permit { uint256 newStake = stakingPoolBalance * amount / (activeSupply + dormantStake); _mint(stakingPool, newStake); } - twabController.mint(receiver, SafeCast.toUint96(amount)); emit Transfer(address(0), receiver, amount); } @@ -125,7 +143,7 @@ contract Harb is ERC20, ERC20Permit { * @param _owner The owner of the tokens * @param _amount The amount of tokens to burn */ - function _burn(address _owner, uint256 _amount) internal virtual override { + function _burn(address _owner, uint256 _amount) internal override { // TODO twabController.burn(_owner, SafeCast.toUint96(_amount)); emit Transfer(_owner, address(0), _amount); @@ -141,8 +159,45 @@ contract Harb is ERC20, ERC20Permit { * @param _to Address to transfer to * @param _amount The amount of tokens to transfer */ - function _transfer(address _from, address _to, uint256 _amount) internal virtual override { + function _transfer(address _from, address _to, uint256 _amount) internal override { + if (_to == TAX_POOL) { + 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); } + + function getUbiDue(address _account) public view returns (uint256) { + UbiTitle storage lastUbiTitle = ubiTitles[_account]; + return ubiDue(_account, lastUbiTitle.time, lastUbiTitle.sumTaxCollected); + } + + function ubiDue(address _account, uint256 lastTaxClaimed, uint256 _sumTaxCollected) internal view returns (uint256) { + uint256 accountTwab = twabController.getTwabBetween(address(this), _account, lastTaxClaimed, block.timestamp); + uint256 stakeTwab = twabController.getTwabBetween(address(this), stakingPool, lastTaxClaimed, block.timestamp); + + + uint256 poolTwab = twabController.getTwabBetween(address(this), address(pool), lastTaxClaimed, block.timestamp); + uint256 totalSupplyTwab = twabController.getTotalSupplyTwabBetween(address(this), lastTaxClaimed, block.timestamp); + + uint256 taxCollectedSinceLastClaim = sumTaxCollected - _sumTaxCollected; + //return taxCollectedSinceLastClaim.mulDiv(accountTwab, (totalSupplyTwab - stakeTwab - poolTwab), Math.Rounding.Down); + return taxCollectedSinceLastClaim * accountTwab / (totalSupplyTwab - stakeTwab - poolTwab); + } + + function claimUbi(address _account) external { + UbiTitle storage lastUbiTitle = ubiTitles[_account]; + uint256 ubiAmountDue = ubiDue(_account, lastUbiTitle.time, lastUbiTitle.sumTaxCollected); + + if (ubiAmountDue > 0) { + ubiTitles[_account].sumTaxCollected = sumTaxCollected; + ubiTitles[_account].time = block.timestamp; + twabController.transfer(TAX_POOL, _account, SafeCast.toUint96(ubiAmountDue)); + } + } + } diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index b319740..5049a87 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -22,7 +22,9 @@ contract LiquidityManager { // the address of the Uniswap V3 factory address public immutable factory; // the address of WETH9 - address public immutable WETH9; + address immutable WETH9; + + IUniswapV3Pool public immutable pool; struct AddLiquidityParams { address token0; @@ -77,9 +79,11 @@ contract LiquidityManager { /// @param amount1 The amount of token1 that was accounted for the decrease in liquidity event DecreaseLiquidity(address indexed token, uint128 liquidity, uint256 amount0, uint256 amount1); - constructor(address _factory, address _WETH9) { + constructor(address _factory, address _WETH9, address harb) { factory = _factory; WETH9 = _WETH9; + PoolKey memory poolKey = PoolAddress.getPoolKey(_WETH9, harb, FEE); + pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); } function getToken(address token0, address token1) internal view returns (bool token0isWeth, address token) { @@ -93,6 +97,10 @@ contract LiquidityManager { return (uint128(feesOwed >> 128), uint128(feesOwed)); } + function getPool() public view returns (address) { + return address(pool); + } + function updateFeesOwed(bool token0isEth, address token, uint128 tokensOwed0, uint128 tokensOwed1) internal { (uint128 tokensOwed, uint128 ethOwed) = getFeesOwed(token); tokensOwed += token0isEth ? tokensOwed1 : tokensOwed0; @@ -126,8 +134,6 @@ contract LiquidityManager { /// @notice Add liquidity to an initialized pool function addLiquidity(AddLiquidityParams memory params) external checkDeadline(params.deadline) { - PoolKey memory poolKey = PoolAddress.getPoolKey(params.token0, params.token1, FEE); - IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); // compute the liquidity amount uint128 liquidity; @@ -143,6 +149,7 @@ contract LiquidityManager { (bool token0isWeth, address token) = getToken(params.token0, params.token1); { + PoolKey memory poolKey = PoolAddress.getPoolKey(params.token0, params.token1, FEE); (uint256 amount0, uint256 amount1) = pool.mint(address(this), params.tickLower, params.tickUpper, liquidity, abi.encode(poolKey)); @@ -202,9 +209,6 @@ contract LiquidityManager { uint128 positionLiquidity = position.liquidity; require(positionLiquidity >= params.liquidity); - PoolKey memory poolKey = PoolAddress.getPoolKey(params.token0, params.token1, FEE); - IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); - (amount0, amount1) = pool.burn(params.tickLower, params.tickUpper, params.liquidity); require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, "Price slippage check"); diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index ef15dbb..8395ba1 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -41,7 +41,7 @@ contract Stake is IStake { uint256 public outstandingStake; uint256 private lastTokenId; uint256 public minStake; - mapping(uint256 positionID => StakingPosition) public positions; + mapping(uint256 => StakingPosition) public positions; constructor(address _tokenContract) { tokenContract = IERC20Metadata(_tokenContract); diff --git a/onchain/test/Harb.t.sol b/onchain/test/Harb.t.sol index 38b35e2..456459f 100644 --- a/onchain/test/Harb.t.sol +++ b/onchain/test/Harb.t.sol @@ -4,19 +4,39 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "forge-std/console.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"; +import "./interfaces/IWETH9.sol"; import "../src/Harb.sol"; import "../src/Stake.sol"; +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; +address constant V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; +address constant TAX_POOL = address(2); +// default fee of 1% +uint24 constant FEE = uint24(10_000); + contract HarbTest is Test { - Harb public harb; - Stake public stake; + uint256 mainnetFork; + IWETH9 weth; + Harb harb; + IUniswapV3Factory factory; + Stake stake; + LiquidityManager liquidityManager; function setUp() public { + mainnetFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET"), 19432879); + vm.selectFork(mainnetFork); + weth = IWETH9(WETH); TwabController tc = new TwabController(60 * 60 * 24, uint32(block.timestamp)); - - harb = new Harb("HARB", "HARB", tc); + harb = new Harb("HARB", "HARB", V3_FACTORY, WETH, tc); + factory = IUniswapV3Factory(V3_FACTORY); + IUniswapV3Pool(factory.createPool(address(weth), address(harb), FEE)); stake = new Stake(address(harb)); harb.setStakingPool(address(stake)); + liquidityManager = new LiquidityManager(V3_FACTORY, WETH, address(harb)); + harb.setLiquidityManager(address(liquidityManager)); } function test_MintStakeUnstake(address account, uint256 amount) public { @@ -38,10 +58,25 @@ contract HarbTest is Test { assertEq(totalAfter, totalSupplyBefore + amount, "total supply should match"); assertEq(harb.balanceOf(account), balanceBefore + amount, "balance should match"); - // test stake + // test UBI title { + // prepare UBI title vm.prank(account); harb.mint(amount * 4); + address alice = makeAddr("alice"); + 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); @@ -51,22 +86,38 @@ contract HarbTest is Test { 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(0); 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 unstake { + // advance the time uint256 timeBefore = block.timestamp; vm.warp(timeBefore + 60 * 60 * 24 * 4); uint256 taxDue = stake.taxDue(0, 60 * 60 * 24 * 3); - console.logUint(taxDue); - console.log("tax due :%i", taxDue); + + uint256 sumTaxCollectedBefore = harb.sumTaxCollected(); vm.prank(account); stake.exitPosition(0); - assertApproxEqRel(harb.balanceOf(account), amount * 5 - taxDue, 1e14, "balance should match"); + 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, "Not UBI paid"); + assertEq(balanceBefore + ubiDue, harb.balanceOf(account), "ubi should match"); } } } diff --git a/onchain/test/interfaces/IWETH9.sol b/onchain/test/interfaces/IWETH9.sol new file mode 100644 index 0000000..cb52a3c --- /dev/null +++ b/onchain/test/interfaces/IWETH9.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.13; + +import '@openzeppelin/token/ERC20/IERC20.sol'; + +/// @title Interface for WETH9 +interface IWETH9 is IERC20 { + /// @notice Deposit ether to get wrapped ether + function deposit() external payable; + + /// @notice Withdraw wrapped ether to get ether + function withdraw(uint256) external; +}