diff --git a/onchain/script/DeployScript.sol b/onchain/script/DeployScript.sol index 693a855..4e6e482 100644 --- a/onchain/script/DeployScript.sol +++ b/onchain/script/DeployScript.sol @@ -5,7 +5,7 @@ import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "../src/Harberg.sol"; import "../src/Stake.sol"; -import "../src/Sentimenter.sol"; +import "../src/Optimizer.sol"; import "../src/helpers/UniswapHelpers.sol"; import {LiquidityManager} from "../src/LiquidityManager.sol"; import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; @@ -34,9 +34,9 @@ contract DeployScript is Script { IUniswapV3Factory factory = IUniswapV3Factory(v3Factory); address liquidityPool = factory.createPool(weth, address(harb), FEE); IUniswapV3Pool(liquidityPool).initializePoolFor1Cent(token0isWeth); - Sentimenter sentimenter = new Sentimenter(); + Optimizer optimizer = new Optimizer(); bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(harb),address(stake)); - ERC1967Proxy proxy = new ERC1967Proxy(address(sentimenter), params); + ERC1967Proxy proxy = new ERC1967Proxy(address(optimizer), params); LiquidityManager liquidityManager = new LiquidityManager(v3Factory, weth, address(harb), address(proxy)); liquidityManager.setFeeDestination(feeDest); // note: this delayed initialization is not a security issue. diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 1654490..2fd4605 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -14,7 +14,7 @@ import {Math} from "@openzeppelin/utils/math/Math.sol"; import {ABDKMath64x64} from "@abdk/ABDKMath64x64.sol"; import "./interfaces/IWETH9.sol"; import {Harberg} from "./Harberg.sol"; -import {Sentimenter} from "./Sentimenter.sol"; +import {Optimizer} from "./Optimizer.sol"; /** * @title LiquidityManager for Harberg Token on Uniswap V3 @@ -33,13 +33,11 @@ contract LiquidityManager { uint256 public cumulativeVolume; // the minimum granularity of liquidity positions in the Uniswap V3 pool. this is a 1% pool. int24 internal constant TICK_SPACING = 200; - // defines the width of the anchor position from the current price to discovery position. - int24 internal constant ANCHOR_SPACING = 5 * TICK_SPACING; // DISCOVERY_SPACING determines the range above the current price where new tokens are minted and sold. // 11000 ticks represent 3x the current price int24 internal constant DISCOVERY_SPACING = 11000; // how much more liquidity per tick discovery is holding over anchor - uint128 internal constant DISCOVERY_DEPTH = 200; // 500 // 500% + uint128 internal constant MIN_DISCOVERY_DEPTH = 200; // 500 // 500% // only working with UNI V3 1% fee tier pools uint24 internal constant FEE = uint24(10_000); // used to double-check price with uni oracle @@ -50,7 +48,7 @@ contract LiquidityManager { address private immutable factory; IWETH9 private immutable weth; Harberg private immutable harb; - Sentimenter private immutable sentimenter; + Optimizer private immutable optimizer; IUniswapV3Pool private immutable pool; bool private immutable token0isWeth; PoolKey private poolKey; @@ -73,8 +71,8 @@ contract LiquidityManager { error ZeroAddressInSetter(); error AddressAlreadySet(); - event EthScarcity(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 sentiment, int24 vwapTick); - event EthAbundance(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 sentiment, int24 vwapTick); + event EthScarcity(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, int24 vwapTick); + event EthAbundance(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, int24 vwapTick); /// @dev Function modifier to ensure that the caller is the feeDestination modifier onlyFeeDestination() { @@ -87,14 +85,14 @@ contract LiquidityManager { /// @param _WETH9 The address of the WETH contract for handling ETH in trades. /// @param _harb The address of the Harberg token contract. /// @dev Computes the Uniswap pool address for the Harberg-WETH pair and sets up the initial configuration for the liquidity manager. - constructor(address _factory, address _WETH9, address _harb, address _sentimenter) { + constructor(address _factory, address _WETH9, address _harb, address _optimizer) { factory = _factory; weth = IWETH9(_WETH9); poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE); pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); harb = Harberg(_harb); token0isWeth = _WETH9 < _harb; - sentimenter = Sentimenter(_sentimenter); + optimizer = Optimizer(_optimizer); } /// @notice Callback function that Uniswap V3 calls for liquidity actions requiring minting or burning of tokens. @@ -203,41 +201,23 @@ contract LiquidityManager { /// @notice Internal function to set or adjust the floor, anchor, and discovery positions based on current market conditions and the manager's strategy. /// @param currentTick The current market tick. /// @dev Recalculates and realigns all liquidity positions according to the latest market data and strategic requirements. - function _set(int24 currentTick, uint256 sentiment) internal { + function _set(int24 currentTick, uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) internal { - // estimate the lower tick of the anchor - int24 vwapTick; - uint256 outstandingSupply = harb.outstandingSupply(); uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))); // this enforces an floor liquidity share of 75% to 95 %; - uint256 floorEthBalance = (3 * ethBalance / 4) + (2 * sentiment * ethBalance / 10**19); - if (outstandingSupply > 0) { - // this enables a "capital inefficiency" of 70% to 170%; - uint256 balancedCapital = (7 * outstandingSupply / 10) + (outstandingSupply * sentiment / 10**18); - vwapTick = tickAtPrice(token0isWeth, balancedCapital, floorEthBalance); - } else { - vwapTick = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - ANCHOR_SPACING; - } - - // move vwapTick below currentTick, if needed - if (token0isWeth) { - vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick; - } else { - vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick; - } + uint256 floorEthBalance = (19 * ethBalance / 20) - (2 * anchorShare * ethBalance / 10**19); // set Anchor position uint256 pulledHarb; + // this enforces a anchor range of 1% to 100% of the price + int24 anchorSpacing = TICK_SPACING + (34 * int24(anchorWidth) * TICK_SPACING / 100); { - int24 tickLower = token0isWeth ? currentTick - ANCHOR_SPACING : vwapTick; - int24 tickUpper = token0isWeth ? vwapTick : currentTick + ANCHOR_SPACING; - tickLower = tickLower / TICK_SPACING * TICK_SPACING; - tickUpper = tickUpper / TICK_SPACING * TICK_SPACING; + int24 tickLower = (currentTick - anchorSpacing) / TICK_SPACING * TICK_SPACING; + int24 tickUpper = (currentTick + anchorSpacing) / TICK_SPACING * TICK_SPACING; uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); - uint256 anchorEthBalance = ethBalance - floorEthBalance; uint128 anchorLiquidity; if (token0isWeth) { @@ -258,12 +238,13 @@ contract LiquidityManager { // set Discovery position uint256 discoveryAmount; { - int24 tickLower = token0isWeth ? currentTick - DISCOVERY_SPACING - ANCHOR_SPACING : currentTick + ANCHOR_SPACING; - int24 tickUpper = token0isWeth ? currentTick - ANCHOR_SPACING : currentTick + DISCOVERY_SPACING + ANCHOR_SPACING; + int24 tickLower = token0isWeth ? currentTick - DISCOVERY_SPACING - anchorSpacing : currentTick + anchorSpacing; + int24 tickUpper = token0isWeth ? currentTick - anchorSpacing : currentTick + DISCOVERY_SPACING + anchorSpacing; uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); - discoveryAmount = pulledHarb * uint24(DISCOVERY_SPACING) * uint24(DISCOVERY_DEPTH) / uint24(ANCHOR_SPACING) / 100; + discoveryDepth = MIN_DISCOVERY_DEPTH + (4 * discoveryDepth * MIN_DISCOVERY_DEPTH / 10**18); + discoveryAmount = pulledHarb * uint24(DISCOVERY_SPACING) * uint24(discoveryDepth) / uint24(anchorSpacing) / 100; uint128 liquidity; if (token0isWeth) { liquidity = LiquidityAmounts.getLiquidityForAmount1( @@ -280,23 +261,24 @@ contract LiquidityManager { // set Floor position { - outstandingSupply = harb.outstandingSupply(); + int24 vwapTick; + uint256 outstandingSupply = harb.outstandingSupply(); outstandingSupply -= pulledHarb; outstandingSupply -= (outstandingSupply >= discoveryAmount) ? discoveryAmount : outstandingSupply; uint256 vwapX96 = 0; uint256 requiredEthForBuyback = 0; if (cumulativeVolume > 0) { vwapX96 = cumulativeVolumeWeightedPriceX96 / cumulativeVolume; // in harb/eth - vwapX96 = (7 * vwapX96 / 10) + (vwapX96 * sentiment / 10**18); + vwapX96 = (7 * vwapX96 / 10) + (vwapX96 * capitalInefficiency / 10**18); requiredEthForBuyback = outstandingSupply.mulDiv(vwapX96, (1 << 96)); } // make a new calculation of the vwapTick, having updated outstandingSupply if (floorEthBalance < requiredEthForBuyback) { // not enough ETH, find a lower price requiredEthForBuyback = floorEthBalance; - uint256 balancedCapital = (7 * outstandingSupply / 10) + (outstandingSupply * sentiment / 10**18); + uint256 balancedCapital = (7 * outstandingSupply / 10) + (outstandingSupply * capitalInefficiency / 10**18); vwapTick = tickAtPrice(token0isWeth, balancedCapital , requiredEthForBuyback); - emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, sentiment, vwapTick); + emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, vwapTick); } else if (vwapX96 == 0) { requiredEthForBuyback = floorEthBalance; vwapTick = currentTick; @@ -305,13 +287,13 @@ contract LiquidityManager { vwapTick = tickAtPriceRatio(int128(int256(vwapX96 >> 32))); // convert to pool tick vwapTick = token0isWeth ? -vwapTick : vwapTick; - emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, sentiment, vwapTick); + emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, vwapTick); } // move floor below anchor, if needed if (token0isWeth) { - vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick; + vwapTick = (vwapTick < currentTick + anchorSpacing) ? currentTick + anchorSpacing : vwapTick; } else { - vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick; + vwapTick = (vwapTick > currentTick - anchorSpacing) ? currentTick - anchorSpacing : vwapTick; } // normalize tick position for pool @@ -379,8 +361,8 @@ contract LiquidityManager { fee1 += collected1 - amount1; if (i == uint256(Stage.ANCHOR)) { // the historic archor position is only an approximation for the price - int24 tick = token0isWeth ? -1 * (position.tickLower + ANCHOR_SPACING): position.tickUpper - ANCHOR_SPACING; - currentPrice = priceAtTick(tick); + int24 tick = position.tickLower + (position.tickUpper - position.tickLower / 2); + currentPrice = priceAtTick(token0isWeth ? -1 * tick : tick); } } } @@ -428,7 +410,7 @@ contract LiquidityManager { /// @notice Adjusts liquidity positions in response to an increase or decrease in the Harberg token's price. /// @dev This function should be called when significant price movement is detected. It recalibrates the liquidity ranges to align with the new market conditions. - function recenter() external returns (bool isUp, uint256 sentiment) { + function recenter() external returns (bool isUp) { // Fetch the current tick from the Uniswap V3 pool (, int24 currentTick, , , , , ) = pool.slot0(); @@ -447,7 +429,7 @@ contract LiquidityManager { int24 anchorTickUpper = positions[Stage.ANCHOR].tickUpper; // center tick can be calculated positive and negative numbers the same - int24 centerTick = token0isWeth ? anchorTickLower + ANCHOR_SPACING : anchorTickUpper - ANCHOR_SPACING; + int24 centerTick = anchorTickLower + (anchorTickUpper - anchorTickLower); uint256 minAmplitude = uint24(TICK_SPACING) * 2; // Determine the correct comparison direction based on token0isWeth @@ -463,16 +445,17 @@ contract LiquidityManager { if (isUp) { harb.setPreviousTotalSupply(harb.totalSupply()); } - - try sentimenter.getSentiment() returns (uint256 currentSentiment) { - sentiment = (currentSentiment > 10**18) ? 10**18 : currentSentiment; + try optimizer.getLiquidityParams() returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { + capitalInefficiency = (capitalInefficiency > 10**18) ? 10**18 : capitalInefficiency; + anchorShare = (anchorShare > 10**18) ? 10**18 : anchorShare; + anchorWidth = (anchorWidth > 100) ? 100 : anchorWidth; + discoveryDepth = (discoveryDepth > 10**18) ? 10**18 : discoveryDepth; + // set new positions + _set(currentTick, capitalInefficiency, anchorShare, anchorWidth, discoveryDepth); } catch { - //sentiment = 10**18 / 2; - sentiment = 0; + // set new positions with default, average parameters + _set(currentTick, 5*10**17, 5*10**17, 5*10, 5*10**17); } - - // set new positions - _set(currentTick, sentiment); } } diff --git a/onchain/src/Optimizer.sol b/onchain/src/Optimizer.sol new file mode 100644 index 0000000..16b9d56 --- /dev/null +++ b/onchain/src/Optimizer.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import {Harberg} from "./Harberg.sol"; +import {Stake} from "./Stake.sol"; +import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; +import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; + +/** + * @title Optimizer + * @notice This contract (formerly Sentimenter) calculates a “sentiment” value and liquidity parameters + * based on the tax rate and the percentage of Harberg staked. + * @dev It is upgradeable using UUPS. Only the admin (set during initialization) can upgrade. + */ +contract Optimizer is Initializable, UUPSUpgradeable { + Harberg private harberg; + Stake private stake; + + /// @dev Reverts if the caller is not the admin. + error UnauthorizedAccount(address account); + + /** + * @notice Initialize the Optimizer. + * @param _harberg The address of the Harberg token. + * @param _stake The address of the Stake contract. + */ + function initialize(address _harberg, address _stake) initializer public { + // Set the admin for upgradeability (using ERC1967Upgrade _changeAdmin) + _changeAdmin(msg.sender); + harberg = Harberg(_harberg); + stake = Stake(_stake); + } + + modifier onlyAdmin() { + _checkAdmin(); + _; + } + + function _checkAdmin() internal view virtual { + if (_getAdmin() != msg.sender) { + revert UnauthorizedAccount(msg.sender); + } + } + + function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {} + + /** + * @notice Calculates the sentiment based on the average tax rate and the percentage staked. + * @param averageTaxRate The average tax rate (as returned by the Stake contract). + * @param percentageStaked The percentage (in 1e18 precision) of the authorized stake that is currently staked. + * @return sentimentValue A value in the range 0 to 1e18 where 1e18 represents the worst sentiment. + */ + function calculateSentiment( + uint256 averageTaxRate, + uint256 percentageStaked + ) public pure returns (uint256 sentimentValue) { + // deltaS is the “slack” available below full staking + uint256 deltaS = 1e18 - percentageStaked; + + if (percentageStaked > 92e16) { + // If more than 92% of the authorized stake is in use, the sentiment drops rapidly. + // Penalty is computed as: (deltaS^3 * averageTaxRate) / (20 * 1e48) + uint256 penalty = (deltaS * deltaS * deltaS * averageTaxRate) / (20 * 1e48); + sentimentValue = penalty / 2; + } else { + // For lower staked percentages, sentiment decreases roughly linearly. + uint256 baseSentiment = 1e18 - ((percentageStaked * 1e18) / (92e16)); + // Apply a penalty based on the average tax rate. + if (averageTaxRate <= 1e16) { + sentimentValue = baseSentiment; + } else if (averageTaxRate <= 5e16) { + uint256 ratePenalty = ((averageTaxRate - 1e16) * baseSentiment) / (4e16); + sentimentValue = baseSentiment > ratePenalty ? baseSentiment - ratePenalty : 0; + } else { + // For very high tax rates, sentiment is maximally poor. + sentimentValue = 1e18; + } + } + return sentimentValue; + } + + /** + * @notice Returns the current sentiment. + * @return sentiment A number (with 1e18 precision) representing the staker sentiment. + */ + function getSentiment() external view returns (uint256 sentiment) { + uint256 percentageStaked = stake.getPercentageStaked(); + uint256 averageTaxRate = stake.getAverageTaxRate(); + sentiment = calculateSentiment(averageTaxRate, percentageStaked); + } + + /** + * @notice Returns liquidity parameters for the liquidity manager. + * @return capitalInefficiency Calculated as (1e18 - sentiment). + * @return anchorShare Set equal to the sentiment. + * @return anchorWidth Here set to a constant 100 (adjust as needed). + * @return discoveryDepth Set equal to the sentiment. + */ + function getLiquidityParams() + external + view + returns ( + uint256 capitalInefficiency, + uint256 anchorShare, + uint24 anchorWidth, + uint256 discoveryDepth + ) + { + uint256 percentageStaked = stake.getPercentageStaked(); + uint256 averageTaxRate = stake.getAverageTaxRate(); + uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked); + capitalInefficiency = 1e18 - sentiment; + anchorShare = sentiment; + // Here we simply set anchorWidth to 100; adjust this formula if needed. + anchorWidth = 100; + discoveryDepth = sentiment; + } +} + diff --git a/onchain/src/Sentimenter.sol b/onchain/src/Sentimenter.sol deleted file mode 100644 index 615e97c..0000000 --- a/onchain/src/Sentimenter.sol +++ /dev/null @@ -1,79 +0,0 @@ - -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import {Harberg} from "./Harberg.sol"; -import {Stake} from "./Stake.sol"; -import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; -import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; - -contract Sentimenter is Initializable, UUPSUpgradeable { - - Harberg private harberg; - Stake private stake; - - /** - * @dev The caller account is not authorized to perform an operation. - */ - error UnauthorizedAccount(address account); - - function initialize(address _harberg, address _stake) initializer public { - _changeAdmin(msg.sender); - harberg = Harberg(_harberg); - stake = Stake(_stake); - } - /** - * @dev Throws if called by any account other than the admin. - */ - modifier onlyAdmin() { - _checkAdmin(); - _; - } - - - /** - * @dev Throws if the sender is not the admin. - */ - function _checkAdmin() internal view virtual { - if (_getAdmin() != msg.sender) { - revert UnauthorizedAccount(msg.sender); - } - } - function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {} - - function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked) public pure returns (uint256 sentimentValue) { - uint256 deltaS = 10**18 - percentageStaked; - - if (percentageStaked > 92 * 10**16) { - // Rapid drop for high percentageStaked values - uint256 penalty = (deltaS * deltaS * deltaS * averageTaxRate) / (20 * 10**48); - sentimentValue = penalty / 2; - } else { - // Linearly decreasing sentiment value with rising percentageStaked - uint256 baseSentiment = 10**18 - (percentageStaked * 10**18) / (92 * 10**16); // Decreases from 10**18 to 0 - - // Apply penalty based on averageTaxRate - if (averageTaxRate <= 10**16) { - sentimentValue = baseSentiment; // No penalty for low averageTaxRate - } else if (averageTaxRate <= 5 * 10**16) { - uint256 ratePenalty = (averageTaxRate - 10**16) * baseSentiment / (4 * 10**16); - sentimentValue = baseSentiment > ratePenalty ? baseSentiment - ratePenalty : 0; - } else { - sentimentValue = 10**18; // High averageTaxRate results in maximum sentiment value (low sentiment) - } - } - - return sentimentValue; - } - - - /// @notice Computes the staker sentiment based on the proportion of the authorized stake that is currently staked. - /// @return sentiment A number between 0 and 200 indicating the market sentiment. - function getSentiment() external view returns (uint256 sentiment) { - uint256 percentageStaked = stake.getPercentageStaked(); - uint256 averageTaxRate = stake.getAverageTaxRate(); - sentiment = calculateSentiment(averageTaxRate, percentageStaked); - } - - -} diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index d9163c0..332303f 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -17,8 +17,8 @@ import "../src/helpers/UniswapHelpers.sol"; import {CSVHelper} from "./helpers/CSVHelper.sol"; import {CSVManager} from "./helpers/CSVManager.sol"; import {UniswapTestBase} from "./helpers/UniswapTestBase.sol"; -import "../src/Sentimenter.sol"; -import "../test/mocks/MockSentimenter.sol"; +import "../src/Optimizer.sol"; +import "../test/mocks/MockOptimizer.sol"; address constant TAX_POOL = address(2); // default fee of 1% @@ -89,9 +89,9 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { stake = new Stake(address(harberg), feeDestination); harberg.setStakingPool(address(stake)); - Sentimenter senti = Sentimenter(address(new MockSentimenter())); - senti.initialize(address(harberg), address(stake)); - lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(senti)); + Optimizer optimizer = Optimizer(address(new MockOptimizer())); + optimizer.initialize(address(harberg), address(stake)); + lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(optimizer)); lm.setFeeDestination(feeDestination); vm.prank(feeDestination); harberg.setLiquidityManager(address(lm)); @@ -104,25 +104,25 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { uint256 timeBefore = block.timestamp; vm.warp(timeBefore + (60 * 60 * 5)); - try lm.recenter() returns (bool isUp, uint256 sentiment) { + try lm.recenter() returns (bool isUp) { - // Check liquidity positions after slide - Response memory rsp; - rsp = checkLiquidity(isUp ? "shift" : "slide", sentiment); - assertGt(rsp.ethFloor, rsp.ethAnchor, "slide - Floor should hold more ETH than Anchor"); - assertGt(rsp.harbergDiscovery, rsp.harbergAnchor * 5, "slide - Discovery should hold more HARB than Anchor"); - assertEq(rsp.harbergFloor, 0, "slide - Floor should have no HARB"); - assertEq(rsp.ethDiscovery, 0, "slide - Discovery should have no ETH"); + // Check liquidity positions after slide + Response memory rsp; + rsp = checkLiquidity(isUp ? "shift" : "slide"); + assertGt(rsp.ethFloor, rsp.ethAnchor, "slide - Floor should hold more ETH than Anchor"); + assertGt(rsp.harbergDiscovery, rsp.harbergAnchor * 5, "slide - Discovery should hold more HARB than Anchor"); + assertEq(rsp.harbergFloor, 0, "slide - Floor should have no HARB"); + assertEq(rsp.ethDiscovery, 0, "slide - Discovery should have no ETH"); - } catch Error(string memory reason) { - if (keccak256(abi.encodePacked(reason)) == keccak256(abi.encodePacked("amplitude not reached."))) { - console.log("slide failed on amplitude"); - } else { - if (!last) { - revert(reason); // Rethrow the error if it's not the expected message - } - } - } + } catch Error(string memory reason) { + if (keccak256(abi.encodePacked(reason)) == keccak256(abi.encodePacked("amplitude not reached."))) { + console.log("slide failed on amplitude"); + } else { + if (!last) { + revert(reason); // Rethrow the error if it's not the expected message + } + } + } } function getBalancesPool(LiquidityManager.Stage s) internal view returns (int24 currentTick, int24 tickLower, int24 tickUpper, uint256 ethAmount, uint256 harbergAmount) { @@ -167,7 +167,7 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { } } - function checkLiquidity(string memory eventName, uint256 sentiment) internal returns (Response memory) { + function checkLiquidity(string memory eventName) internal returns (Response memory) { Response memory rsp; int24 currentTick; string memory floorData; @@ -198,19 +198,19 @@ contract LiquidityManagerTest is UniswapTestBase, CSVManager { } } - string memory newRow = string(abi.encodePacked(eventName, ",", CSVHelper.intToStr(currentTick), ",", CSVHelper.uintToStr(sentiment / 1e12), ",", floorData, anchorData, discoveryData)); + string memory newRow = string(abi.encodePacked(eventName, ",", CSVHelper.intToStr(currentTick), ",", floorData, anchorData, discoveryData)); appendCSVRow(newRow); // Append the new row to the CSV return rsp; } function buy(uint256 amountEth) internal { performSwap(amountEth, true); - checkLiquidity(string.concat("buy ", CSVHelper.uintToStr(amountEth)), 0); + checkLiquidity(string.concat("buy ", CSVHelper.uintToStr(amountEth))); } function sell(uint256 amountHarb) internal { performSwap(amountHarb, false); - checkLiquidity(string.concat("sell ", CSVHelper.uintToStr(amountHarb)), 0); + checkLiquidity(string.concat("sell ", CSVHelper.uintToStr(amountHarb))); } receive() external payable {} diff --git a/onchain/test/Sentimenter.t.sol b/onchain/test/Sentimenter.t.sol deleted file mode 100644 index 0c61458..0000000 --- a/onchain/test/Sentimenter.t.sol +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import "forge-std/console.sol"; -import "../src/Harberg.sol"; -import {TooMuchSnatch, Stake} from "../src/Stake.sol"; -import "../src/Sentimenter.sol"; -import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; -import {MockSentimenter} from "./mocks/MockSentimenter.sol"; - -contract SentimenterTest is Test { - Harberg harberg; - Stake stake; - Sentimenter sentimenter; - address liquidityManager; - - function setUp() public { - harberg = new Harberg("HARB", "HARB"); - stake = new Stake(address(harberg), makeAddr("taxRecipient")); - harberg.setStakingPool(address(stake)); - liquidityManager = makeAddr("liquidityManager"); - harberg.setLiquidityManager(liquidityManager); - // deploy upgradeable tuner contract - Sentimenter _sentimenter = new Sentimenter(); - bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(harberg),address(stake)); - ERC1967Proxy proxy = new ERC1967Proxy(address(_sentimenter), params); - sentimenter = Sentimenter(address(proxy)); - } - - function doSnatch(address staker, uint256 amount, uint32 taxRate) private returns (uint256 positionId) { - vm.startPrank(staker); - harberg.approve(address(stake), amount); - uint256[] memory empty; - positionId = stake.snatch(amount, staker, taxRate, empty); - vm.stopPrank(); - } - - function testSentiment() public { - uint256 smallstake = 0.3e17; - uint256 stakeOneThird = 1 ether; - uint256 stakeTwoThird = 2 ether; - address staker = makeAddr("staker"); - - // Mint and distribute tokens - vm.startPrank(liquidityManager); - // mint all the tokens we will need in the test - harberg.mint((smallstake + stakeOneThird + stakeTwoThird) * 5); - // send 20% of that to staker - harberg.transfer(staker, (smallstake + stakeOneThird + stakeTwoThird) * 2); - vm.stopPrank(); - - // Setup initial stakers - uint256 positionId1 = doSnatch(staker, smallstake, 0); - - uint256 sentiment; - sentiment = sentimenter.getSentiment(); - // 0.99 - horrible sentiment - assertApproxEqRel(sentiment, 9.9e17, 1e16); - - vm.prank(staker); - stake.exitPosition(positionId1); - uint256 positionId2 = doSnatch(staker, stakeOneThird, 2); - - sentiment = sentimenter.getSentiment(); - - // 0.64 - depressive sentiment - assertApproxEqRel(sentiment, 6.4e17, 1e16); - - vm.prank(staker); - stake.exitPosition(positionId2); - positionId1 = doSnatch(staker, stakeOneThird, 10); - positionId2 = doSnatch(staker, stakeTwoThird, 11); - - sentiment = sentimenter.getSentiment(); - - // 0.00018 - feaking good sentiment - assertApproxEqRel(sentiment, 1.8e14, 1e17); - - vm.startPrank(staker); - stake.exitPosition(positionId1); - stake.exitPosition(positionId2); - vm.stopPrank(); - positionId1 = doSnatch(staker, stakeOneThird, 29); - positionId2 = doSnatch(staker, stakeTwoThird, 29); - - sentiment = sentimenter.getSentiment(); - // 0.024 - pretty good sentiment - assertApproxEqRel(sentiment, 2.4e16, 2e16); - - vm.startPrank(staker); - stake.exitPosition(positionId1); - stake.exitPosition(positionId2); - vm.stopPrank(); - positionId2 = doSnatch(staker, stakeTwoThird, 15); - - sentiment = sentimenter.getSentiment(); - - // 0.17 - positive sentiment - assertApproxEqRel(sentiment, 1.7e17, 2e16); - - vm.startPrank(staker); - stake.exitPosition(positionId2); - vm.stopPrank(); - - positionId1 = doSnatch(staker, stakeOneThird, 15); - - sentiment = sentimenter.getSentiment(); - - // 0.4 - OK sentiment - assertApproxEqRel(sentiment, 3.9e17, 2e16); - } - - function testContractUpgrade() public { - uint256 sentiment = sentimenter.getSentiment(); - assertEq(sentiment, 1e18, "should have been upgraded"); - address newSent = address(new MockSentimenter()); - sentimenter.upgradeTo(newSent); - sentiment = sentimenter.getSentiment(); - assertEq(sentiment, 0, "should have been upgraded"); - } -} diff --git a/onchain/test/Simulations.t.sol b/onchain/test/Simulations.t.sol deleted file mode 100644 index 8a61675..0000000 --- a/onchain/test/Simulations.t.sol +++ /dev/null @@ -1,300 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import "@aperture/uni-v3-lib/TickMath.sol"; -import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; -import "../src/interfaces/IWETH9.sol"; -import {WETH} from "solmate/tokens/WETH.sol"; -import {PoolAddress, PoolKey} from "@aperture/uni-v3-lib/PoolAddress.sol"; -import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; -import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; -import {Harberg} from "../src/Harberg.sol"; -import "../src/helpers/UniswapHelpers.sol"; -import {Stake, ExceededAvailableStake} from "../src/Stake.sol"; -import {LiquidityManager} from "../src/LiquidityManager.sol"; -import {UniswapTestBase} from "./helpers/UniswapTestBase.sol"; -import {CSVHelper} from "./helpers/CSVHelper.sol"; -import {CSVManager} from "./helpers/CSVManager.sol"; -import "../src/Sentimenter.sol"; -import "./mocks/MockSentimenter.sol"; - -// default fee of 1% -uint24 constant FEE = uint24(10_000); - -// Dummy.sol -contract Dummy { - // This contract can be empty as it is only used to affect the nonce -} - -contract SimulationsTest is UniswapTestBase, CSVManager { - using UniswapHelpers for IUniswapV3Pool; - using CSVHelper for *; - - IUniswapV3Factory factory; - Stake stakingPool; - PoolKey private poolKey; - LiquidityManager lm; - address feeDestination = makeAddr("fees"); - uint256 supplyOnRecenter; - uint256 timeOnRecenter; - int256 supplyChange; - - struct Position { - uint128 liquidity; - int24 tickLower; - int24 tickUpper; - } - - enum ActionType { - Buy, - Sell, - Snatch, - Unstake, - PayTax, - Recenter, - Mint, - Burn - } - - struct Action { - uint256 kind; // buy, sell, snatch, unstake, paytax, recenter, mint, burn - uint256 amount1; // x , x , x , x , x , x , x , x - uint256 amount2; // , , x , , , , x , x - string position; // , , x , , , , , - } - - struct Scenario { - uint256 VWAP; - uint256 comHarbBal; - uint256 comStakeShare; - Position[] liquidity; // the positions are floor, anchor, liquidity, [comPos1, comPos2 ...] - uint160 sqrtPriceX96; - uint256 time; - bool token0IsWeth; - Action[] txns; - } - - - // Utility to deploy dummy contracts - function deployDummies(uint count) internal { - for (uint i = 0; i < count; i++) { - new Dummy(); // Just increment the nonce - } - } - - function setUp() public { - factory = UniswapHelpers.deployUniswapFactory(); - - weth = IWETH9(address(new WETH())); - harberg = new Harberg("Harberg", "HRB"); - pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE)); - - poolKey = PoolAddress.getPoolKey(address(weth), address(harberg), FEE); - token0isWeth = address(weth) < address(harberg); - //pool.initializePoolFor1Cent(token0isWeth); - - stakingPool = new Stake(address(harberg), feeDestination); - harberg.setStakingPool(address(stakingPool)); - Sentimenter senti = new Sentimenter(); - senti.initialize(address(harberg), address(stakingPool)); - lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(senti)); - lm.setFeeDestination(feeDestination); - vm.prank(feeDestination); - harberg.setLiquidityManager(address(lm)); - vm.deal(address(lm), 1 ether); - timeOnRecenter = block.timestamp; - initializeTimeSeriesCSV(); - } - - function setUpCustomToken0(bool token0shouldBeWeth) public { - factory = UniswapHelpers.deployUniswapFactory(); - - bool setupComplete = false; - uint retryCount = 0; - while (!setupComplete && retryCount < 5) { - // Clean slate if retrying - if (retryCount > 0) { - deployDummies(1); // Deploy a dummy contract to shift addresses - } - - weth = IWETH9(address(new WETH())); - harberg = new Harberg("HARB", "HARB"); - - // Check if the setup meets the required condition - if (token0shouldBeWeth == address(weth) < address(harberg)) { - setupComplete = true; - } else { - // Clear current instances for re-deployment - delete weth; - delete harberg; - retryCount++; - } - } - require(setupComplete, "Setup failed to meet the condition after several retries"); - - pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE)); - - token0isWeth = address(weth) < address(harberg); - stakingPool = new Stake(address(harberg), feeDestination); - harberg.setStakingPool(address(stakingPool)); - Sentimenter senti = Sentimenter(address(new MockSentimenter())); - senti.initialize(address(harberg), address(stakingPool)); - lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(senti)); - lm.setFeeDestination(feeDestination); - vm.prank(feeDestination); - harberg.setLiquidityManager(address(lm)); - vm.deal(address(lm), 10 ether); - initializePositionsCSV(); // Set up the CSV header - } - - - function buy(uint256 amountEth) internal { - performSwap(amountEth, true); - } - - function sell(uint256 amountHarb) internal { - performSwap(amountHarb, false); - } - - receive() external payable {} - - - function writeCsv() public { - writeCSVToFile("./out/timeSeries.csv"); // Write CSV to file - } - - function recenter() internal { - // have some time pass to record prices in uni oracle - uint256 timeBefore = block.timestamp; - vm.warp(timeBefore + 5 minutes); - - // uint256 - // uint256 supplyDelta = currentSupply - supplyOnRecenter; - // uint256 timeDelta = block.timestamp - timeOnRecenter; - // uint256 growthPerYear = supplyDelta * 52 weeks / timeDelta; - // // console.log("supplyOnLastRecenter"); - // // console.log(supplyOnRecenter); - // // console.log("currentSupply"); - // // console.log(currentSupply); - // uint256 growthPercentage = growthPerYear * 100 >= currentSupply ? 101 : growthPerYear * 100 / currentSupply; - // supplyOnRecenter = currentSupply; - - timeOnRecenter = block.timestamp; - uint256 supplyBefore = harberg.totalSupply(); - lm.recenter(); - supplyChange = int256(harberg.totalSupply()) - int256(supplyBefore); - // TODO: update supplyChangeOnLastRecenter - - // have some time pass to record prices in uni oracle - timeBefore = block.timestamp; - vm.warp(timeBefore + 5 minutes); - } - - function getPriceInHarb(uint160 sqrtPriceX96) internal view returns (uint256 price) { - uint256 sqrtPrice = uint256(sqrtPriceX96); - - if (token0isWeth) { - // WETH is token0, price = (sqrtPriceX96 / 2^96)^2 - price = (sqrtPrice * sqrtPrice) / (1 << 192); - } else { - // WETH is token1, price = (2^96 / sqrtPriceX96)^2 - price = ((1 << 192) * 1e18) / (sqrtPrice * sqrtPrice); - } - } - - function recordState() internal { - - uint160 sqrtPriceX96; - uint256 outstandingStake = stakingPool.outstandingStake(); - - (sqrtPriceX96, , , , , , ) = pool.slot0(); - - string memory newRow = string(abi.encodePacked(CSVHelper.uintToStr(block.timestamp), - ",", CSVHelper.uintToStr(getPriceInHarb(sqrtPriceX96)), - ",", CSVHelper.uintToStr(harberg.totalSupply() / 1e18), - ",", CSVHelper.intToStr(supplyChange / 1e18), - ",", CSVHelper.uintToStr(outstandingStake * 500 / 1e25) - )); - if (outstandingStake > 0) { - uint256 sentiment; - uint256 avgTaxRate; - //(sentiment, avgTaxRate) = stakingPool.getSentiment(); - newRow = string.concat(newRow, - ",", CSVHelper.uintToStr(avgTaxRate), - ",", CSVHelper.uintToStr(sentiment) - ); - } else { - newRow = string.concat(newRow, ", 0, 100, 95, 25, 0"); - } - appendCSVRow(newRow); // Append the new row to the CSV - } - - function stake(uint256 harbAmount, uint32 taxRateIndex) internal returns (uint256) { - vm.startPrank(account); - harberg.approve(address(stakingPool), harbAmount); - uint256[] memory empty; - uint256 posId = stakingPool.snatch(harbAmount, account, taxRateIndex, empty); - vm.stopPrank(); - return posId; - } - - function unstake(uint256 positionId) internal { - vm.prank(account); - stakingPool.exitPosition(positionId); - } - - function handleAction(Action memory action) internal { - if (action.kind == uint256(ActionType.Buy)) { - buy(action.amount1 * 10**18); - } else if (action.kind == uint256(ActionType.Sell)) { - sell(action.amount1 * 10**18); - } else if (action.kind == uint256(ActionType.Snatch)) { - stake(action.amount1 ** 10**18, uint32(action.amount2)); - } else if (action.kind == uint256(ActionType.Unstake)) { - unstake(action.amount1); - } else if (action.kind == uint256(ActionType.PayTax)) { - stakingPool.payTax(action.amount1); - } else if (action.kind == uint256(ActionType.Recenter)) { - uint256 timeBefore = block.timestamp; - vm.warp(timeBefore + action.amount1); - recenter(); - } - } - - function testGeneration() public { - // for each member of the generation, run all scenarios - string memory json = vm.readFile("test/data/scenarios.json"); - bytes memory data = vm.parseJson(json); - Scenario memory scenario = abi.decode(data, (Scenario)); - // vm.deal(account, scenario.comEthBal * 10**18); - vm.prank(account); - pool.initialize(scenario.sqrtPriceX96); - // initialize the liquidity - for (uint256 i = 0; i < scenario.liquidity.length; i++) { - - pool.mint( - address(this), - scenario.liquidity[i].tickLower, - scenario.liquidity[i].tickUpper, - scenario.liquidity[i].liquidity, - abi.encode(poolKey) - ); - } - weth.deposit{value: address(account).balance}(); - - for (uint256 i = 0; i < scenario.txns.length; i++) { - handleAction(scenario.txns[i]); - recordState(); - } - - //writeCsv(); - - // for each member, combine the single results into an overall fitness core - // apply the selection - // apply mating - // apply mutation - } - - -} diff --git a/onchain/test/data/scenarios.json b/onchain/test/data/scenarios.json deleted file mode 100644 index 2dd6217..0000000 --- a/onchain/test/data/scenarios.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "VWAP": 0, - "comHarbBal": 0, - "comStakeShare": 0, - "liquidity": [{ - "liquidity": 0, - "tickLower": -123891, - "tickUpper": -125000 - }, { - "liquidity": 0, - "tickLower": -123891, - "tickUpper": -125000 - }, { - "liquidity": 0, - "tickLower": -123891, - "tickUpper": -125000 - }], - "sqrtPriceX96": 38813714283599478074587411019430, - "time": 0, - "token0IsWeth": true, - "txns": [{ - "kind": 0, - "amount1": 10, - "amount2": 0, - "position": "" - }, { - "kind": 1, - "amount1": 1, - "amount2": 0, - "position": "" - }] -} diff --git a/onchain/test/mocks/MockSentimenter.sol b/onchain/test/mocks/MockOptimizer.sol similarity index 96% rename from onchain/test/mocks/MockSentimenter.sol rename to onchain/test/mocks/MockOptimizer.sol index 9c39ea7..b0b5045 100644 --- a/onchain/test/mocks/MockSentimenter.sol +++ b/onchain/test/mocks/MockOptimizer.sol @@ -7,7 +7,7 @@ import {Stake} from "../../src/Stake.sol"; import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; -contract MockSentimenter is Initializable, UUPSUpgradeable { +contract MockOptimizer is Initializable, UUPSUpgradeable { Harberg private harberg; Stake private stake;