got UBI to work

This commit is contained in:
JulesCrown 2024-03-14 12:40:57 +01:00
parent 06581a0b8d
commit b93664431f
7 changed files with 154 additions and 28 deletions

2
.gitignore vendored
View file

@ -16,4 +16,4 @@ docs/
.infura
.DS_Store
/onchain/lib/**/node-modules/
/onchain/lib/**/node-modules/

View file

@ -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));

View file

@ -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));
}
}
}

View file

@ -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");

View file

@ -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);

View file

@ -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");
}
}
}

View file

@ -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;
}