237 lines
10 KiB
Solidity
237 lines
10 KiB
Solidity
// 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.");
|
|
}
|
|
}
|
|
|
|
}
|