first deployment

This commit is contained in:
JulesCrown 2024-03-12 20:22:10 +01:00
parent 9279e0c045
commit 06581a0b8d
13 changed files with 180 additions and 225 deletions

3
onchain/.gitignore vendored
View file

@ -12,3 +12,6 @@ docs/
# Dotenv file # Dotenv file
.env .env
.secret
/broadcast/

File diff suppressed because one or more lines are too long

View file

@ -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/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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