replaced sentiment with specific params

This commit is contained in:
giteadmin 2025-02-01 21:49:15 +01:00
parent 6fe349de9a
commit 78b48f1639
9 changed files with 187 additions and 618 deletions

View file

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

View file

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

119
onchain/src/Optimizer.sol Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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": ""
}]
}

View file

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