## Changes ### Configuration - Added .solhint.json with recommended rules + custom config - 160 char line length (warn) - Double quotes enforcement (error) - Explicit visibility required (error) - Console statements allowed (scripts/tests need them) - Gas optimization warnings enabled - Ignores test/helpers/, lib/, out/, cache/, broadcast/ - Added foundry.toml [fmt] section - 160 char line length - 4-space tabs - Double quotes - Thousands separators for numbers - Sort imports enabled - Added .lintstagedrc.json for pre-commit auto-fix - Runs solhint --fix on .sol files - Runs forge fmt on .sol files - Added husky pre-commit hook via lint-staged ### NPM Scripts - lint:sol - run solhint - lint:sol:fix - auto-fix solhint issues - format:sol - format with forge fmt - format:sol:check - check formatting - lint / lint:fix - combined commands ### Code Changes - Added explicit visibility modifiers (internal) to constants in scripts and tests - Fixed quote style in DeployLocal.sol - All Solidity files formatted with forge fmt ## Verification - ✅ forge fmt --check passes - ✅ No solhint errors (warnings only) - ✅ forge build succeeds - ✅ forge test passes (107/107) resolves #44 Co-authored-by: johba <johba@harb.eth> Reviewed-on: https://codeberg.org/johba/harb/pulls/51
210 lines
9.7 KiB
Solidity
210 lines
9.7 KiB
Solidity
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
pragma solidity ^0.8.19;
|
|
|
|
import { Kraiken } from "./Kraiken.sol";
|
|
import { Stake } from "./Stake.sol";
|
|
|
|
import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol";
|
|
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
|
|
|
/**
|
|
* @title Optimizer
|
|
* @notice This contract (formerly Sentimenter) calculates a "sentiment" value and liquidity parameters
|
|
* based on the tax rate and the percentage of Kraiken staked.
|
|
* @dev It is upgradeable using UUPS. Only the admin (set during initialization) can upgrade.
|
|
*
|
|
* Key features:
|
|
* - Analyzes staking sentiment (% staked, average tax rate)
|
|
* - Returns four key parameters for liquidity management:
|
|
* 1. capitalInefficiency (0 to 1e18): Capital buffer level
|
|
* 2. anchorShare (0 to 1e18): % of non-floor ETH in anchor
|
|
* 3. anchorWidth (0 to 100): Anchor position width %
|
|
* 4. discoveryDepth (0 to 1e18): Discovery liquidity density (2x-10x)
|
|
* - Upgradeable for future algorithm improvements
|
|
*
|
|
* AnchorWidth Price Ranges:
|
|
* The anchor position's price range depends on anchorWidth value:
|
|
* - anchorWidth = 10: ±9% range (0.92x to 1.09x current price)
|
|
* - anchorWidth = 40: ±33% range (0.75x to 1.34x current price)
|
|
* - anchorWidth = 50: ±42% range (0.70x to 1.43x current price)
|
|
* - anchorWidth = 80: ±74% range (0.57x to 1.75x current price)
|
|
* - anchorWidth = 100: -50% to +100% range (0.50x to 2.00x current price)
|
|
*
|
|
* The formula: anchorSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100)
|
|
* creates a non-linear price range due to Uniswap V3's tick-based system
|
|
*/
|
|
contract Optimizer is Initializable, UUPSUpgradeable {
|
|
Kraiken private kraiken;
|
|
Stake private stake;
|
|
|
|
/// @dev Reverts if the caller is not the admin.
|
|
error UnauthorizedAccount(address account);
|
|
|
|
/**
|
|
* @notice Initialize the Optimizer.
|
|
* @param _kraiken The address of the Kraiken token.
|
|
* @param _stake The address of the Stake contract.
|
|
*/
|
|
function initialize(address _kraiken, address _stake) public initializer {
|
|
// Set the admin for upgradeability (using ERC1967Upgrade _changeAdmin)
|
|
_changeAdmin(msg.sender);
|
|
kraiken = Kraiken(_kraiken);
|
|
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) {
|
|
// Ensure percentageStaked doesn't exceed 100%
|
|
require(percentageStaked <= 1e18, "Invalid percentage staked");
|
|
|
|
// 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.
|
|
// Ensure we don't underflow if percentageStaked approaches 92%
|
|
uint256 scaledStake = (percentageStaked * 1e18) / (92e16);
|
|
uint256 baseSentiment = scaledStake >= 1e18 ? 0 : 1e18 - scaledStake;
|
|
// 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 Calculates the optimal anchor width based on staking metrics.
|
|
* @param percentageStaked The percentage of tokens staked (0 to 1e18)
|
|
* @param averageTaxRate The average tax rate across all stakers (0 to 1e18)
|
|
* @return anchorWidth The calculated anchor width (10 to 80)
|
|
*
|
|
* @dev This function implements a staking-based approach to determine anchor width:
|
|
*
|
|
* Base Strategy:
|
|
* - Start with base width of 40% (balanced default)
|
|
*
|
|
* Staking Adjustment (-20% to +20%):
|
|
* - High staking (>70%) indicates bullish confidence → narrow anchor for fee optimization
|
|
* - Low staking (<30%) indicates bearish/uncertainty → wide anchor for safety
|
|
* - Inverse relationship: higher staking = lower width adjustment
|
|
*
|
|
* Tax Rate Adjustment (-10% to +30%):
|
|
* - High tax rates signal expected volatility → wider anchor to reduce rebalancing
|
|
* - Low tax rates signal expected stability → narrower anchor for fee collection
|
|
* - Direct relationship: higher tax = higher width adjustment
|
|
*
|
|
* The Harberger tax mechanism acts as a decentralized prediction market where:
|
|
* - Tax rates reflect holders' expectations of being "snatched" (volatility)
|
|
* - Staking percentage reflects overall market confidence
|
|
*
|
|
* Final width is clamped between 10 (minimum safe) and 80 (maximum effective)
|
|
*/
|
|
function _calculateAnchorWidth(uint256 percentageStaked, uint256 averageTaxRate) internal pure returns (uint24) {
|
|
// Base width: 40% is our neutral starting point
|
|
int256 baseWidth = 40;
|
|
|
|
// Staking adjustment: -20% to +20% based on staking percentage
|
|
// Formula: 20 - (percentageStaked * 40 / 1e18)
|
|
// High staking (1e18) → -20 adjustment → narrower width
|
|
// Low staking (0) → +20 adjustment → wider width
|
|
int256 stakingAdjustment = 20 - int256(percentageStaked * 40 / 1e18);
|
|
|
|
// Tax rate adjustment: -10% to +30% based on average tax rate
|
|
// Formula: (averageTaxRate * 40 / 1e18) - 10
|
|
// High tax (1e18) → +30 adjustment → wider width for volatility
|
|
// Low tax (0) → -10 adjustment → narrower width for stability
|
|
int256 taxAdjustment = int256(averageTaxRate * 40 / 1e18) - 10;
|
|
|
|
// Combine all adjustments
|
|
int256 totalWidth = baseWidth + stakingAdjustment + taxAdjustment;
|
|
|
|
// Clamp to safe bounds (10 to 80)
|
|
// Below 10%: rebalancing costs exceed benefits
|
|
// Above 80%: capital efficiency degrades significantly
|
|
if (totalWidth < 10) {
|
|
return 10;
|
|
}
|
|
if (totalWidth > 80) {
|
|
return 80;
|
|
}
|
|
|
|
return uint24(uint256(totalWidth));
|
|
}
|
|
|
|
/**
|
|
* @notice Returns liquidity parameters for the liquidity manager.
|
|
* @return capitalInefficiency Calculated as (1e18 - sentiment). Capital buffer level (0-1e18)
|
|
* @return anchorShare Set equal to the sentiment. % of non-floor ETH in anchor (0-1e18)
|
|
* @return anchorWidth Dynamically adjusted based on staking metrics. Anchor position width % (1-100)
|
|
* @return discoveryDepth Set equal to the sentiment.
|
|
*
|
|
* @dev AnchorWidth Strategy:
|
|
* The anchorWidth parameter controls the price range of the anchor liquidity position.
|
|
* - anchorWidth = 50: Price range from 0.70x to 1.43x current price
|
|
* - anchorWidth = 100: Price range from 0.50x to 2.00x current price
|
|
*
|
|
* We use staking metrics as a decentralized prediction market:
|
|
* - High staking % → Bullish sentiment → Narrower width (30-50%) for fee optimization
|
|
* - Low staking % → Bearish/uncertain → Wider width (60-80%) for defensive positioning
|
|
* - High avg tax rate → Expects volatility → Wider anchor to reduce rebalancing
|
|
* - Low avg tax rate → Expects stability → Narrower anchor for fee collection
|
|
*/
|
|
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);
|
|
|
|
// Ensure sentiment doesn't exceed 1e18 to prevent underflow
|
|
// Cap sentiment at 1e18 if it somehow exceeds it
|
|
if (sentiment > 1e18) {
|
|
sentiment = 1e18;
|
|
}
|
|
capitalInefficiency = 1e18 - sentiment;
|
|
anchorShare = sentiment;
|
|
|
|
// Calculate dynamic anchorWidth based on staking metrics
|
|
anchorWidth = _calculateAnchorWidth(percentageStaked, averageTaxRate);
|
|
|
|
discoveryDepth = sentiment;
|
|
}
|
|
}
|