harb/onchain/src/Optimizer.sol
johba d7c2184ccf Add Solidity linting with solhint, Foundry formatter, and pre-commit hooks (#51)
## 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
2025-10-04 15:17:09 +02:00

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