got UBI to work
This commit is contained in:
parent
06581a0b8d
commit
b93664431f
7 changed files with 154 additions and 28 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -16,4 +16,4 @@ docs/
|
|||
.infura
|
||||
.DS_Store
|
||||
|
||||
/onchain/lib/**/node-modules/
|
||||
/onchain/lib/**/node-modules/
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
onchain/test/interfaces/IWETH9.sol
Normal file
13
onchain/test/interfaces/IWETH9.sol
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue