## 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
239 lines
9.6 KiB
Solidity
239 lines
9.6 KiB
Solidity
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
pragma solidity ^0.8.19;
|
|
|
|
import "../src/Optimizer.sol";
|
|
|
|
import "./mocks/MockKraiken.sol";
|
|
import "./mocks/MockStake.sol";
|
|
import "forge-std/Test.sol";
|
|
import "forge-std/console.sol";
|
|
|
|
contract OptimizerTest is Test {
|
|
Optimizer optimizer;
|
|
MockStake mockStake;
|
|
MockKraiken mockKraiken;
|
|
|
|
function setUp() public {
|
|
// Deploy mocks
|
|
mockKraiken = new MockKraiken();
|
|
mockStake = new MockStake();
|
|
|
|
// Deploy Optimizer implementation
|
|
Optimizer implementation = new Optimizer();
|
|
|
|
// Deploy proxy and initialize
|
|
bytes memory initData = abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
|
|
|
|
// For simplicity, we'll test the implementation directly
|
|
// In production, you'd use a proper proxy setup
|
|
optimizer = implementation;
|
|
optimizer.initialize(address(mockKraiken), address(mockStake));
|
|
}
|
|
|
|
/**
|
|
* @notice Test that anchorWidth adjusts correctly for bull market conditions
|
|
* @dev High staking, low tax → narrow anchor (30-35%)
|
|
*/
|
|
function testBullMarketAnchorWidth() public {
|
|
// Set bull market conditions: high staking (80%), low tax (10%)
|
|
mockStake.setPercentageStaked(0.8e18);
|
|
mockStake.setAverageTaxRate(0.1e18);
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
|
|
|
// Expected: base(40) + staking_adj(20 - 32 = -12) + tax_adj(4 - 10 = -6) = 22
|
|
assertEq(anchorWidth, 22, "Bull market should have narrow anchor width");
|
|
assertTrue(anchorWidth >= 20 && anchorWidth <= 35, "Bull market width should be 20-35%");
|
|
}
|
|
|
|
/**
|
|
* @notice Test that anchorWidth adjusts correctly for bear market conditions
|
|
* @dev Low staking, high tax → wide anchor (60-80%)
|
|
*/
|
|
function testBearMarketAnchorWidth() public {
|
|
// Set bear market conditions: low staking (20%), high tax (70%)
|
|
mockStake.setPercentageStaked(0.2e18);
|
|
mockStake.setAverageTaxRate(0.7e18);
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
|
|
|
// Expected: base(40) + staking_adj(20 - 8 = 12) + tax_adj(28 - 10 = 18) = 70
|
|
assertEq(anchorWidth, 70, "Bear market should have wide anchor width");
|
|
assertTrue(anchorWidth >= 60 && anchorWidth <= 80, "Bear market width should be 60-80%");
|
|
}
|
|
|
|
/**
|
|
* @notice Test neutral market conditions
|
|
* @dev Medium staking, medium tax → balanced anchor (35-50%)
|
|
*/
|
|
function testNeutralMarketAnchorWidth() public {
|
|
// Set neutral conditions: medium staking (50%), medium tax (30%)
|
|
mockStake.setPercentageStaked(0.5e18);
|
|
mockStake.setAverageTaxRate(0.3e18);
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
|
|
|
// Expected: base(40) + staking_adj(20 - 20 = 0) + tax_adj(12 - 10 = 2) = 42
|
|
assertEq(anchorWidth, 42, "Neutral market should have balanced anchor width");
|
|
assertTrue(anchorWidth >= 35 && anchorWidth <= 50, "Neutral width should be 35-50%");
|
|
}
|
|
|
|
/**
|
|
* @notice Test high volatility scenario
|
|
* @dev High staking with high tax (speculative frenzy) → moderate-wide anchor
|
|
*/
|
|
function testHighVolatilityAnchorWidth() public {
|
|
// High staking (70%) but also high tax (80%) - speculative market
|
|
mockStake.setPercentageStaked(0.7e18);
|
|
mockStake.setAverageTaxRate(0.8e18);
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
|
|
|
// Expected: base(40) + staking_adj(20 - 28 = -8) + tax_adj(32 - 10 = 22) = 54
|
|
assertEq(anchorWidth, 54, "High volatility should have moderate-wide anchor");
|
|
assertTrue(anchorWidth >= 50 && anchorWidth <= 60, "Volatile width should be 50-60%");
|
|
}
|
|
|
|
/**
|
|
* @notice Test stable market conditions
|
|
* @dev Medium staking with very low tax → narrow anchor for fee optimization
|
|
*/
|
|
function testStableMarketAnchorWidth() public {
|
|
// Medium staking (50%), very low tax (5%) - stable conditions
|
|
mockStake.setPercentageStaked(0.5e18);
|
|
mockStake.setAverageTaxRate(0.05e18);
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
|
|
|
// Expected: base(40) + staking_adj(20 - 20 = 0) + tax_adj(2 - 10 = -8) = 32
|
|
assertEq(anchorWidth, 32, "Stable market should have narrower anchor");
|
|
assertTrue(anchorWidth >= 30 && anchorWidth <= 40, "Stable width should be 30-40%");
|
|
}
|
|
|
|
/**
|
|
* @notice Test minimum bound enforcement
|
|
* @dev Extreme conditions that would result in width < 10 should clamp to 10
|
|
*/
|
|
function testMinimumWidthBound() public {
|
|
// Extreme bull: very high staking (95%), zero tax
|
|
mockStake.setPercentageStaked(0.95e18);
|
|
mockStake.setAverageTaxRate(0);
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
|
|
|
// Expected: base(40) + staking_adj(20 - 38 = -18) + tax_adj(0 - 10 = -10) = 12
|
|
// But should be at least 10
|
|
assertEq(anchorWidth, 12, "Should not go below calculated value if above 10");
|
|
assertTrue(anchorWidth >= 10, "Width should never be less than 10");
|
|
}
|
|
|
|
/**
|
|
* @notice Test maximum bound enforcement
|
|
* @dev Extreme conditions that would result in width > 80 should clamp to 80
|
|
*/
|
|
function testMaximumWidthBound() public {
|
|
// Extreme bear: zero staking, maximum tax
|
|
mockStake.setPercentageStaked(0);
|
|
mockStake.setAverageTaxRate(1e18);
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
|
|
|
// Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(40 - 10 = 30) = 90
|
|
// But should be clamped to 80
|
|
assertEq(anchorWidth, 80, "Should clamp to maximum of 80");
|
|
assertTrue(anchorWidth <= 80, "Width should never exceed 80");
|
|
}
|
|
|
|
/**
|
|
* @notice Test edge case with exactly minimum staking and tax
|
|
*/
|
|
function testEdgeCaseMinimumInputs() public {
|
|
mockStake.setPercentageStaked(0);
|
|
mockStake.setAverageTaxRate(0);
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
|
|
|
// Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(0 - 10 = -10) = 50
|
|
assertEq(anchorWidth, 50, "Zero inputs should give moderate width");
|
|
}
|
|
|
|
/**
|
|
* @notice Test edge case with exactly maximum staking and tax
|
|
*/
|
|
function testEdgeCaseMaximumInputs() public {
|
|
mockStake.setPercentageStaked(1e18);
|
|
mockStake.setAverageTaxRate(1e18);
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
|
|
|
// Expected: base(40) + staking_adj(20 - 40 = -20) + tax_adj(40 - 10 = 30) = 50
|
|
assertEq(anchorWidth, 50, "Maximum inputs should balance out to moderate width");
|
|
}
|
|
|
|
/**
|
|
* @notice Test edge case with high staking and high tax rate
|
|
* @dev This specific case previously caused an overflow
|
|
*/
|
|
function testHighStakingHighTaxEdgeCase() public {
|
|
// Set conditions that previously caused overflow
|
|
// ~94.6% staked, ~96.7% tax rate
|
|
mockStake.setPercentageStaked(946_350_908_835_331_692);
|
|
mockStake.setAverageTaxRate(966_925_542_613_630_263);
|
|
|
|
(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = optimizer.getLiquidityParams();
|
|
|
|
// With very high staking (>92%) and high tax, sentiment reaches maximum (1e18)
|
|
// This results in zero capital inefficiency
|
|
assertEq(capitalInefficiency, 0, "Max sentiment should result in zero capital inefficiency");
|
|
|
|
// Anchor share should be at maximum
|
|
assertEq(anchorShare, 1e18, "Max sentiment should result in maximum anchor share");
|
|
|
|
// Anchor width should still be within bounds
|
|
assertTrue(anchorWidth >= 10 && anchorWidth <= 80, "Anchor width should be within bounds");
|
|
|
|
// Expected: base(40) + staking_adj(20 - 37 = -17) + tax_adj(38 - 10 = 28) = 51
|
|
assertEq(anchorWidth, 51, "Should calculate correct width for edge case");
|
|
}
|
|
|
|
/**
|
|
* @notice Fuzz test to ensure anchorWidth always stays within bounds
|
|
*/
|
|
function testFuzzAnchorWidthBounds(uint256 percentageStaked, uint256 averageTaxRate) public {
|
|
// Bound inputs to valid ranges
|
|
percentageStaked = bound(percentageStaked, 0, 1e18);
|
|
averageTaxRate = bound(averageTaxRate, 0, 1e18);
|
|
|
|
mockStake.setPercentageStaked(percentageStaked);
|
|
mockStake.setAverageTaxRate(averageTaxRate);
|
|
|
|
(,, uint24 anchorWidth,) = optimizer.getLiquidityParams();
|
|
|
|
// Assert bounds are always respected
|
|
assertTrue(anchorWidth >= 10, "Width should never be less than 10");
|
|
assertTrue(anchorWidth <= 80, "Width should never exceed 80");
|
|
|
|
// Edge cases (10 or 80) are valid and tested by assertions
|
|
}
|
|
|
|
/**
|
|
* @notice Test that other liquidity params are still calculated correctly
|
|
*/
|
|
function testOtherLiquidityParams() public {
|
|
mockStake.setPercentageStaked(0.6e18);
|
|
mockStake.setAverageTaxRate(0.4e18);
|
|
|
|
(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = optimizer.getLiquidityParams();
|
|
|
|
uint256 sentiment = optimizer.getSentiment();
|
|
|
|
// Verify relationships
|
|
assertEq(capitalInefficiency, 1e18 - sentiment, "Capital inefficiency should be 1 - sentiment");
|
|
assertEq(anchorShare, sentiment, "Anchor share should equal sentiment");
|
|
assertEq(discoveryDepth, sentiment, "Discovery depth should equal sentiment");
|
|
|
|
// Verify anchor width is calculated independently
|
|
// Expected: base(40) + staking_adj(20 - 24 = -4) + tax_adj(16 - 10 = 6) = 42
|
|
assertEq(anchorWidth, 42, "Anchor width should be independently calculated");
|
|
}
|
|
}
|