added comments
This commit is contained in:
parent
ba298cfd50
commit
236469f023
6 changed files with 174 additions and 318 deletions
|
|
@ -135,261 +135,16 @@ open features:
|
|||
- liquidation bot
|
||||
- shift/slide bot
|
||||
- ubi claim bot
|
||||
- deployment on L2
|
||||
- make minStake a gov param
|
||||
- prep for audit
|
||||
- clean up TODOs
|
||||
- clean up magic numbers
|
||||
- coverage
|
||||
- overflows
|
||||
- reentry
|
||||
- definition of severity of finding
|
||||
|
||||
## old lp
|
||||
|
||||
```
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@uniswap-v3-periphery/libraries/PositionKey.sol";
|
||||
import "@uniswap-v3-core/libraries/FixedPoint128.sol";
|
||||
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import "@aperture/uni-v3-lib/TickMath.sol";
|
||||
import "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
||||
import "@aperture/uni-v3-lib/PoolAddress.sol";
|
||||
import "@aperture/uni-v3-lib/CallbackValidation.sol";
|
||||
import "@openzeppelin/token/ERC20/IERC20.sol";
|
||||
|
||||
/**
|
||||
* @title LiquidityManager - A contract that supports the harb ecosystem. It
|
||||
* protects the communities liquidity while allowing a manager role to
|
||||
* take strategic liqudity positions.
|
||||
*/
|
||||
contract LiquidityManager {
|
||||
// default fee of 1%
|
||||
uint24 constant FEE = uint24(10_000);
|
||||
|
||||
// the address of the Uniswap V3 factory
|
||||
address immutable factory;
|
||||
IWETH9 immutable weth;
|
||||
Harb immutable harb;
|
||||
IUniswapV3Pool immutable pool;
|
||||
PoolKey immutable poolKey;
|
||||
bool immutable token0isWeth;
|
||||
|
||||
|
||||
struct TokenPosition {
|
||||
// the liquidity of the position
|
||||
uint128 liquidity;
|
||||
uint128 ethOwed;
|
||||
// the fee growth of the aggregate position as of the last action on the individual position
|
||||
uint256 feeGrowthInside0LastX128;
|
||||
uint256 feeGrowthInside1LastX128;
|
||||
}
|
||||
|
||||
/// @dev The token ID position data
|
||||
mapping(bytes26 => TokenPosition) private _positions;
|
||||
|
||||
modifier checkDeadline(uint256 deadline) {
|
||||
require(block.timestamp <= deadline, "Transaction too old");
|
||||
_;
|
||||
}
|
||||
|
||||
/// @notice Emitted when liquidity is increased for a position
|
||||
/// @param liquidity The amount by which liquidity for the NFT position was increased
|
||||
/// @param amount0 The amount of token0 that was paid for the increase in liquidity
|
||||
/// @param amount1 The amount of token1 that was paid for the increase in liquidity
|
||||
event IncreaseLiquidity(int24 indexed tickLower, int24 indexed tickUpper, uint128 liquidity, uint256 amount0, uint256 amount1);
|
||||
/// @notice Emitted when liquidity is decreased for a position
|
||||
/// @param liquidity The amount by which liquidity for the NFT position was decreased
|
||||
/// @param ethReceived The amount of WETH that was accounted for the decrease in liquidity
|
||||
event PositionLiquidated(int24 indexed tickLower, int24 indexed tickUpper, uint128 liquidity, uint256 ethReceived);
|
||||
|
||||
constructor(address _factory, address _WETH9, address _harb) {
|
||||
factory = _factory;
|
||||
weth = IWETH9(_WETH9);
|
||||
poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE);
|
||||
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
|
||||
harb = Harb(_harb);
|
||||
token0isWeth = _WETH9 < _harb;
|
||||
}
|
||||
|
||||
function posKey(int24 tickLower, int24 tickUpper) internal pure returns (bytes6 _posKey) {
|
||||
bytes memory _posKeyBytes = abi.encodePacked(tickLower, tickUpper);
|
||||
assembly {
|
||||
_posKey := mload(add(_posKeyBytes, 6))
|
||||
}
|
||||
}
|
||||
|
||||
function positions(int24 tickLower, int24 tickUpper)
|
||||
external
|
||||
view
|
||||
returns (uint128 liquidity, uint128 ethOwed, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128)
|
||||
{
|
||||
TokenPosition memory position = _positions[posKey(tickLower, tickUpper)];
|
||||
return (position.liquidity, position.ethOwed, position.feeGrowthInside0LastX128, position.feeGrowthInside1LastX128);
|
||||
}
|
||||
|
||||
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata data) external {
|
||||
CallbackValidation.verifyCallback(factory, poolKey);
|
||||
|
||||
if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed);
|
||||
if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed);
|
||||
}
|
||||
|
||||
|
||||
/// @notice Add liquidity to an initialized pool
|
||||
// TODO: use uint256 amount0Min; uint256 amount1Min; if addLiquidity is called directly
|
||||
function addLiquidity(int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 deadline) internal checkDeadline(deadline) {
|
||||
|
||||
|
||||
(uint256 amount0, uint256 amount1) = pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey));
|
||||
// If addLiquidity is only called after other pool operations that have checked slippage, this here is not needed
|
||||
//require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, "Price slippage check");
|
||||
|
||||
|
||||
// read position and start tracking in storage
|
||||
bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper);
|
||||
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
|
||||
TokenPosition storage position = _positions[posKey(token, tickLower, tickUpper)];
|
||||
if (liquidity == 0) {
|
||||
// create entry
|
||||
position = TokenPosition({
|
||||
liquidity: liquidity,
|
||||
ethOwed: 0,
|
||||
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
|
||||
feeGrowthInside1LastX128: feeGrowthInside1LastX128
|
||||
});
|
||||
} else {
|
||||
position.ethOwed += FullMath.mulDiv(
|
||||
(token0isWeth) ? feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128 : feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
|
||||
position.liquidity,
|
||||
FixedPoint128.Q128
|
||||
);
|
||||
position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
|
||||
position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
|
||||
position.liquidity += liquidity;
|
||||
}
|
||||
emit IncreaseLiquidity(tickLower, tickUpper, liquidity, amount0, amount1);
|
||||
}
|
||||
|
||||
function liquidatePosition(int24 tickLower, int24 tickUpper, uint256 amount0Min, uint256 amount1Min)
|
||||
internal
|
||||
returns (uint256 ethReceived, uint256 liquidity)
|
||||
{
|
||||
// load position
|
||||
TokenPosition storage position = _positions[posKey(tickLower, tickUpper)];
|
||||
|
||||
// burn and check slippage
|
||||
uint256 liquidity = position.liquidity;
|
||||
(uint256 amount0, uint256 amount1) = pool.burn(tickLower, tickUpper, liquidity);
|
||||
require(amount0 >= amount0Min && amount1 >= amount1Min, "Price slippage check");
|
||||
// TODO: send harb fees or burn?
|
||||
//harb.burn(token0isWeth ? amount1 : amount0);
|
||||
|
||||
// calculate and transfer fees
|
||||
bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper);
|
||||
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
|
||||
uint256 ethOwed = position.ethOwed;
|
||||
ethOwed +=
|
||||
FullMath.mulDiv(
|
||||
(token0isWeth) ? feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128 : feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
|
||||
liquidity,
|
||||
FixedPoint128.Q128
|
||||
);
|
||||
weth.withdraw(ethOwed);
|
||||
(bool sent, ) = feeRecipient.call{value: ethOwed}("");
|
||||
require(sent, "Failed to send Ether");
|
||||
|
||||
// event, cleanup and return
|
||||
ethReceived = token0isWeth ? amount0 - ethOwed : amount1 - ethOwed;
|
||||
emit PositionLiquidated(tickLower, tickUpper, liquidity, ethReceived);
|
||||
delete position.liquidity;
|
||||
delete position.ethOwed;
|
||||
delete position.feeGrowthInside0LastX128;
|
||||
delete position.feeGrowthInside1LastX128;
|
||||
}
|
||||
|
||||
// function compareTokenToEthBalance(uint256 ethAmountInPosition, uint256 tokenAmountInPosition) external view returns (bool hasMoreToken) {
|
||||
// // Fetch the current sqrtPriceX96 from the pool
|
||||
// (uint160 sqrtPriceX96,,,) = uniswapV3Pool.slot0();
|
||||
|
||||
// // Convert sqrtPriceX96 to a conventional price format
|
||||
// // Note: The price is calculated as (sqrtPriceX96^2 / 2^192), simplified here as (price / 2^96) for the sake of example
|
||||
// uint256 price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) / (1 << 96);
|
||||
|
||||
// // Calculate the equivalent token amount for the ETH in the position at the current price
|
||||
// // Assuming price is expressed as the amount of token per ETH
|
||||
// uint256 equivalentTokenAmountForEth = ethAmountInPosition * price;
|
||||
|
||||
// // Compare to the actual token amount in the position
|
||||
// hasMoreToken = tokenAmountInPosition > equivalentTokenAmountForEth;
|
||||
|
||||
// return hasMoreToken;
|
||||
// }
|
||||
|
||||
////////
|
||||
// - check if tick in range, otherwise revert
|
||||
// - check if the position has more Token or more ETH, at current price
|
||||
// - if more ETH,
|
||||
// - calculate the amount of Token needed to be minted to bring the position to 50/50
|
||||
// - mint
|
||||
// - deposit Token into pool
|
||||
// - if more TOKEN
|
||||
// - calculate the amount of token needed to be withdrawn from the position, to bring the position to 50/50
|
||||
// - withdraw
|
||||
// - burn tokens
|
||||
|
||||
|
||||
|
||||
function stretch(int24 tickLower, int24 tickUpper, uint256 deadline, uint256 amount0Min, uint256 amount1Min) external checkDeadline(deadline) {
|
||||
|
||||
// Fetch the current tick from the Uniswap V3 pool
|
||||
(, int24 currentTick, , , , , ) = pool.slot0();
|
||||
|
||||
// Check if current tick is within the specified range
|
||||
int24 centerTick = tickLower + ((tickUpper - tickLower) / 2);
|
||||
// TODO: add hysteresis
|
||||
if (token0isWeth) {
|
||||
require(currentTick > centerTick && currentTick <= tickUpper, "Current tick out of range for stretch");
|
||||
} else {
|
||||
require(currentTick >= tickLower && currentTick < centerTick, "Current tick out of range for stretch");
|
||||
}
|
||||
|
||||
(uint256 ethReceived, uint256 oldliquidity) = liquidatePosition(tickLower, tickUpper, amount0Min, amount1Min);
|
||||
|
||||
uint256 liquidity;
|
||||
int24 newTickLower;
|
||||
int24 newTickUpper;
|
||||
if (token0isWeth) {
|
||||
newTickLower = tickLower;
|
||||
newTickUpper = currentTick + (currentTick - tickLower);
|
||||
// extend the range up
|
||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
|
||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(newTickUpper);
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmount0(
|
||||
sqrtRatioAX96, sqrtRatioBX96, ethReceived
|
||||
);
|
||||
// calculate amount for new liquidity
|
||||
uint256 newAmount1 = LiquidityAmounts.getAmount1ForLiquidity(
|
||||
sqrtRatioAX96, sqrtRatioBX96, liquidity
|
||||
);
|
||||
uint256 currentBal = harb.balanceOf(address(this));
|
||||
if (currentBal < newAmount1) {
|
||||
harb.mint(address(this), newAmount1 - currentBal);
|
||||
}
|
||||
|
||||
} else {
|
||||
newTickUpper = tickUpper;
|
||||
// extend the range down
|
||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickUpper);
|
||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - (tickUpper - currentTick));
|
||||
liquidity = LiquidityAmounts.getLiquidityForAmount1(
|
||||
sqrtRatioAX96, sqrtRatioBX96, ethReceived
|
||||
);
|
||||
// calculate amount for new liquidity
|
||||
uint256 newAmount0 = LiquidityAmounts.getAmount0ForLiquidity(
|
||||
sqrtRatioAX96, sqrtRatioBX96, liquidity
|
||||
);
|
||||
uint256 currentBal = harb.balanceOf(address(this));
|
||||
if (currentBal < newAmount0) {
|
||||
harb.mint(address(this), newAmount0 - currentBal);
|
||||
}
|
||||
newTickLower = ...
|
||||
}
|
||||
addLiquidity(newTickLower, newTickUpper, liquidity, deadline);
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
- find a way to account harb/eth for attacker
|
||||
- NFT support of etherscan
|
||||
https://etherscan.io/nft/0xe12edaab53023c75473a5a011bdb729ee73545e8/4218
|
||||
|
|
@ -6,7 +6,7 @@ import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
|
|||
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import "../src/Harb.sol";
|
||||
import "../src/Stake.sol";
|
||||
import {BaseLineLP} from "../src/BaseLineLP.sol";
|
||||
import {LiquidityManager} from "../src/LiquidityManager.sol";
|
||||
|
||||
address constant WETH = 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14; //Sepolia
|
||||
address constant V3_FACTORY = 0x0227628f3F023bb0B980b67D528571c95c6DaC1c; //Sepolia
|
||||
|
|
@ -58,6 +58,7 @@ contract SepoliaScript is Script {
|
|||
vm.startBroadcast(privateKey);
|
||||
|
||||
TwabController tc = TwabController(TWABC);
|
||||
// in case you want to deploy an new TwabController
|
||||
//TwabController tc = new TwabController(60 * 60, uint32(block.timestamp));
|
||||
Harb harb = new Harb("Harberger Tax", "HARB", tc);
|
||||
token0isWeth = address(WETH) < address(harb);
|
||||
|
|
@ -67,11 +68,12 @@ contract SepoliaScript is Script {
|
|||
address liquidityPool = factory.createPool(WETH, address(harb), FEE);
|
||||
initializePoolFor1Cent(liquidityPool);
|
||||
harb.setLiquidityPool(liquidityPool);
|
||||
BaseLineLP liquidityManager = new BaseLineLP(V3_FACTORY, WETH, address(harb));
|
||||
LiquidityManager liquidityManager = new LiquidityManager(V3_FACTORY, WETH, address(harb));
|
||||
// note: this delayed initialization is not a security issue.
|
||||
harb.setLiquidityManager(address(liquidityManager));
|
||||
//TODO: send some eth and call slide
|
||||
(bool sent, ) = address(liquidityManager).call{value: 0.1 ether}("");
|
||||
require(sent, "Failed to send Ether");
|
||||
//TODO: wait few minutes and call slide
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {ERC20, IERC20, IERC20Metadata} from "@openzeppelin/token/ERC20/ERC20.sol";
|
||||
|
|
@ -9,23 +8,18 @@ import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
|
|||
import {Math} from "@openzeppelin/utils/math/Math.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.
|
||||
* @title Harb ERC20 Token
|
||||
* @notice Implements an ERC20 token with mechanisms for minting, burning, and transferring tokens,
|
||||
* integrated with a TWAB controller for time-weighted balance tracking. This token supports
|
||||
* a novel economic model that finances Universal Basic Income (UBI) through a Harberger tax,
|
||||
* applied to staked tokens. The liquidityManager manages token supply to stabilize market liquidity.
|
||||
*/
|
||||
contract Harb is ERC20, ERC20Permit {
|
||||
using Math for uint256;
|
||||
|
||||
address public constant TAX_POOL = address(2);
|
||||
uint24 constant FEE = uint24(10_000);
|
||||
uint256 immutable PERIOD_OFFSET;
|
||||
uint256 immutable PERIOD_LENGTH;
|
||||
|
||||
/* ============ Public Variables ============ */
|
||||
uint256 public sumTaxCollected;
|
||||
uint256 public previousTotalSupply;
|
||||
uint24 private constant FEE = uint24(10_000);
|
||||
uint256 private immutable PERIOD_OFFSET;
|
||||
uint256 private immutable PERIOD_LENGTH;
|
||||
|
||||
//periphery contracts
|
||||
TwabController private immutable twabController;
|
||||
|
|
@ -33,6 +27,12 @@ contract Harb is ERC20, ERC20Permit {
|
|||
address private stakingPool;
|
||||
address private liquidityPool;
|
||||
|
||||
address public constant TAX_POOL = address(2);
|
||||
|
||||
/* ============ Public Variables ============ */
|
||||
uint256 public sumTaxCollected;
|
||||
uint256 public previousTotalSupply;
|
||||
|
||||
|
||||
struct UbiTitle {
|
||||
uint256 sumTaxCollected;
|
||||
|
|
@ -42,8 +42,6 @@ contract Harb is ERC20, ERC20Permit {
|
|||
mapping(address => UbiTitle) public ubiTitles;
|
||||
|
||||
/* ============ Errors ============ */
|
||||
|
||||
/// @notice Thrown if the some address is unexpectedly the zero address.
|
||||
error ZeroAddressInConstructor();
|
||||
error ZeroAddressInSetter();
|
||||
error AddressAlreadySet();
|
||||
|
|
@ -73,18 +71,30 @@ contract Harb is ERC20, ERC20Permit {
|
|||
PERIOD_LENGTH = twabController.PERIOD_LENGTH();
|
||||
}
|
||||
|
||||
/// @notice Sets the address for the liquidityPool. Used once post-deployment to initialize the contract.
|
||||
/// @dev Should be called only once right after the contract deployment to set the liquidity pool address.
|
||||
/// Throws AddressAlreadySet if called more than once.
|
||||
/// @param liquidityPool_ The address of the liquidity pool.
|
||||
function setLiquidityPool(address liquidityPool_) external {
|
||||
if (address(0) == liquidityPool_) revert ZeroAddressInSetter();
|
||||
if (liquidityPool != address(0)) revert AddressAlreadySet();
|
||||
liquidityPool = liquidityPool_;
|
||||
}
|
||||
|
||||
/// @notice Sets the address for the liquidityManager. Used once post-deployment to initialize the contract.
|
||||
/// @dev Should be called only once right after the contract deployment to set the liquidity manager address.
|
||||
/// Throws AddressAlreadySet if called more than once.
|
||||
/// @param liquidityManager_ The address of the liquidity manager.
|
||||
function setLiquidityManager(address liquidityManager_) external {
|
||||
if (address(0) == liquidityManager_) revert ZeroAddressInSetter();
|
||||
if (liquidityManager != address(0)) revert AddressAlreadySet();
|
||||
liquidityManager = liquidityManager_;
|
||||
}
|
||||
|
||||
/// @notice Sets the address for the stakingPool. Used once post-deployment to initialize the contract.
|
||||
/// @dev Should be called only once right after the contract deployment to set the staking pool address.
|
||||
/// Throws AddressAlreadySet if called more than once.
|
||||
/// @param stakingPool_ The address of the staking pool.
|
||||
function setStakingPool(address stakingPool_) external {
|
||||
if (address(0) == stakingPool_) revert ZeroAddressInSetter();
|
||||
if (stakingPool != address(0)) revert AddressAlreadySet();
|
||||
|
|
@ -97,9 +107,10 @@ contract Harb is ERC20, ERC20Permit {
|
|||
|
||||
/* ============ 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
|
||||
/// @notice Allows the liquidityManager to mint tokens for itself.
|
||||
/// @dev Tokens minted are managed as community liquidity in the Uniswap pool to stabilize HARB prices.
|
||||
/// Only callable by the Liquidity Manager. Minting rules and limits are defined externally.
|
||||
/// @param _amount The number of tokens to mint.
|
||||
function mint(uint256 _amount) external onlyLiquidityManager {
|
||||
_mint(address(liquidityManager), _amount);
|
||||
if (previousTotalSupply == 0) {
|
||||
|
|
@ -107,9 +118,10 @@ contract Harb is ERC20, ERC20Permit {
|
|||
}
|
||||
}
|
||||
|
||||
/// @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
|
||||
/// @notice Allows the liquidityManager to burn tokens from its account, adjusting the staking pool accordingly.
|
||||
/// @dev When tokens are burned, the total supply shrinks, making excess tokens in the staking pool unnecessary.
|
||||
/// These excess tokens are burned to maintain the guaranteed fixed percentage of the total supply for stakers.
|
||||
/// @param _amount The number of tokens to burn.
|
||||
function burn(uint256 _amount) external onlyLiquidityManager {
|
||||
_burn(address(liquidityManager), _amount);
|
||||
}
|
||||
|
|
@ -205,11 +217,6 @@ contract Harb is ERC20, ERC20Permit {
|
|||
|
||||
/* ============ 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;
|
||||
|
|
@ -226,6 +233,20 @@ contract Harb is ERC20, ERC20Permit {
|
|||
amountDue = taxCollectedSinceLastClaim.mulDiv(accountTwab, (totalSupplyTwab - stakeTwab - poolTwab - taxTwab), Math.Rounding.Down);
|
||||
}
|
||||
|
||||
/// @notice Calculates the UBI due to an account based on time-weighted average balances.
|
||||
/// @dev Uses historic TWAB data to determine an account's proportionate share of collected taxes since last claim.
|
||||
/// `lastTaxClaimed` is the timestamp of the last UBI claim. `_sumTaxCollected` is the tax collected up to the last claim.
|
||||
/// @param _account The account whose UBI is being calculated.
|
||||
/// @return amountDue The amount of UBI due to the account.
|
||||
/// @return lastPeriodEndAt The timestamp marking the end of the last period considered for UBI calculation.
|
||||
function getUbiDue(address _account) public view returns (uint256 amountDue, uint256 lastPeriodEndAt) {
|
||||
UbiTitle storage lastUbiTitle = ubiTitles[_account];
|
||||
return ubiDue(_account, lastUbiTitle.time, lastUbiTitle.sumTaxCollected);
|
||||
}
|
||||
|
||||
/// @notice Claims the calculated UBI amount for the caller.
|
||||
/// @dev Transfers the due UBI from the tax pool to the account, updating the UBI title.
|
||||
/// Emits UbiClaimed event on successful transfer.
|
||||
function claimUbi(address _account) external returns (uint256 ubiAmountDue) {
|
||||
UbiTitle storage lastUbiTitle = ubiTitles[_account];
|
||||
uint256 lastPeriodEndAt;
|
||||
|
|
|
|||
|
|
@ -16,14 +16,16 @@ import {Harb} from "./Harb.sol";
|
|||
|
||||
|
||||
/**
|
||||
* @title LiquidityManager - A contract that implements an automated market making strategy.
|
||||
* It maintains 3 positions:
|
||||
* - The floor position guarantees the capacity needed to maintain a minimum price of the HARB token It is a very tight liquidity range with enough reserve assets to buy back the circulating supply.
|
||||
* - The anchor range provides liquidity around the current market price, ensuring liquid trading conditions for the token, regardless of the market environment.
|
||||
* - The discovery range starts 1000 ticks above the current market price and increases from there. It consists solely of unissued tokens, which are sold as the market price increases.
|
||||
* The liquidity surplus obtained from selling tokens in the discovery range is directed back into the floor and anchor positions.
|
||||
*/
|
||||
contract BaseLineLP {
|
||||
* @title LiquidityManager for Harb Token on Uniswap V3
|
||||
* @notice Manages liquidity provisioning on Uniswap V3 for the Harb token by maintaining three distinct positions:
|
||||
* - Floor Position: Ensures a minimum price support by having enough reserve assets to potentially buy back the circulating supply of Harb.
|
||||
* - Anchor Position: Provides liquidity around the current market price to facilitate trading and maintain market stability.
|
||||
* - Discovery Position: Expands liquidity by minting new Harb tokens as the price rises, capturing potential growth in the ecosystem.
|
||||
* The contract dynamically adjusts these positions in response to market movements to maintain strategic liquidity levels and support the Harb token's price.
|
||||
* It also collects and transfers fees generated from trading activities to a designated fee destination.
|
||||
* @dev Utilizes Uniswap V3's concentrated liquidity feature, enabling highly efficient use of capital.
|
||||
*/
|
||||
contract LiquidityManager {
|
||||
int24 internal constant TICK_SPACING = 200;
|
||||
int24 internal constant ANCHOR_SPACING = 5 * TICK_SPACING;
|
||||
int24 internal constant DISCOVERY_SPACING = 11000;
|
||||
|
|
@ -62,6 +64,11 @@ contract BaseLineLP {
|
|||
|
||||
// TODO: add events
|
||||
|
||||
/// @notice Creates a liquidity manager for managing Harb token liquidity on Uniswap V3.
|
||||
/// @param _factory The address of the Uniswap V3 factory.
|
||||
/// @param _WETH9 The address of the WETH contract for handling ETH in trades.
|
||||
/// @param _harb The address of the Harb token contract.
|
||||
/// @dev Computes the Uniswap pool address for the Harb-WETH pair and sets up the initial configuration for the liquidity manager.
|
||||
constructor(address _factory, address _WETH9, address _harb) {
|
||||
factory = _factory;
|
||||
weth = IWETH9(_WETH9);
|
||||
|
|
@ -71,6 +78,10 @@ contract BaseLineLP {
|
|||
token0isWeth = _WETH9 < _harb;
|
||||
}
|
||||
|
||||
/// @notice Callback function that Uniswap V3 calls for liquidity actions requiring minting or burning of tokens.
|
||||
/// @param amount0Owed The amount of token0 owed for the liquidity provision.
|
||||
/// @param amount1Owed The amount of token1 owed for the liquidity provision.
|
||||
/// @dev This function mints Harb tokens as needed and handles WETH deposits for ETH conversions during liquidity interactions.
|
||||
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
|
||||
CallbackValidation.verifyCallback(factory, poolKey);
|
||||
// take care of harb
|
||||
|
|
@ -85,6 +96,9 @@ contract BaseLineLP {
|
|||
if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed);
|
||||
}
|
||||
|
||||
/// @notice Sets the address to which trading fees are transferred.
|
||||
/// @param feeDestination_ The address that will receive the collected trading fees.
|
||||
/// @dev Can only be called once to set the fee destination, further attempts will revert.
|
||||
function setFeeDestination(address feeDestination_) external {
|
||||
if (address(0) == feeDestination_) revert ZeroAddressInSetter();
|
||||
if (feeDestination != address(0)) revert AddressAlreadySet();
|
||||
|
|
@ -96,6 +110,11 @@ contract BaseLineLP {
|
|||
|
||||
}
|
||||
|
||||
/// @notice Calculates the Uniswap V3 tick corresponding to a given price ratio between Harb and ETH.
|
||||
/// @param t0isWeth Boolean flag indicating if token0 is WETH.
|
||||
/// @param tokenAmount Amount of the Harb token.
|
||||
/// @param ethAmount Amount of Ethereum.
|
||||
/// @return tick_ The calculated tick for the given price ratio.
|
||||
function tickAtPrice(bool t0isWeth, uint256 tokenAmount, uint256 ethAmount) internal pure returns (int24 tick_) {
|
||||
require(ethAmount > 0, "ETH amount cannot be zero");
|
||||
uint160 sqrtPriceX96;
|
||||
|
|
@ -117,12 +136,20 @@ contract BaseLineLP {
|
|||
tick_ = t0isWeth ? tick_ : -tick_;
|
||||
}
|
||||
|
||||
/// @notice Calculates the price ratio from a given Uniswap V3 tick.
|
||||
/// @param tick The tick for which to calculate the price ratio.
|
||||
/// @return priceRatio The price ratio corresponding to the given tick.
|
||||
function tickToPrice(int24 tick) public pure returns (uint256 priceRatio) {
|
||||
uint160 sqrtRatio = TickMath.getSqrtRatioAtTick(tick);
|
||||
uint256 adjustedSqrtRatio = uint256(sqrtRatio) / (1 << 48);
|
||||
priceRatio = adjustedSqrtRatio * adjustedSqrtRatio;
|
||||
}
|
||||
|
||||
/// @notice Internal function to mint liquidity positions in the Uniswap V3 pool.
|
||||
/// @param stage The liquidity stage (floor, anchor, discovery) being adjusted.
|
||||
/// @param tickLower The lower bound of the tick range for the position.
|
||||
/// @param tickUpper The upper bound of the tick range for the position.
|
||||
/// @param liquidity The amount of liquidity to mint at the specified range.
|
||||
function _mint(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal {
|
||||
// create position
|
||||
pool.mint(
|
||||
|
|
@ -141,6 +168,10 @@ contract BaseLineLP {
|
|||
});
|
||||
}
|
||||
|
||||
/// @notice Internal function to set or adjust the floor, anchor, and discovery positions based on current market conditions and the manager's strategy.
|
||||
/// @param sqrtPriceX96 The current price, expressed as a square root value that Uniswap V3 uses.
|
||||
/// @param currentTick The current market tick.
|
||||
/// @dev Recalculates and realigns all liquidity positions according to the latest market data and strategic requirements.
|
||||
function _set(uint160 sqrtPriceX96, int24 currentTick) internal {
|
||||
|
||||
// ### set Floor position
|
||||
|
|
@ -324,8 +355,8 @@ contract BaseLineLP {
|
|||
return (currentTick >= averageTick - MAX_TICK_DEVIATION && currentTick <= averageTick + MAX_TICK_DEVIATION);
|
||||
}
|
||||
|
||||
// call this function when price has moved up x%
|
||||
// TODO: write a bot that calls this function regularly
|
||||
/// @notice Adjusts liquidity positions upward in response to an increase in the Harb token's price.
|
||||
/// @dev This function should be called when significant upward price movement is detected. It recalibrates the liquidity ranges to align with the new market conditions.
|
||||
function shift() external {
|
||||
require(positions[Stage.ANCHOR].liquidity > 0, "Not initialized");
|
||||
// Fetch the current tick from the Uniswap V3 pool
|
||||
|
|
@ -357,7 +388,8 @@ contract BaseLineLP {
|
|||
_set(sqrtPriceX96, currentTick);
|
||||
}
|
||||
|
||||
|
||||
/// @notice Adjusts liquidity positions downward in response to a decrease in the Harb token's price.
|
||||
/// @dev This function should be called when significant downward price movement is detected. It recalibrates the liquidity ranges to align with the new market conditions.
|
||||
function slide() external {
|
||||
// Fetch the current tick from the Uniswap V3 pool
|
||||
(uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0();
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {IERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
|
||||
|
|
@ -12,6 +11,22 @@ import "./Harb.sol";
|
|||
error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake);
|
||||
error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStake, uint256 smallestShare);
|
||||
|
||||
/**
|
||||
* @title Stake Contract for Harb Token
|
||||
* @notice This contract manages the staking positions for the Harb token, allowing users to stake tokens
|
||||
* in exchange for a share of the total supply. Stakers can set and adjust tax rates on their stakes,
|
||||
* which affect the Universal Basic Income (UBI) paid from the tax pool.
|
||||
*
|
||||
* The contract handles:
|
||||
* - Creation of staking positions with specific tax rates.
|
||||
* - Snatching of existing positions under certain conditions to consolidate stakes.
|
||||
* - Calculation and payment of taxes based on stake duration and tax rate.
|
||||
* - Adjustment of tax rates with protections against griefing through rapid changes.
|
||||
* - Exiting of positions, either partially or fully, returning the staked assets to the owner.
|
||||
*
|
||||
* Tax rates and staking positions are adjustable, with a mechanism to prevent snatch-grieving by
|
||||
* enforcing a minimum tax payment duration.
|
||||
*/
|
||||
contract Stake {
|
||||
using Math for uint256;
|
||||
|
||||
|
|
@ -51,6 +66,9 @@ contract Stake {
|
|||
uint256 public nextPositionId;
|
||||
mapping(uint256 => StakingPosition) public positions;
|
||||
|
||||
/// @notice Initializes the stake contract with references to the Harb contract and sets the initial position ID.
|
||||
/// @param _harb Address of the Harb contract which this Stake contract interacts with.
|
||||
/// @dev Sets up the total supply based on the decimals of the Harb token plus a fixed offset.
|
||||
constructor(address _harb) {
|
||||
harb = Harb(_harb);
|
||||
|
||||
|
|
@ -63,17 +81,29 @@ contract Stake {
|
|||
return totalSupply * MAX_STAKE / 100;
|
||||
}
|
||||
|
||||
/// @notice Converts Harb token assets to shares of the total staking pool.
|
||||
/// @param assets Number of Harb tokens to convert.
|
||||
/// @return Number of shares corresponding to the input assets based on the current total supply of Harb tokens.
|
||||
function assetsToShares(uint256 assets) public view returns (uint256) {
|
||||
return assets.mulDiv(totalSupply, harb.totalSupply(), Math.Rounding.Down);
|
||||
//return assets.mulDiv(totalSupply, harb.totalSupply() + 1, rounding);
|
||||
}
|
||||
|
||||
/// @notice Converts shares of the total staking pool back to Harb token assets.
|
||||
/// @param shares Number of shares to convert.
|
||||
/// @return The equivalent number of Harb tokens for the given shares.
|
||||
function sharesToAssets(uint256 shares) public view returns (uint256) {
|
||||
//return shares.mulDiv(harb.totalSupply() + 1, totalSupply, rounding);
|
||||
// TODO: should the average total supply be used for this calculation?
|
||||
return shares.mulDiv(harb.totalSupply(), totalSupply, Math.Rounding.Down);
|
||||
}
|
||||
|
||||
/// @notice Combines an ERC20 permit operation with the snatch function, allowing a staking position creation in one transaction.
|
||||
/// @param assets Number of Harb tokens to stake.
|
||||
/// @param receiver Address that will own the new staking position.
|
||||
/// @param taxRate The initial tax rate for the new staking position.
|
||||
/// @param positionsToSnatch Array of position IDs that the new position will replace by snatching.
|
||||
/// @param deadline Time until which the permit is valid.
|
||||
/// @param v, r, s Components of the signature for the permit.
|
||||
/// @return positionId The ID of the newly created staking position.
|
||||
function permitAndSnatch(
|
||||
uint256 assets,
|
||||
address receiver,
|
||||
|
|
@ -93,13 +123,13 @@ contract Stake {
|
|||
return snatch(assets, receiver, taxRate, positionsToSnatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: deal with metatransactions: While these are generally available
|
||||
* via msg.sender and msg.data, they should not be accessed in such a direct
|
||||
* manner, since when dealing with meta-transactions the account sending and
|
||||
* paying for execution may not be the actual sender (as far as an application
|
||||
* is concerned).
|
||||
*/
|
||||
/// @notice Creates a new staking position by potentially snatching shares from existing positions.
|
||||
/// @param assets Amount of Harb tokens to convert into a staking position.
|
||||
/// @param receiver Address that will own the new staking position.
|
||||
/// @param taxRate The initial tax rate for the new staking position.
|
||||
/// @param positionsToSnatch Array of position IDs that the new position will replace by snatching.
|
||||
/// @return positionId The ID of the newly created staking position.
|
||||
/// @dev Handles staking logic, including tax rate validation and position merging or dissolving.
|
||||
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch)
|
||||
public
|
||||
returns (uint256 positionId)
|
||||
|
|
@ -195,6 +225,10 @@ contract Stake {
|
|||
emit PositionCreated(positionId, sp.owner, assets, sp.share, sp.taxRate);
|
||||
}
|
||||
|
||||
/// @notice Changes the tax rate of an existing staking position.
|
||||
/// @param positionId The ID of the staking position to update.
|
||||
/// @param taxRate The new tax rate to apply to the position.
|
||||
/// @dev Ensures that the tax rate change is valid and applies the minimum tax based on the TAX_FLOOR_DURATION.
|
||||
function changeTax(uint256 positionId, uint32 taxRate) public {
|
||||
require(taxRate < TAX_RATES.length, "tax rate out of bounds");
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
|
|
@ -211,6 +245,9 @@ contract Stake {
|
|||
emit PositionRateHiked(positionId, pos.owner, taxRate);
|
||||
}
|
||||
|
||||
/// @notice Allows the owner of a staking position to exit, returning the staked assets.
|
||||
/// @param positionId The ID of the staking position to exit.
|
||||
/// @dev Pays the due taxes based on the TAX_FLOOR_DURATION and returns the remaining assets to the position owner.
|
||||
function exitPosition(uint256 positionId) public {
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
if (pos.creationTime == 0) {
|
||||
|
|
@ -224,13 +261,19 @@ contract Stake {
|
|||
_exitPosition(positionId, pos);
|
||||
}
|
||||
|
||||
// TODO: write a bot that calls this function regularly
|
||||
/// @notice Manually triggers the tax payment for a specified staking position.
|
||||
/// @param positionId The ID of the staking position for which to pay taxes.
|
||||
/// @dev Calculates and pays the tax due, possibly adjusting the position's share count.
|
||||
function payTax(uint256 positionId) public {
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
// TODO: what if someone calls payTax and exitPosition in the same transaction?
|
||||
_payTax(positionId, pos, 0);
|
||||
}
|
||||
|
||||
/// @notice Calculates the Tax that is due to be paid on specific positoin
|
||||
/// @param positionId The ID of the staking position for which to pay taxes.
|
||||
/// @param taxFloorDuration if a minimum holding duration is applied to the position this value is > 0 in seconds.
|
||||
/// @dev Calculates the tax due.
|
||||
function taxDue(uint256 positionId, uint256 taxFloorDuration) public view returns (uint256 amountDue) {
|
||||
StakingPosition storage pos = positions[positionId];
|
||||
// ihet = Implied Holding Expiry Timestamp
|
||||
|
|
@ -242,6 +285,7 @@ contract Stake {
|
|||
amountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
||||
}
|
||||
|
||||
/// @dev Internal function to calculate and pay taxes for a position, adjusting shares and handling position liquidation if necessary.
|
||||
function _payTax(uint256 positionId, StakingPosition storage pos, uint256 taxFloorDuration) private {
|
||||
// ihet = Implied Holding Expiry Timestamp
|
||||
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration)
|
||||
|
|
@ -274,6 +318,7 @@ contract Stake {
|
|||
}
|
||||
}
|
||||
|
||||
/// @dev Internal function to close a staking position, transferring the remaining Harb tokens back to the owner after tax payment.
|
||||
function _exitPosition(uint256 positionId, StakingPosition storage pos) private {
|
||||
outstandingStake -= pos.share;
|
||||
address owner = pos.owner;
|
||||
|
|
@ -285,6 +330,7 @@ contract Stake {
|
|||
SafeERC20.safeTransfer(harb, owner, assets);
|
||||
}
|
||||
|
||||
/// @dev Internal function to reduce the size of a staking position by a specified number of shares, transferring the corresponding Harb tokens to the owner.
|
||||
function _shrinkPosition(uint256 positionId, StakingPosition storage pos, uint256 sharesToTake) private {
|
||||
require (sharesToTake < pos.share, "position too small");
|
||||
uint256 assets = sharesToAssets(sharesToTake);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
|||
import {Harb} from "../src/Harb.sol";
|
||||
|
||||
import {Stake, ExceededAvailableStake} from "../src/Stake.sol";
|
||||
import {BaseLineLP} from "../src/BaseLineLP.sol";
|
||||
import {LiquidityManager} from "../src/LiquidityManager.sol";
|
||||
|
||||
|
||||
address constant TAX_POOL = address(2);
|
||||
|
|
@ -27,13 +27,13 @@ contract Dummy {
|
|||
// This contract can be empty as it is only used to affect the nonce
|
||||
}
|
||||
|
||||
contract BaseLineLP2Test is Test {
|
||||
contract LiquidityManagerTest is Test {
|
||||
|
||||
IWETH9 weth;
|
||||
Harb harb;
|
||||
IUniswapV3Factory factory;
|
||||
Stake stake;
|
||||
BaseLineLP lm;
|
||||
LiquidityManager lm;
|
||||
IUniswapV3Pool pool;
|
||||
bool token0isWeth;
|
||||
address account = makeAddr("alice");
|
||||
|
|
@ -124,7 +124,7 @@ contract BaseLineLP2Test is Test {
|
|||
|
||||
stake = new Stake(address(harb));
|
||||
harb.setStakingPool(address(stake));
|
||||
lm = new BaseLineLP(factoryAddress, address(weth), address(harb));
|
||||
lm = new LiquidityManager(factoryAddress, address(weth), address(harb));
|
||||
lm.setFeeDestination(feeDestination);
|
||||
harb.setLiquidityManager(address(lm));
|
||||
vm.deal(address(lm), 10 ether);
|
||||
|
|
@ -178,7 +178,7 @@ contract BaseLineLP2Test is Test {
|
|||
}
|
||||
|
||||
|
||||
function getBalancesPool(BaseLineLP.Stage s) internal view returns (int24 currentTick, int24 tickLower, int24 tickUpper, uint256 ethAmount, uint256 harbAmount) {
|
||||
function getBalancesPool(LiquidityManager.Stage s) internal view returns (int24 currentTick, int24 tickLower, int24 tickUpper, uint256 ethAmount, uint256 harbAmount) {
|
||||
(,tickLower, tickUpper) = lm.positions(s);
|
||||
(uint128 liquidity, , , ,) = pool.positions(keccak256(abi.encodePacked(address(lm), tickLower, tickUpper)));
|
||||
|
||||
|
|
@ -225,13 +225,13 @@ contract BaseLineLP2Test is Test {
|
|||
int24 currentTick;
|
||||
int24 tickLower;
|
||||
int24 tickUpper;
|
||||
(currentTick, tickLower, tickUpper, ethFloor, harbFloor) = getBalancesPool(BaseLineLP.Stage.FLOOR);
|
||||
(currentTick, tickLower, tickUpper, ethFloor, harbFloor) = getBalancesPool(LiquidityManager.Stage.FLOOR);
|
||||
string memory floorData = string(abi.encodePacked(intToStr(tickLower), ",", intToStr(tickUpper), ",", uintToStr(ethFloor), ",", uintToStr(harbFloor), ","));
|
||||
|
||||
(,tickLower, tickUpper, ethAnchor, harbAnchor) = getBalancesPool(BaseLineLP.Stage.ANCHOR);
|
||||
(,tickLower, tickUpper, ethAnchor, harbAnchor) = getBalancesPool(LiquidityManager.Stage.ANCHOR);
|
||||
string memory anchorData = string(abi.encodePacked(intToStr(tickLower), ",", intToStr(tickUpper), ",", uintToStr(ethAnchor), ",", uintToStr(harbAnchor), ","));
|
||||
|
||||
(,tickLower, tickUpper, ethDiscovery, harbDiscovery) = getBalancesPool(BaseLineLP.Stage.DISCOVERY);
|
||||
(,tickLower, tickUpper, ethDiscovery, harbDiscovery) = getBalancesPool(LiquidityManager.Stage.DISCOVERY);
|
||||
string memory discoveryData = string(abi.encodePacked(intToStr(tickLower), ",", intToStr(tickUpper), ",", uintToStr(ethDiscovery), ",", uintToStr(harbDiscovery), ","));
|
||||
|
||||
csv = string(abi.encodePacked(csv, "\n", eventName, ",", intToStr(currentTick), ",", floorData, anchorData, discoveryData));
|
||||
|
|
@ -525,7 +525,7 @@ contract BaseLineLP2Test is Test {
|
|||
|
||||
if (f >= frequency) {
|
||||
(, int24 currentTick, , , , , ) = pool.slot0();
|
||||
(, int24 tickLower, int24 tickUpper) = lm.positions(BaseLineLP.Stage.ANCHOR);
|
||||
(, int24 tickLower, int24 tickUpper) = lm.positions(LiquidityManager.Stage.ANCHOR);
|
||||
int24 midTick = token0isWeth ? tickLower + ANCHOR_SPACING : tickUpper - ANCHOR_SPACING;
|
||||
if (currentTick < midTick) {
|
||||
// Current tick is below the midpoint, so call slide()
|
||||
Loading…
Add table
Add a link
Reference in a new issue