first deployment
This commit is contained in:
parent
9279e0c045
commit
06581a0b8d
13 changed files with 180 additions and 225 deletions
3
onchain/.gitignore
vendored
3
onchain/.gitignore
vendored
|
|
@ -12,3 +12,6 @@ docs/
|
||||||
|
|
||||||
# Dotenv file
|
# Dotenv file
|
||||||
.env
|
.env
|
||||||
|
.secret
|
||||||
|
|
||||||
|
/broadcast/
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,4 @@
|
||||||
@openzeppelin/=lib/openzeppelin-contracts/contracts/
|
@openzeppelin/=lib/openzeppelin-contracts/contracts/
|
||||||
@uniswap-v3-core/=lib/uni-v3-lib/node_modules/@uniswap/v3-core/contracts/
|
@uniswap-v3-core/=lib/uni-v3-lib/node_modules/@uniswap/v3-core/contracts/
|
||||||
|
@uniswap-v3-periphery=lib/uni-v3-lib/node_modules/@uniswap/v3-periphery/contracts/
|
||||||
@aperture/uni-v3-lib/=lib/uni-v3-lib/src/
|
@aperture/uni-v3-lib/=lib/uni-v3-lib/src/
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
// SPDX-License-Identifier: UNLICENSED
|
|
||||||
pragma solidity ^0.8.13;
|
|
||||||
|
|
||||||
import {Script, console} from "forge-std/Script.sol";
|
|
||||||
|
|
||||||
contract CounterScript is Script {
|
|
||||||
function setUp() public {}
|
|
||||||
|
|
||||||
function run() public {
|
|
||||||
vm.broadcast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
pragma solidity ^0.8.4;
|
pragma solidity ^0.8.4;
|
||||||
|
|
||||||
import "forge-std/Script.sol";
|
import "forge-std/Script.sol";
|
||||||
import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
|
import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
|
||||||
import "../src/Harb.sol";
|
import "../src/Harb.sol";
|
||||||
import "../src/Stake.sol";
|
import "../src/Stake.sol";
|
||||||
|
|
||||||
contract GoerliScript is Script {
|
contract SepoliaScript is Script {
|
||||||
function setUp() public {}
|
function setUp() public {}
|
||||||
|
|
||||||
function run() public {
|
function run() public {
|
||||||
|
|
@ -13,11 +13,11 @@ contract GoerliScript is Script {
|
||||||
uint256 privateKey = vm.deriveKey(seedPhrase, 0);
|
uint256 privateKey = vm.deriveKey(seedPhrase, 0);
|
||||||
vm.startBroadcast(privateKey);
|
vm.startBroadcast(privateKey);
|
||||||
|
|
||||||
TwabController tc = new TwabController(60*60*24, uint32(block.timestamp));
|
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", tc);
|
||||||
Stake stake = new Stake(address(harb));
|
Stake stake = new Stake(address(harb));
|
||||||
harb.setStakingPool(address(stake));
|
harb.setStakingPool(address(stake));
|
||||||
|
|
||||||
vm.stopBroadcast();
|
vm.stopBroadcast();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
pragma solidity ^0.8.19;
|
pragma solidity ^0.8.19;
|
||||||
|
|
||||||
import { ERC20, IERC20, IERC20Metadata } from "@openzeppelin/token/ERC20/ERC20.sol";
|
import {ERC20, IERC20, IERC20Metadata} from "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
import { ERC20Permit, IERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
|
import {ERC20Permit, IERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
|
||||||
import { SafeCast } from "@openzeppelin/utils/math/SafeCast.sol";
|
import {SafeCast} from "@openzeppelin/utils/math/SafeCast.sol";
|
||||||
import { IStake } from "./interfaces/IStake.sol";
|
import {IStake} from "./interfaces/IStake.sol";
|
||||||
import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
|
import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title TWAB ERC20 Token
|
* @title TWAB ERC20 Token
|
||||||
|
|
@ -16,7 +16,6 @@ import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
|
||||||
* gas savings. Any mints that increase a balance past this limit will fail.
|
* gas savings. Any mints that increase a balance past this limit will fail.
|
||||||
*/
|
*/
|
||||||
contract Harb is ERC20, ERC20Permit {
|
contract Harb is ERC20, ERC20Permit {
|
||||||
|
|
||||||
address public constant TAX_POOL = address(2);
|
address public constant TAX_POOL = address(2);
|
||||||
|
|
||||||
/* ============ Public Variables ============ */
|
/* ============ Public Variables ============ */
|
||||||
|
|
@ -47,11 +46,10 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
* @param name_ The name of the token
|
* @param name_ The name of the token
|
||||||
* @param symbol_ The token symbol
|
* @param symbol_ The token symbol
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(string memory name_, string memory symbol_, TwabController twabController_)
|
||||||
string memory name_,
|
ERC20(name_, symbol_)
|
||||||
string memory symbol_,
|
ERC20Permit(name_)
|
||||||
TwabController twabController_
|
{
|
||||||
) ERC20(name_, symbol_) ERC20Permit(name_) {
|
|
||||||
if (address(0) == address(twabController_)) revert ZeroAddressInConstructor();
|
if (address(0) == address(twabController_)) revert ZeroAddressInConstructor();
|
||||||
twabController = twabController_;
|
twabController = twabController_;
|
||||||
}
|
}
|
||||||
|
|
@ -87,9 +85,7 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
/* ============ Public ERC20 Overrides ============ */
|
/* ============ Public ERC20 Overrides ============ */
|
||||||
|
|
||||||
/// @inheritdoc ERC20
|
/// @inheritdoc ERC20
|
||||||
function balanceOf(
|
function balanceOf(address _account) public view virtual override(ERC20) returns (uint256) {
|
||||||
address _account
|
|
||||||
) public view virtual override(ERC20) returns (uint256) {
|
|
||||||
return twabController.balanceOf(address(this), _account);
|
return twabController.balanceOf(address(this), _account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,8 +94,6 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
return twabController.totalSupply(address(this));
|
return twabController.totalSupply(address(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ============ Internal ERC20 Overrides ============ */
|
/* ============ Internal ERC20 Overrides ============ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -115,8 +109,8 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
uint256 activeSupply = totalSupply() - stakingPoolBalance;
|
uint256 activeSupply = totalSupply() - stakingPoolBalance;
|
||||||
uint256 dormantStake = IStake(stakingPool).dormantSupply();
|
uint256 dormantStake = IStake(stakingPool).dormantSupply();
|
||||||
if (stakingPoolBalance > 0) {
|
if (stakingPoolBalance > 0) {
|
||||||
uint256 newStake = stakingPoolBalance * amount / (activeSupply + dormantStake);
|
uint256 newStake = stakingPoolBalance * amount / (activeSupply + dormantStake);
|
||||||
_mint(stakingPool, newStake);
|
_mint(stakingPool, newStake);
|
||||||
}
|
}
|
||||||
|
|
||||||
twabController.mint(receiver, SafeCast.toUint96(amount));
|
twabController.mint(receiver, SafeCast.toUint96(amount));
|
||||||
|
|
@ -151,5 +145,4 @@ contract Harb is ERC20, ERC20Permit {
|
||||||
twabController.transfer(_from, _to, SafeCast.toUint96(_amount));
|
twabController.transfer(_from, _to, SafeCast.toUint96(_amount));
|
||||||
emit Transfer(_from, _to, _amount);
|
emit Transfer(_from, _to, _amount);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
pragma solidity ^0.8.20;
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
import './lib/PositionKey.sol';
|
import "@uniswap-v3-periphery/libraries/PositionKey.sol";
|
||||||
import './lib/FixedPoint128.sol';
|
import "@uniswap-v3-core/libraries/FixedPoint128.sol";
|
||||||
import '@aperture/uni-v3-lib/TickMath.sol';
|
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||||
import '@aperture/uni-v3-lib/LiquidityAmounts.sol';
|
import "@aperture/uni-v3-lib/TickMath.sol";
|
||||||
import '@aperture/uni-v3-lib/PoolAddress.sol';
|
import "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
||||||
import '@aperture/uni-v3-lib/CallbackValidation.sol';
|
import "@aperture/uni-v3-lib/PoolAddress.sol";
|
||||||
import '@uniswap-v3-core/interfaces/IUniswapV3Pool.sol';
|
import "@aperture/uni-v3-lib/CallbackValidation.sol";
|
||||||
import '@openzeppelin/token/ERC20/IERC20.sol';
|
import "@openzeppelin/token/ERC20/IERC20.sol";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title LiquidityManager - A contract that supports the $bloodX ecosystem. It
|
* @title LiquidityManager - A contract that supports the $bloodX ecosystem. It
|
||||||
* protects the communities liquidity while allowing a manager role to
|
* protects the communities liquidity while allowing a manager role to
|
||||||
* take strategic liqudity positions.
|
* take strategic liqudity positions.
|
||||||
*/
|
*/
|
||||||
contract LiquidityManager {
|
contract LiquidityManager {
|
||||||
|
|
||||||
// default fee of 1%
|
// default fee of 1%
|
||||||
uint24 constant FEE = uint24(10_000);
|
uint24 constant FEE = uint24(10_000);
|
||||||
|
|
||||||
|
|
@ -63,7 +62,7 @@ contract LiquidityManager {
|
||||||
mapping(bytes26 => TokenPosition) private _positions;
|
mapping(bytes26 => TokenPosition) private _positions;
|
||||||
|
|
||||||
modifier checkDeadline(uint256 deadline) {
|
modifier checkDeadline(uint256 deadline) {
|
||||||
require(block.timestamp <= deadline, 'Transaction too old');
|
require(block.timestamp <= deadline, "Transaction too old");
|
||||||
_;
|
_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,13 +77,9 @@ contract LiquidityManager {
|
||||||
/// @param amount1 The amount of token1 that was accounted for the decrease in liquidity
|
/// @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);
|
event DecreaseLiquidity(address indexed token, uint128 liquidity, uint256 amount0, uint256 amount1);
|
||||||
|
|
||||||
constructor(
|
constructor(address _factory, address _WETH9) {
|
||||||
address _factory,
|
|
||||||
address _WETH9
|
|
||||||
) {
|
|
||||||
factory = _factory;
|
factory = _factory;
|
||||||
WETH9 = _WETH9;
|
WETH9 = _WETH9;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getToken(address token0, address token1) internal view returns (bool token0isWeth, address token) {
|
function getToken(address token0, address token1) internal view returns (bool token0isWeth, address token) {
|
||||||
|
|
@ -112,26 +107,16 @@ contract LiquidityManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function positions(address token, int24 tickLower, int24 tickUpper) external view
|
function positions(address token, int24 tickLower, int24 tickUpper)
|
||||||
returns (
|
external
|
||||||
uint128 liquidity,
|
view
|
||||||
uint256 feeGrowthInside0LastX128,
|
returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128)
|
||||||
uint256 feeGrowthInside1LastX128
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)];
|
TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)];
|
||||||
return (
|
return (position.liquidity, position.feeGrowthInside0LastX128, position.feeGrowthInside1LastX128);
|
||||||
position.liquidity,
|
|
||||||
position.feeGrowthInside0LastX128,
|
|
||||||
position.feeGrowthInside1LastX128
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function uniswapV3MintCallback(
|
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata data) external {
|
||||||
uint256 amount0Owed,
|
|
||||||
uint256 amount1Owed,
|
|
||||||
bytes calldata data
|
|
||||||
) external {
|
|
||||||
PoolKey memory poolKey = abi.decode(data, (PoolKey));
|
PoolKey memory poolKey = abi.decode(data, (PoolKey));
|
||||||
CallbackValidation.verifyCallback(factory, poolKey);
|
CallbackValidation.verifyCallback(factory, poolKey);
|
||||||
|
|
||||||
|
|
@ -147,62 +132,57 @@ contract LiquidityManager {
|
||||||
// compute the liquidity amount
|
// compute the liquidity amount
|
||||||
uint128 liquidity;
|
uint128 liquidity;
|
||||||
{
|
{
|
||||||
(uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
|
(uint160 sqrtPriceX96,,,,,,) = pool.slot0();
|
||||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower);
|
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower);
|
||||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper);
|
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper);
|
||||||
|
|
||||||
liquidity = LiquidityAmounts.getLiquidityForAmounts(
|
liquidity = LiquidityAmounts.getLiquidityForAmounts(
|
||||||
sqrtPriceX96,
|
sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, params.amount0Desired, params.amount1Desired
|
||||||
sqrtRatioAX96,
|
|
||||||
sqrtRatioBX96,
|
|
||||||
params.amount0Desired,
|
|
||||||
params.amount1Desired
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
(bool token0isWeth, address token) = getToken(params.token0, params.token1);
|
(bool token0isWeth, address token) = getToken(params.token0, params.token1);
|
||||||
{
|
{
|
||||||
(uint256 amount0, uint256 amount1) = pool.mint(
|
(uint256 amount0, uint256 amount1) =
|
||||||
address(this),
|
pool.mint(address(this), params.tickLower, params.tickUpper, liquidity, abi.encode(poolKey));
|
||||||
params.tickLower,
|
|
||||||
params.tickUpper,
|
|
||||||
liquidity,
|
|
||||||
abi.encode(poolKey)
|
|
||||||
);
|
|
||||||
|
|
||||||
require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');
|
require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, "Price slippage check");
|
||||||
emit IncreaseLiquidity(token, liquidity, amount0, amount1);
|
emit IncreaseLiquidity(token, liquidity, amount0, amount1);
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper);
|
bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper);
|
||||||
|
|
||||||
// this is now updated to the current transaction
|
// this is now updated to the current transaction
|
||||||
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);
|
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
|
||||||
|
|
||||||
|
|
||||||
TokenPosition memory position = _positions[posKey(token, params.tickLower, params.tickUpper)];
|
TokenPosition memory position = _positions[posKey(token, params.tickLower, params.tickUpper)];
|
||||||
if (liquidity == 0) {
|
if (liquidity == 0) {
|
||||||
// create entry
|
// create entry
|
||||||
position = TokenPosition({
|
position = TokenPosition({
|
||||||
liquidity: liquidity,
|
liquidity: liquidity,
|
||||||
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
|
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
|
||||||
feeGrowthInside1LastX128: feeGrowthInside1LastX128
|
feeGrowthInside1LastX128: feeGrowthInside1LastX128
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// update entry
|
// update entry
|
||||||
updateFeesOwed(token0isWeth, token, uint128(
|
updateFeesOwed(
|
||||||
FullMath.mulDiv(
|
token0isWeth,
|
||||||
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
|
token,
|
||||||
position.liquidity,
|
uint128(
|
||||||
FixedPoint128.Q128
|
FullMath.mulDiv(
|
||||||
|
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
|
||||||
|
position.liquidity,
|
||||||
|
FixedPoint128.Q128
|
||||||
|
)
|
||||||
|
),
|
||||||
|
uint128(
|
||||||
|
FullMath.mulDiv(
|
||||||
|
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
|
||||||
|
position.liquidity,
|
||||||
|
FixedPoint128.Q128
|
||||||
|
)
|
||||||
)
|
)
|
||||||
), uint128(
|
);
|
||||||
FullMath.mulDiv(
|
|
||||||
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
|
|
||||||
position.liquidity,
|
|
||||||
FixedPoint128.Q128
|
|
||||||
)
|
|
||||||
));
|
|
||||||
|
|
||||||
position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
|
position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
|
||||||
position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
|
position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
|
||||||
|
|
@ -227,27 +207,28 @@ contract LiquidityManager {
|
||||||
|
|
||||||
(amount0, amount1) = pool.burn(params.tickLower, params.tickUpper, params.liquidity);
|
(amount0, amount1) = pool.burn(params.tickLower, params.tickUpper, params.liquidity);
|
||||||
|
|
||||||
require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');
|
require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, "Price slippage check");
|
||||||
|
|
||||||
bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper);
|
bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper);
|
||||||
// this is now updated to the current transaction
|
// this is now updated to the current transaction
|
||||||
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey);
|
(, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey);
|
||||||
|
|
||||||
updateFeesOwed(token0isWeth, token, uint128(amount0) +
|
updateFeesOwed(
|
||||||
uint128(
|
token0isWeth,
|
||||||
FullMath.mulDiv(
|
token,
|
||||||
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
|
uint128(amount0)
|
||||||
positionLiquidity,
|
+ uint128(
|
||||||
FixedPoint128.Q128
|
FullMath.mulDiv(
|
||||||
|
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, positionLiquidity, FixedPoint128.Q128
|
||||||
|
)
|
||||||
|
),
|
||||||
|
uint128(amount1)
|
||||||
|
+ uint128(
|
||||||
|
FullMath.mulDiv(
|
||||||
|
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, positionLiquidity, FixedPoint128.Q128
|
||||||
|
)
|
||||||
)
|
)
|
||||||
), uint128(amount1) +
|
);
|
||||||
uint128(
|
|
||||||
FullMath.mulDiv(
|
|
||||||
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
|
|
||||||
positionLiquidity,
|
|
||||||
FixedPoint128.Q128
|
|
||||||
)
|
|
||||||
));
|
|
||||||
|
|
||||||
position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
|
position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
|
||||||
position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
|
position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
|
||||||
|
|
@ -257,36 +238,35 @@ contract LiquidityManager {
|
||||||
emit DecreaseLiquidity(token, params.liquidity, amount0, amount1);
|
emit DecreaseLiquidity(token, params.liquidity, amount0, amount1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// function compareTokenToEthBalance(uint256 ethAmountInPosition, uint256 tokenAmountInPosition) external view returns (bool hasMoreToken) {
|
// function compareTokenToEthBalance(uint256 ethAmountInPosition, uint256 tokenAmountInPosition) external view returns (bool hasMoreToken) {
|
||||||
// // Fetch the current sqrtPriceX96 from the pool
|
// // Fetch the current sqrtPriceX96 from the pool
|
||||||
// (uint160 sqrtPriceX96,,,) = uniswapV3Pool.slot0();
|
// (uint160 sqrtPriceX96,,,) = uniswapV3Pool.slot0();
|
||||||
|
|
||||||
// // Convert sqrtPriceX96 to a conventional price format
|
// // 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
|
// // 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);
|
// uint256 price = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) / (1 << 96);
|
||||||
|
|
||||||
// // Calculate the equivalent token amount for the ETH in the position at the current price
|
// // 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
|
// // Assuming price is expressed as the amount of token per ETH
|
||||||
// uint256 equivalentTokenAmountForEth = ethAmountInPosition * price;
|
// uint256 equivalentTokenAmountForEth = ethAmountInPosition * price;
|
||||||
|
|
||||||
// // Compare to the actual token amount in the position
|
// // Compare to the actual token amount in the position
|
||||||
// hasMoreToken = tokenAmountInPosition > equivalentTokenAmountForEth;
|
// hasMoreToken = tokenAmountInPosition > equivalentTokenAmountForEth;
|
||||||
|
|
||||||
// return hasMoreToken;
|
// return hasMoreToken;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
////////
|
////////
|
||||||
// - check if tick in range, otherwise revert
|
// - check if tick in range, otherwise revert
|
||||||
// - check if the position has more Token or more ETH, at current price
|
// - check if the position has more Token or more ETH, at current price
|
||||||
// - if more ETH,
|
// - if more ETH,
|
||||||
// - calculate the amount of Token needed to be minted to bring the position to 50/50
|
// - calculate the amount of Token needed to be minted to bring the position to 50/50
|
||||||
// - mint
|
// - mint
|
||||||
// - deposit Token into pool
|
// - deposit Token into pool
|
||||||
// - if more TOKEN
|
// - if more TOKEN
|
||||||
// - calculate the amount of token needed to be withdrawn from the position, to bring the position to 50/50
|
// - calculate the amount of token needed to be withdrawn from the position, to bring the position to 50/50
|
||||||
// - withdraw
|
// - withdraw
|
||||||
// - burn tokens
|
// - burn tokens
|
||||||
|
|
||||||
// function rebalance(address token, int24 tickLower, int24 tickUpper) external {
|
// function rebalance(address token, int24 tickLower, int24 tickUpper) external {
|
||||||
// bool ETH_TOKEN_ZERO = WETH9 < token;
|
// bool ETH_TOKEN_ZERO = WETH9 < token;
|
||||||
|
|
@ -335,9 +315,5 @@ contract LiquidityManager {
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
pragma solidity ^0.8.13;
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
|
import {IERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
|
||||||
import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
|
import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
|
||||||
import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
|
import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
|
||||||
import {Math} from "@openzeppelin/utils/math/Math.sol";
|
import {Math} from "@openzeppelin/utils/math/Math.sol";
|
||||||
import "./interfaces/IStake.sol";
|
import "./interfaces/IStake.sol";
|
||||||
import "./Harb.sol";
|
import "./Harb.sol";
|
||||||
|
|
@ -13,26 +13,26 @@ contract Stake is IStake {
|
||||||
using Math for uint256;
|
using Math for uint256;
|
||||||
|
|
||||||
uint256 internal DECIMAL_OFFSET = 5 + 2;
|
uint256 internal DECIMAL_OFFSET = 5 + 2;
|
||||||
uint256 internal constant MAX_STAKE = 20; // 20% of HARB supply
|
uint256 internal constant MAX_STAKE = 20; // 20% of HARB supply
|
||||||
uint256 internal constant MAX_TAX = 1000; // max 1000% tax per year
|
uint256 internal constant MAX_TAX = 1000; // max 1000% tax per year
|
||||||
uint256 internal constant TAX_RATE_BASE = 100;
|
uint256 internal constant TAX_RATE_BASE = 100;
|
||||||
uint256 internal constant TAX_FLOOR_DURATION = 60 * 60 * 24 * 3; //this duration is the minimum basis for fee calculation, regardless of actual holding time.
|
uint256 internal constant TAX_FLOOR_DURATION = 60 * 60 * 24 * 3; //this duration is the minimum basis for fee calculation, regardless of actual holding time.
|
||||||
/**
|
/**
|
||||||
* @dev Attempted to deposit more assets than the max amount for `receiver`.
|
* @dev Attempted to deposit more assets than the max amount for `receiver`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake);
|
error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake);
|
||||||
error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId);
|
error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId);
|
||||||
error SharesTooLow(address receiver, uint256 assets, uint256 sharesWanted, uint256 minStake);
|
error SharesTooLow(address receiver, uint256 assets, uint256 sharesWanted, uint256 minStake);
|
||||||
error NoPermission(address requester, address owner);
|
error NoPermission(address requester, address owner);
|
||||||
error PositionNotFound();
|
error PositionNotFound();
|
||||||
|
|
||||||
|
|
||||||
struct StakingPosition {
|
struct StakingPosition {
|
||||||
uint256 share;
|
uint256 share;
|
||||||
address owner;
|
address owner;
|
||||||
uint32 creationTime;
|
uint32 creationTime;
|
||||||
uint32 lastTaxTime;
|
uint32 lastTaxTime;
|
||||||
uint32 taxRate; // e.g. value of 60 = 60% tax per year
|
uint32 taxRate; // e.g. value of 60 = 60% tax per year
|
||||||
}
|
}
|
||||||
|
|
||||||
uint256 public immutable totalSupply;
|
uint256 public immutable totalSupply;
|
||||||
|
|
@ -41,23 +41,20 @@ contract Stake is IStake {
|
||||||
uint256 public outstandingStake;
|
uint256 public outstandingStake;
|
||||||
uint256 private lastTokenId;
|
uint256 private lastTokenId;
|
||||||
uint256 public minStake;
|
uint256 public minStake;
|
||||||
mapping (uint256 positionID => StakingPosition) public positions;
|
mapping(uint256 positionID => StakingPosition) public positions;
|
||||||
|
|
||||||
|
constructor(address _tokenContract) {
|
||||||
constructor(
|
|
||||||
address _tokenContract
|
|
||||||
) {
|
|
||||||
tokenContract = IERC20Metadata(_tokenContract);
|
tokenContract = IERC20Metadata(_tokenContract);
|
||||||
|
|
||||||
totalSupply = 10**(tokenContract.decimals() + DECIMAL_OFFSET);
|
totalSupply = 10 ** (tokenContract.decimals() + DECIMAL_OFFSET);
|
||||||
taxPool = Harb(_tokenContract).TAX_POOL();
|
taxPool = Harb(_tokenContract).TAX_POOL();
|
||||||
}
|
}
|
||||||
|
|
||||||
function dormantSupply() public view override returns(uint256) {
|
function dormantSupply() public view override returns (uint256) {
|
||||||
return totalSupply * (100 - MAX_STAKE) / 100;
|
return totalSupply * (100 - MAX_STAKE) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
function authorizedStake() private view returns(uint256) {
|
function authorizedStake() private view returns (uint256) {
|
||||||
return totalSupply * MAX_STAKE / 100;
|
return totalSupply * MAX_STAKE / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,16 +68,17 @@ contract Stake is IStake {
|
||||||
return shares.mulDiv(tokenContract.totalSupply(), totalSupply, rounding);
|
return shares.mulDiv(tokenContract.totalSupply(), totalSupply, rounding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO: deal with metatransactions: While these are generally available
|
||||||
* TODO: deal with metatransactions: While these are generally available
|
|
||||||
* via msg.sender and msg.data, they should not be accessed in such a direct
|
* 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
|
* 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
|
* paying for execution may not be the actual sender (as far as an application
|
||||||
* is concerned).
|
* is concerned).
|
||||||
*/
|
*/
|
||||||
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) public returns(uint256) {
|
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch)
|
||||||
|
public
|
||||||
|
returns (uint256)
|
||||||
|
{
|
||||||
// check lower boundary
|
// check lower boundary
|
||||||
uint256 sharesWanted = assetsToShares(assets, Math.Rounding.Down);
|
uint256 sharesWanted = assetsToShares(assets, Math.Rounding.Down);
|
||||||
if (sharesWanted < minStake) {
|
if (sharesWanted < minStake) {
|
||||||
|
|
@ -88,10 +86,10 @@ contract Stake is IStake {
|
||||||
}
|
}
|
||||||
|
|
||||||
// run through all suggested positions to snatch
|
// run through all suggested positions to snatch
|
||||||
for (uint i = 0; i < positionsToSnatch.length; i++) {
|
for (uint256 i = 0; i < positionsToSnatch.length; i++) {
|
||||||
StakingPosition storage pos = positions[positionsToSnatch[i]];
|
StakingPosition storage pos = positions[positionsToSnatch[i]];
|
||||||
if (pos.creationTime == 0) {
|
if (pos.creationTime == 0) {
|
||||||
//TODO:
|
//TODO:
|
||||||
revert PositionNotFound();
|
revert PositionNotFound();
|
||||||
}
|
}
|
||||||
// check that tax lower
|
// check that tax lower
|
||||||
|
|
@ -103,7 +101,7 @@ contract Stake is IStake {
|
||||||
_payTax(pos, 0);
|
_payTax(pos, 0);
|
||||||
_exitPosition(pos);
|
_exitPosition(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to make a new position in the free space and hope it is big enough
|
// try to make a new position in the free space and hope it is big enough
|
||||||
uint256 availableStake = authorizedStake() - outstandingStake;
|
uint256 availableStake = authorizedStake() - outstandingStake;
|
||||||
if (sharesWanted > availableStake) {
|
if (sharesWanted > availableStake) {
|
||||||
|
|
@ -126,17 +124,16 @@ contract Stake is IStake {
|
||||||
return lastTokenId;
|
return lastTokenId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function exitPosition(uint256 positionID) public {
|
function exitPosition(uint256 positionID) public {
|
||||||
StakingPosition storage pos = positions[positionID];
|
StakingPosition storage pos = positions[positionID];
|
||||||
if(pos.owner != msg.sender) {
|
if (pos.owner != msg.sender) {
|
||||||
revert NoPermission(msg.sender, pos.owner);
|
revert NoPermission(msg.sender, pos.owner);
|
||||||
}
|
}
|
||||||
// to prevent snatch-and-exit grieving attack, pay TAX_FLOOR_DURATION
|
// to prevent snatch-and-exit grieving attack, pay TAX_FLOOR_DURATION
|
||||||
_payTax(pos, TAX_FLOOR_DURATION);
|
_payTax(pos, TAX_FLOOR_DURATION);
|
||||||
_exitPosition(pos);
|
_exitPosition(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
function payTax(uint256 positionID) public {
|
function payTax(uint256 positionID) public {
|
||||||
StakingPosition storage pos = positions[positionID];
|
StakingPosition storage pos = positions[positionID];
|
||||||
// TODO: what if someone calls payTax and exitPosition in the same transaction?
|
// TODO: what if someone calls payTax and exitPosition in the same transaction?
|
||||||
|
|
@ -146,16 +143,19 @@ contract Stake is IStake {
|
||||||
function taxDue(uint256 positionID, uint256 taxFloorDuration) public view returns (uint256 amountDue) {
|
function taxDue(uint256 positionID, uint256 taxFloorDuration) public view returns (uint256 amountDue) {
|
||||||
StakingPosition storage pos = positions[positionID];
|
StakingPosition storage pos = positions[positionID];
|
||||||
// ihet = Implied Holding Expiry Timestamp
|
// ihet = Implied Holding Expiry Timestamp
|
||||||
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp;
|
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration)
|
||||||
|
? pos.creationTime + taxFloorDuration
|
||||||
|
: block.timestamp;
|
||||||
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
||||||
uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down);
|
uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down);
|
||||||
amountDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
amountDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function _payTax(StakingPosition storage pos, uint256 taxFloorDuration) private {
|
function _payTax(StakingPosition storage pos, uint256 taxFloorDuration) private {
|
||||||
// ihet = Implied Holding Expiry Timestamp
|
// ihet = Implied Holding Expiry Timestamp
|
||||||
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp;
|
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration)
|
||||||
|
? pos.creationTime + taxFloorDuration
|
||||||
|
: block.timestamp;
|
||||||
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
||||||
uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down);
|
uint256 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down);
|
||||||
uint256 taxAmountDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
uint256 taxAmountDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
||||||
|
|
@ -185,5 +185,4 @@ contract Stake is IStake {
|
||||||
delete pos.creationTime;
|
delete pos.creationTime;
|
||||||
SafeERC20.safeTransfer(tokenContract, owner, assets);
|
SafeERC20.safeTransfer(tokenContract, owner, assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
pragma solidity ^0.8.13;
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
interface IStake {
|
interface IStake {
|
||||||
|
function dormantSupply() external view returns (uint256);
|
||||||
function dormantSupply() external view returns(uint256);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
pragma solidity >=0.4.0;
|
|
||||||
|
|
||||||
/// @title FixedPoint128
|
|
||||||
/// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format)
|
|
||||||
library FixedPoint128 {
|
|
||||||
uint256 internal constant Q128 = 0x100000000000000000000000000000000;
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
pragma solidity >=0.4.0;
|
|
||||||
|
|
||||||
/// @title FixedPoint96
|
|
||||||
/// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format)
|
|
||||||
/// @dev Used in SqrtPriceMath.sol
|
|
||||||
library FixedPoint96 {
|
|
||||||
uint8 internal constant RESOLUTION = 96;
|
|
||||||
uint256 internal constant Q96 = 0x1000000000000000000000000;
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
pragma solidity >=0.5.0;
|
|
||||||
|
|
||||||
library PositionKey {
|
|
||||||
/// @dev Returns the key of the position in the core library
|
|
||||||
function compute(
|
|
||||||
address owner,
|
|
||||||
int24 tickLower,
|
|
||||||
int24 tickUpper
|
|
||||||
) internal pure returns (bytes32) {
|
|
||||||
return keccak256(abi.encodePacked(owner, tickLower, tickUpper));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@ pragma solidity ^0.8.13;
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "forge-std/Test.sol";
|
||||||
import "forge-std/console.sol";
|
import "forge-std/console.sol";
|
||||||
import { TwabController } from "pt-v5-twab-controller/TwabController.sol";
|
import {TwabController} from "pt-v5-twab-controller/TwabController.sol";
|
||||||
import "../src/Harb.sol";
|
import "../src/Harb.sol";
|
||||||
import "../src/Stake.sol";
|
import "../src/Stake.sol";
|
||||||
|
|
||||||
|
|
@ -12,8 +12,7 @@ contract HarbTest is Test {
|
||||||
Stake public stake;
|
Stake public stake;
|
||||||
|
|
||||||
function setUp() public {
|
function setUp() public {
|
||||||
|
TwabController tc = new TwabController(60 * 60 * 24, uint32(block.timestamp));
|
||||||
TwabController tc = new TwabController(60*60*24, uint32(block.timestamp));
|
|
||||||
|
|
||||||
harb = new Harb("HARB", "HARB", tc);
|
harb = new Harb("HARB", "HARB", tc);
|
||||||
stake = new Stake(address(harb));
|
stake = new Stake(address(harb));
|
||||||
|
|
@ -21,15 +20,15 @@ contract HarbTest is Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_MintStakeUnstake(address account, uint256 amount) public {
|
function test_MintStakeUnstake(address account, uint256 amount) public {
|
||||||
vm.assume(amount > 10000);
|
vm.assume(amount > 10000);
|
||||||
vm.assume(amount < 2**93); // TWAB limit = 2**96
|
vm.assume(amount < 2 ** 93); // TWAB limit = 2**96
|
||||||
vm.assume(account != address(0));
|
vm.assume(account != address(0));
|
||||||
vm.assume(account != address(1)); // TWAB sponsorship address
|
vm.assume(account != address(1)); // TWAB sponsorship address
|
||||||
vm.assume(account != address(2)); // tax pool address
|
vm.assume(account != address(2)); // tax pool address
|
||||||
vm.assume(account != address(harb));
|
vm.assume(account != address(harb));
|
||||||
vm.assume(account != address(stake));
|
vm.assume(account != address(stake));
|
||||||
|
|
||||||
// test mint
|
// test mint
|
||||||
uint256 totalSupplyBefore = harb.totalSupply();
|
uint256 totalSupplyBefore = harb.totalSupply();
|
||||||
uint256 balanceBefore = harb.balanceOf(account);
|
uint256 balanceBefore = harb.balanceOf(account);
|
||||||
harb.setLiquidityManager(account);
|
harb.setLiquidityManager(account);
|
||||||
|
|
@ -47,17 +46,17 @@ contract HarbTest is Test {
|
||||||
vm.prank(account);
|
vm.prank(account);
|
||||||
harb.approve(address(stake), amount);
|
harb.approve(address(stake), amount);
|
||||||
uint256[] memory empty;
|
uint256[] memory empty;
|
||||||
vm.prank(account);
|
vm.prank(account);
|
||||||
stake.snatch(amount, account, 1, empty);
|
stake.snatch(amount, account, 1, empty);
|
||||||
assertEq(harb.totalSupply(), totalAfter * 5, "total supply should match after stake");
|
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(account), amount * 4, "balance should match after stake");
|
||||||
assertEq(harb.balanceOf(address(stake)), amount, "balance should match after stake");
|
assertEq(harb.balanceOf(address(stake)), amount, "balance should match after stake");
|
||||||
(uint256 share, address owner, uint32 creationTime, uint32 lastTaxTime, uint32 taxRate) = stake.positions(0);
|
(uint256 share, address owner, uint32 creationTime, uint32 lastTaxTime, uint32 taxRate) = stake.positions(0);
|
||||||
assertEq(share, stake.totalSupply() / 5, "share should match");
|
assertEq(share, stake.totalSupply() / 5, "share should match");
|
||||||
assertEq(owner, account, "owners should match");
|
assertEq(owner, account, "owners should match");
|
||||||
assertEq(taxRate, 1, "tax rate should match");
|
assertEq(taxRate, 1, "tax rate should match");
|
||||||
}
|
}
|
||||||
|
|
||||||
// test unstake
|
// test unstake
|
||||||
{
|
{
|
||||||
uint256 timeBefore = block.timestamp;
|
uint256 timeBefore = block.timestamp;
|
||||||
|
|
@ -65,10 +64,9 @@ contract HarbTest is Test {
|
||||||
uint256 taxDue = stake.taxDue(0, 60 * 60 * 24 * 3);
|
uint256 taxDue = stake.taxDue(0, 60 * 60 * 24 * 3);
|
||||||
console.logUint(taxDue);
|
console.logUint(taxDue);
|
||||||
console.log("tax due :%i", taxDue);
|
console.log("tax due :%i", taxDue);
|
||||||
vm.prank(account);
|
vm.prank(account);
|
||||||
stake.exitPosition(0);
|
stake.exitPosition(0);
|
||||||
assertApproxEqRel(harb.balanceOf(account), amount * 5 - taxDue, 1e15, "balance should match");
|
assertApproxEqRel(harb.balanceOf(account), amount * 5 - taxDue, 1e14, "balance should match");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue