// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; 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 {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 * @notice This contract creates an ERC20 token with balances stored in a TwabController, * enabling time-weighted average balances for each holder. * @dev The TwabController limits all balances including total token supply to uint96 for * 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; uint256 immutable PERIOD_OFFSET; uint256 immutable PERIOD_LENGTH; IUniswapV3Pool immutable pool; /// @notice Address of the LiquidityManager contract that mints and burns supply address public liquidityManager; address public stakingPool; uint256 public sumTaxCollected; uint256 public previousTotalSupply; struct UbiTitle { uint256 sumTaxCollected; uint256 time; } mapping(address => UbiTitle) public ubiTitles; /* ============ Errors ============ */ /// @notice Thrown if the some address is unexpectedly the zero address. error ZeroAddressInConstructor(); error ZeroAddressInSetter(); error AddressAlreadySet(); event UbiClaimed(address indexed owner, uint256 ubiAmount); /// @dev Function modifier to ensure that the caller is the liquidityManager modifier onlyLiquidityManager() { require(msg.sender == address(liquidityManager), "Harb/only-lm"); _; } /* ============ Constructor ============ */ /** * @notice TwabERC20 Constructor * @param name_ The name of the token * @param symbol_ The token symbol */ 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_; PERIOD_OFFSET = twabController.PERIOD_OFFSET(); PERIOD_LENGTH = twabController.PERIOD_LENGTH(); PoolKey memory poolKey = PoolAddress.getPoolKey(_WETH9, address(this), FEE); pool = IUniswapV3Pool(PoolAddress.computeAddress(_factory, poolKey)); } function setLiquidityManager(address liquidityManager_) external { if (address(0) == liquidityManager_) revert ZeroAddressInSetter(); if (liquidityManager != address(0)) revert AddressAlreadySet(); liquidityManager = liquidityManager_; } function setStakingPool(address stakingPool_) external { if (address(0) == stakingPool_) revert ZeroAddressInSetter(); if (stakingPool != address(0)) revert AddressAlreadySet(); stakingPool = stakingPool_; } /* ============ External Functions ============ */ /// @notice Allows the liquidityManager to mint tokens for itself /// @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(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(address(liquidityManager), _amount); } function setPreviousTotalSupply(uint256 _ts) external onlyLiquidityManager { previousTotalSupply = _ts; } /* ============ 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)); } /* ============ 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 { // TODO: limit supply to 2^96? // 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 Destroys tokens from `_owner` and reduces the total supply. * @dev Emits a {Transfer} event with `to` set to the zero address. * @dev `_owner` cannot be the zero address. * @dev `_owner` must have at least `_amount` tokens. * @param _owner The owner of the tokens * @param _amount The amount of tokens to burn */ function _burn(address _owner, uint256 _amount) internal override { // 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) { 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 ============ */ function getUbiDue(address _account) public view returns (uint256 amountDue, uint256 lastPeriodEndAt) { UbiTitle storage lastUbiTitle = ubiTitles[_account]; return ubiDue(_account, lastUbiTitle.time, lastUbiTitle.sumTaxCollected); } 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 - taxTwab), Math.Rounding.Down); } 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 UbiClaimed(_account, ubiAmountDue); } else { revert("No UBI to claim."); } } }