From 06581a0b8df7dcca551924340bd6e9984fcb9a1a Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Tue, 12 Mar 2024 20:22:10 +0100 Subject: [PATCH] first deployment --- onchain/.gitignore | 3 + onchain/README.md | 32 +++++- onchain/remappings.txt | 1 + onchain/script/Counter.s.sol | 12 --- onchain/script/Deploy.sol | 8 +- onchain/src/Harb.sol | 33 +++--- onchain/src/LiquidityManager.sol | 174 +++++++++++++----------------- onchain/src/Stake.sol | 61 ++++++----- onchain/src/interfaces/IStake.sol | 6 +- onchain/src/lib/FixedPoint128.sol | 8 -- onchain/src/lib/FixedPoint96.sol | 10 -- onchain/src/lib/PositionKey.sol | 13 --- onchain/test/Harb.t.sol | 44 ++++---- 13 files changed, 180 insertions(+), 225 deletions(-) delete mode 100644 onchain/script/Counter.s.sol delete mode 100644 onchain/src/lib/FixedPoint128.sol delete mode 100644 onchain/src/lib/FixedPoint96.sol delete mode 100644 onchain/src/lib/PositionKey.sol diff --git a/onchain/.gitignore b/onchain/.gitignore index 85198aa..e4f1ab3 100644 --- a/onchain/.gitignore +++ b/onchain/.gitignore @@ -12,3 +12,6 @@ docs/ # Dotenv file .env +.secret + +/broadcast/ \ No newline at end of file diff --git a/onchain/README.md b/onchain/README.md index 9265b45..30633ab 100644 --- a/onchain/README.md +++ b/onchain/README.md @@ -15,6 +15,16 @@ https://book.getfoundry.sh/ ## Usage +### Install + +```shell +$ git clone +$ git submodule init +$ git submodule update +$ cd lib/uni-v3-lib +$ yarn +``` + ### Build ```shell @@ -48,7 +58,8 @@ $ anvil ### Deploy ```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +source .env +forge script script/Deploy.sol:SepoliaScript --broadcast --verify --rpc-url ${SEPOLIA_RPC_URL} ``` ### Cast @@ -64,3 +75,22 @@ $ forge --help $ anvil --help $ cast --help ``` + + +## Deployment on Sepolia + +### Harb + +address: 0x275403401f9c6f4659b6ffb6ab01798e1de9a912 +abi: +``` +[{"type":"constructor","inputs":[{"name":"name_","type":"string","internalType":"string"},{"name":"symbol_","type":"string","internalType":"string"},{"name":"twabController_","type":"address","internalType":"contract TwabController"}],"stateMutability":"nonpayable"},{"type":"function","name":"DOMAIN_SEPARATOR","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"TAX_POOL","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"allowance","inputs":[{"name":"owner","type":"address","internalType":"address"},{"name":"spender","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"approve","inputs":[{"name":"spender","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"balanceOf","inputs":[{"name":"_account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"burn","inputs":[{"name":"_amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"decimals","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"type":"function","name":"decreaseAllowance","inputs":[{"name":"spender","type":"address","internalType":"address"},{"name":"subtractedValue","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"eip712Domain","inputs":[],"outputs":[{"name":"fields","type":"bytes1","internalType":"bytes1"},{"name":"name","type":"string","internalType":"string"},{"name":"version","type":"string","internalType":"string"},{"name":"chainId","type":"uint256","internalType":"uint256"},{"name":"verifyingContract","type":"address","internalType":"address"},{"name":"salt","type":"bytes32","internalType":"bytes32"},{"name":"extensions","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"increaseAllowance","inputs":[{"name":"spender","type":"address","internalType":"address"},{"name":"addedValue","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"liquidityManager","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"mint","inputs":[{"name":"_amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"name","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"nonces","inputs":[{"name":"owner","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"permit","inputs":[{"name":"owner","type":"address","internalType":"address"},{"name":"spender","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setLiquidityManager","inputs":[{"name":"liquidityManager_","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStakingPool","inputs":[{"name":"stakingPool_","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"stakingPool","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"totalSupply","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"transfer","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"transferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"twabController","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract TwabController"}],"stateMutability":"view"},{"type":"event","name":"Approval","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"spender","type":"address","indexed":true,"internalType":"address"},{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"EIP712DomainChanged","inputs":[],"anonymous":false},{"type":"event","name":"Transfer","inputs":[{"name":"from","type":"address","indexed":true,"internalType":"address"},{"name":"to","type":"address","indexed":true,"internalType":"address"},{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"InvalidShortString","inputs":[]},{"type":"error","name":"StringTooLong","inputs":[{"name":"str","type":"string","internalType":"string"}]},{"type":"error","name":"ZeroAddressInConstructor","inputs":[]}] +``` + +### Stake + +address: 0xe7cba9fc61c53176527d4bd1d5868f9c0ae3d8e1 +abi: +``` +[{"type":"constructor","inputs":[{"name":"_tokenContract","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"dormantSupply","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"exitPosition","inputs":[{"name":"positionID","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"minStake","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"outstandingStake","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"payTax","inputs":[{"name":"positionID","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"positions","inputs":[{"name":"positionID","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"share","type":"uint256","internalType":"uint256"},{"name":"owner","type":"address","internalType":"address"},{"name":"creationTime","type":"uint32","internalType":"uint32"},{"name":"lastTaxTime","type":"uint32","internalType":"uint32"},{"name":"taxRate","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"snatch","inputs":[{"name":"assets","type":"uint256","internalType":"uint256"},{"name":"receiver","type":"address","internalType":"address"},{"name":"taxRate","type":"uint32","internalType":"uint32"},{"name":"positionsToSnatch","type":"uint256[]","internalType":"uint256[]"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"taxDue","inputs":[{"name":"positionID","type":"uint256","internalType":"uint256"},{"name":"taxFloorDuration","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"amountDue","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"totalSupply","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"error","name":"ExceededAvailableStake","inputs":[{"name":"receiver","type":"address","internalType":"address"},{"name":"stakeWanted","type":"uint256","internalType":"uint256"},{"name":"availableStake","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"NoPermission","inputs":[{"name":"requester","type":"address","internalType":"address"},{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"PositionNotFound","inputs":[]},{"type":"error","name":"SharesTooLow","inputs":[{"name":"receiver","type":"address","internalType":"address"},{"name":"assets","type":"uint256","internalType":"uint256"},{"name":"sharesWanted","type":"uint256","internalType":"uint256"},{"name":"minStake","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"TaxTooLow","inputs":[{"name":"receiver","type":"address","internalType":"address"},{"name":"taxRateWanted","type":"uint64","internalType":"uint64"},{"name":"taxRateMet","type":"uint64","internalType":"uint64"},{"name":"positionId","type":"uint256","internalType":"uint256"}]}] +``` \ No newline at end of file diff --git a/onchain/remappings.txt b/onchain/remappings.txt index 5ea0f2a..90b1bc0 100644 --- a/onchain/remappings.txt +++ b/onchain/remappings.txt @@ -1,3 +1,4 @@ @openzeppelin/=lib/openzeppelin-contracts/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/ diff --git a/onchain/script/Counter.s.sol b/onchain/script/Counter.s.sol deleted file mode 100644 index df9ee8b..0000000 --- a/onchain/script/Counter.s.sol +++ /dev/null @@ -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(); - } -} diff --git a/onchain/script/Deploy.sol b/onchain/script/Deploy.sol index bbfcb37..3909685 100644 --- a/onchain/script/Deploy.sol +++ b/onchain/script/Deploy.sol @@ -1,11 +1,11 @@ pragma solidity ^0.8.4; 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/Stake.sol"; -contract GoerliScript is Script { +contract SepoliaScript is Script { function setUp() public {} function run() public { @@ -13,11 +13,11 @@ contract GoerliScript is Script { uint256 privateKey = vm.deriveKey(seedPhrase, 0); 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); Stake stake = new Stake(address(harb)); harb.setStakingPool(address(stake)); vm.stopBroadcast(); } -} \ No newline at end of file +} diff --git a/onchain/src/Harb.sol b/onchain/src/Harb.sol index 6591175..6e6a4b0 100644 --- a/onchain/src/Harb.sol +++ b/onchain/src/Harb.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.19; -import { ERC20, IERC20, IERC20Metadata } from "@openzeppelin/token/ERC20/ERC20.sol"; -import { ERC20Permit, IERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; -import { SafeCast } from "@openzeppelin/utils/math/SafeCast.sol"; -import { IStake } from "./interfaces/IStake.sol"; -import { TwabController } from "pt-v5-twab-controller/TwabController.sol"; +import {ERC20, IERC20, IERC20Metadata} from "@openzeppelin/token/ERC20/ERC20.sol"; +import {ERC20Permit, IERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; +import {SafeCast} from "@openzeppelin/utils/math/SafeCast.sol"; +import {IStake} from "./interfaces/IStake.sol"; +import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; /** * @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. */ contract Harb is ERC20, ERC20Permit { - address public constant TAX_POOL = address(2); /* ============ Public Variables ============ */ @@ -47,11 +46,10 @@ contract Harb is ERC20, ERC20Permit { * @param name_ The name of the token * @param symbol_ The token symbol */ - constructor( - string memory name_, - string memory symbol_, - TwabController twabController_ - ) ERC20(name_, symbol_) ERC20Permit(name_) { + constructor(string memory name_, string memory symbol_, TwabController twabController_) + ERC20(name_, symbol_) + ERC20Permit(name_) + { if (address(0) == address(twabController_)) revert ZeroAddressInConstructor(); twabController = twabController_; } @@ -87,9 +85,7 @@ contract Harb is ERC20, ERC20Permit { /* ============ Public ERC20 Overrides ============ */ /// @inheritdoc ERC20 - function balanceOf( - address _account - ) public view virtual override(ERC20) returns (uint256) { + function balanceOf(address _account) public view virtual override(ERC20) returns (uint256) { return twabController.balanceOf(address(this), _account); } @@ -98,8 +94,6 @@ contract Harb is ERC20, ERC20Permit { return twabController.totalSupply(address(this)); } - - /* ============ Internal ERC20 Overrides ============ */ /** @@ -115,8 +109,8 @@ contract Harb is ERC20, ERC20Permit { uint256 activeSupply = totalSupply() - stakingPoolBalance; uint256 dormantStake = IStake(stakingPool).dormantSupply(); if (stakingPoolBalance > 0) { - uint256 newStake = stakingPoolBalance * amount / (activeSupply + dormantStake); - _mint(stakingPool, newStake); + uint256 newStake = stakingPoolBalance * amount / (activeSupply + dormantStake); + _mint(stakingPool, newStake); } twabController.mint(receiver, SafeCast.toUint96(amount)); @@ -151,5 +145,4 @@ contract Harb is ERC20, ERC20Permit { twabController.transfer(_from, _to, SafeCast.toUint96(_amount)); emit Transfer(_from, _to, _amount); } - -} \ No newline at end of file +} diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index cefb2ff..b319740 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -1,22 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.20; -import './lib/PositionKey.sol'; -import './lib/FixedPoint128.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 '@uniswap-v3-core/interfaces/IUniswapV3Pool.sol'; -import '@openzeppelin/token/ERC20/IERC20.sol'; +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 $bloodX ecosystem. It - * protects the communities liquidity while allowing a manager role to + * @title LiquidityManager - A contract that supports the $bloodX 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); @@ -63,7 +62,7 @@ contract LiquidityManager { mapping(bytes26 => TokenPosition) private _positions; 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 event DecreaseLiquidity(address indexed token, uint128 liquidity, uint256 amount0, uint256 amount1); - constructor( - address _factory, - address _WETH9 - ) { + constructor(address _factory, address _WETH9) { factory = _factory; WETH9 = _WETH9; - } 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 - returns ( - uint128 liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128 - ) + function positions(address token, int24 tickLower, int24 tickUpper) + external + view + returns (uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) { TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)]; - return ( - position.liquidity, - position.feeGrowthInside0LastX128, - position.feeGrowthInside1LastX128 - ); + return (position.liquidity, position.feeGrowthInside0LastX128, position.feeGrowthInside1LastX128); } - function uniswapV3MintCallback( - uint256 amount0Owed, - uint256 amount1Owed, - bytes calldata data - ) external { + function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata data) external { PoolKey memory poolKey = abi.decode(data, (PoolKey)); CallbackValidation.verifyCallback(factory, poolKey); @@ -147,62 +132,57 @@ contract LiquidityManager { // compute the liquidity amount uint128 liquidity; { - (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); + (uint160 sqrtPriceX96,,,,,,) = pool.slot0(); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper); liquidity = LiquidityAmounts.getLiquidityForAmounts( - sqrtPriceX96, - sqrtRatioAX96, - sqrtRatioBX96, - params.amount0Desired, - params.amount1Desired + sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, params.amount0Desired, params.amount1Desired ); } (bool token0isWeth, address token) = getToken(params.token0, params.token1); { - (uint256 amount0, uint256 amount1) = pool.mint( - address(this), - params.tickLower, - params.tickUpper, - liquidity, - abi.encode(poolKey) - ); + (uint256 amount0, uint256 amount1) = + pool.mint(address(this), 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); } bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper); // 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)]; if (liquidity == 0) { // create entry - position = TokenPosition({ + position = TokenPosition({ liquidity: liquidity, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128 }); } else { // update entry - updateFeesOwed(token0isWeth, token, uint128( - FullMath.mulDiv( - feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, - position.liquidity, - FixedPoint128.Q128 + updateFeesOwed( + token0isWeth, + token, + uint128( + 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.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; @@ -227,27 +207,28 @@ contract LiquidityManager { (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); // 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) + - uint128( - FullMath.mulDiv( - feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, - positionLiquidity, - FixedPoint128.Q128 + updateFeesOwed( + token0isWeth, + token, + uint128(amount0) + + uint128( + 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.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; @@ -257,36 +238,35 @@ contract LiquidityManager { emit DecreaseLiquidity(token, params.liquidity, amount0, amount1); } - // 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 + //////// + // - 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 rebalance(address token, int24 tickLower, int24 tickUpper) external { // bool ETH_TOKEN_ZERO = WETH9 < token; @@ -335,9 +315,5 @@ contract LiquidityManager { // ); // } - - - - // } -} \ No newline at end of file +} diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index b714cc5..ef15dbb 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -2,9 +2,9 @@ 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 { 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 "./interfaces/IStake.sol"; import "./Harb.sol"; @@ -13,26 +13,26 @@ contract Stake is IStake { using Math for uint256; uint256 internal DECIMAL_OFFSET = 5 + 2; - 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_STAKE = 20; // 20% of HARB supply + uint256 internal constant MAX_TAX = 1000; // max 1000% tax per year 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. /** * @dev Attempted to deposit more assets than the max amount for `receiver`. */ + error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake); error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId); error SharesTooLow(address receiver, uint256 assets, uint256 sharesWanted, uint256 minStake); error NoPermission(address requester, address owner); error PositionNotFound(); - struct StakingPosition { uint256 share; address owner; uint32 creationTime; 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; @@ -41,23 +41,20 @@ contract Stake is IStake { uint256 public outstandingStake; uint256 private lastTokenId; uint256 public minStake; - mapping (uint256 positionID => StakingPosition) public positions; + mapping(uint256 positionID => StakingPosition) public positions; - - constructor( - address _tokenContract - ) { + constructor(address _tokenContract) { tokenContract = IERC20Metadata(_tokenContract); - - totalSupply = 10**(tokenContract.decimals() + DECIMAL_OFFSET); + + totalSupply = 10 ** (tokenContract.decimals() + DECIMAL_OFFSET); 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; } - function authorizedStake() private view returns(uint256) { + function authorizedStake() private view returns (uint256) { return totalSupply * MAX_STAKE / 100; } @@ -71,16 +68,17 @@ contract Stake is IStake { 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 * 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). */ - 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 uint256 sharesWanted = assetsToShares(assets, Math.Rounding.Down); if (sharesWanted < minStake) { @@ -88,10 +86,10 @@ contract Stake is IStake { } // 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]]; if (pos.creationTime == 0) { - //TODO: + //TODO: revert PositionNotFound(); } // check that tax lower @@ -103,7 +101,7 @@ contract Stake is IStake { _payTax(pos, 0); _exitPosition(pos); } - + // try to make a new position in the free space and hope it is big enough uint256 availableStake = authorizedStake() - outstandingStake; if (sharesWanted > availableStake) { @@ -126,17 +124,16 @@ contract Stake is IStake { return lastTokenId; } - function exitPosition(uint256 positionID) public { StakingPosition storage pos = positions[positionID]; - if(pos.owner != msg.sender) { + if (pos.owner != msg.sender) { revert NoPermission(msg.sender, pos.owner); } // to prevent snatch-and-exit grieving attack, pay TAX_FLOOR_DURATION _payTax(pos, TAX_FLOOR_DURATION); - _exitPosition(pos); + _exitPosition(pos); } - + function payTax(uint256 positionID) public { StakingPosition storage pos = positions[positionID]; // 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) { StakingPosition storage pos = positions[positionID]; // 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 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down); amountDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE; } - function _payTax(StakingPosition storage pos, uint256 taxFloorDuration) private { // 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 assetsBefore = sharesToAssets(pos.share, Math.Rounding.Down); uint256 taxAmountDue = assetsBefore * pos.taxRate * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE; @@ -185,5 +185,4 @@ contract Stake is IStake { delete pos.creationTime; SafeERC20.safeTransfer(tokenContract, owner, assets); } - } diff --git a/onchain/src/interfaces/IStake.sol b/onchain/src/interfaces/IStake.sol index 2866dcd..59b2e8f 100644 --- a/onchain/src/interfaces/IStake.sol +++ b/onchain/src/interfaces/IStake.sol @@ -2,8 +2,6 @@ pragma solidity ^0.8.13; -interface IStake { - - function dormantSupply() external view returns(uint256); - +interface IStake { + function dormantSupply() external view returns (uint256); } diff --git a/onchain/src/lib/FixedPoint128.sol b/onchain/src/lib/FixedPoint128.sol deleted file mode 100644 index 6d6948b..0000000 --- a/onchain/src/lib/FixedPoint128.sol +++ /dev/null @@ -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; -} diff --git a/onchain/src/lib/FixedPoint96.sol b/onchain/src/lib/FixedPoint96.sol deleted file mode 100644 index 63b42c2..0000000 --- a/onchain/src/lib/FixedPoint96.sol +++ /dev/null @@ -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; -} diff --git a/onchain/src/lib/PositionKey.sol b/onchain/src/lib/PositionKey.sol deleted file mode 100644 index a60fc18..0000000 --- a/onchain/src/lib/PositionKey.sol +++ /dev/null @@ -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)); - } -} diff --git a/onchain/test/Harb.t.sol b/onchain/test/Harb.t.sol index 7cc3992..38b35e2 100644 --- a/onchain/test/Harb.t.sol +++ b/onchain/test/Harb.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import { TwabController } from "pt-v5-twab-controller/TwabController.sol"; +import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; import "../src/Harb.sol"; import "../src/Stake.sol"; @@ -12,8 +12,7 @@ contract HarbTest is Test { Stake public stake; 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); stake = new Stake(address(harb)); @@ -21,15 +20,15 @@ contract HarbTest is Test { } function test_MintStakeUnstake(address account, uint256 amount) public { - vm.assume(amount > 10000); - vm.assume(amount < 2**93); // TWAB limit = 2**96 - vm.assume(account != address(0)); - vm.assume(account != address(1)); // TWAB sponsorship address - vm.assume(account != address(2)); // tax pool address - vm.assume(account != address(harb)); - vm.assume(account != address(stake)); + vm.assume(amount > 10000); + vm.assume(amount < 2 ** 93); // TWAB limit = 2**96 + vm.assume(account != address(0)); + vm.assume(account != address(1)); // TWAB sponsorship address + vm.assume(account != address(2)); // tax pool address + vm.assume(account != address(harb)); + vm.assume(account != address(stake)); - // test mint + // test mint uint256 totalSupplyBefore = harb.totalSupply(); uint256 balanceBefore = harb.balanceOf(account); harb.setLiquidityManager(account); @@ -47,17 +46,17 @@ contract HarbTest is Test { vm.prank(account); harb.approve(address(stake), amount); uint256[] memory empty; - vm.prank(account); - stake.snatch(amount, account, 1, empty); - assertEq(harb.totalSupply(), totalAfter * 5, "total supply should match after stake"); - assertEq(harb.balanceOf(account), amount * 4, "balance should match after stake"); + vm.prank(account); + stake.snatch(amount, account, 1, empty); + assertEq(harb.totalSupply(), totalAfter * 5, "total supply should match after stake"); + assertEq(harb.balanceOf(account), amount * 4, "balance should match after stake"); assertEq(harb.balanceOf(address(stake)), amount, "balance should match after stake"); (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(taxRate, 1, "tax rate should match"); - } - + } + // test unstake { uint256 timeBefore = block.timestamp; @@ -65,10 +64,9 @@ contract HarbTest is Test { uint256 taxDue = stake.taxDue(0, 60 * 60 * 24 * 3); console.logUint(taxDue); console.log("tax due :%i", taxDue); - vm.prank(account); - stake.exitPosition(0); - assertApproxEqRel(harb.balanceOf(account), amount * 5 - taxDue, 1e15, "balance should match"); - } + vm.prank(account); + stake.exitPosition(0); + assertApproxEqRel(harb.balanceOf(account), amount * 5 - taxDue, 1e14, "balance should match"); + } } - }