2026-02-21 07:48:43 +00:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
pragma solidity ^0.8.19;
|
|
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
import {OptimizerV3Push3} from "../src/OptimizerV3Push3.sol";
|
|
|
|
|
import {OptimizerInput} from "../src/IOptimizer.sol";
|
2026-02-21 07:48:43 +00:00
|
|
|
import "forge-std/Test.sol";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @title OptimizerV3Push3Test
|
2026-03-10 23:13:57 +00:00
|
|
|
* @notice Verifies that the transpiled Push3 optimizer produces correct
|
|
|
|
|
* bear/bull parameters via the 8-slot dyadic rational interface.
|
|
|
|
|
*
|
|
|
|
|
* Bear output: CI=0, AS=0.3e18, AW=100, DD=0.3e18
|
|
|
|
|
* Bull output: CI=0, AS=1e18, AW=20, DD=1e18
|
|
|
|
|
*
|
|
|
|
|
* Bull condition: stakedPct > 91 AND penalty < 50
|
|
|
|
|
* where penalty = deltaS^3 * effIdx / 20
|
2026-02-21 07:48:43 +00:00
|
|
|
*/
|
|
|
|
|
contract OptimizerV3Push3Test is Test {
|
2026-02-26 14:20:11 +00:00
|
|
|
OptimizerV3Push3 push3;
|
2026-02-21 07:48:43 +00:00
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
uint256[30] TAX_RATES = [
|
|
|
|
|
uint256(1),
|
|
|
|
|
3,
|
|
|
|
|
5,
|
|
|
|
|
8,
|
|
|
|
|
12,
|
|
|
|
|
18,
|
|
|
|
|
24,
|
|
|
|
|
30,
|
|
|
|
|
40,
|
|
|
|
|
50,
|
|
|
|
|
60,
|
|
|
|
|
80,
|
|
|
|
|
100,
|
|
|
|
|
130,
|
|
|
|
|
180,
|
|
|
|
|
250,
|
|
|
|
|
320,
|
|
|
|
|
420,
|
|
|
|
|
540,
|
|
|
|
|
700,
|
|
|
|
|
920,
|
|
|
|
|
1200,
|
|
|
|
|
1600,
|
|
|
|
|
2000,
|
|
|
|
|
2600,
|
|
|
|
|
3400,
|
|
|
|
|
4400,
|
|
|
|
|
5700,
|
|
|
|
|
7500,
|
|
|
|
|
9700
|
|
|
|
|
];
|
2026-02-21 07:48:43 +00:00
|
|
|
uint256 constant MAX_TAX = 9700;
|
|
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
// Expected bear/bull outputs
|
|
|
|
|
uint256 constant BEAR_CI = 0;
|
|
|
|
|
uint256 constant BEAR_ANCHOR_SHARE = 3e17; // 0.3e18
|
|
|
|
|
uint24 constant BEAR_ANCHOR_WIDTH = 100;
|
|
|
|
|
uint256 constant BEAR_DISCOVERY = 3e17; // 0.3e18
|
|
|
|
|
|
|
|
|
|
uint256 constant BULL_CI = 0;
|
|
|
|
|
uint256 constant BULL_ANCHOR_SHARE = 1e18;
|
|
|
|
|
uint24 constant BULL_ANCHOR_WIDTH = 20;
|
|
|
|
|
uint256 constant BULL_DISCOVERY = 1e18;
|
|
|
|
|
|
2026-02-21 07:48:43 +00:00
|
|
|
function setUp() public {
|
|
|
|
|
push3 = new OptimizerV3Push3();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
// ---- Helpers ----
|
|
|
|
|
|
2026-02-21 07:48:43 +00:00
|
|
|
function _norm(uint256 taxIdx) internal view returns (uint256) {
|
|
|
|
|
return TAX_RATES[taxIdx] * 1e18 / MAX_TAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _pct(uint256 pct) internal pure returns (uint256) {
|
|
|
|
|
return pct * 1e18 / 100;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
/// @dev Build an 8-slot input array with only slots 0 and 1 populated.
|
|
|
|
|
function _inputs(uint256 percentageStaked, uint256 averageTaxRate)
|
|
|
|
|
internal
|
|
|
|
|
pure
|
|
|
|
|
returns (OptimizerInput[8] memory inp)
|
|
|
|
|
{
|
|
|
|
|
inp[0] = OptimizerInput({mantissa: int256(percentageStaked), shift: 0});
|
|
|
|
|
inp[1] = OptimizerInput({mantissa: int256(averageTaxRate), shift: 0});
|
|
|
|
|
// slots 2-7 default to (0, 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _assertBear(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure {
|
|
|
|
|
assertEq(ci, BEAR_CI, "bear: ci");
|
|
|
|
|
assertEq(as_, BEAR_ANCHOR_SHARE, "bear: anchorShare");
|
|
|
|
|
assertEq(aw, BEAR_ANCHOR_WIDTH, "bear: anchorWidth");
|
|
|
|
|
assertEq(dd, BEAR_DISCOVERY, "bear: discoveryDepth");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _assertBull(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure {
|
|
|
|
|
assertEq(ci, BULL_CI, "bull: ci");
|
|
|
|
|
assertEq(as_, BULL_ANCHOR_SHARE, "bull: anchorShare");
|
|
|
|
|
assertEq(aw, BULL_ANCHOR_WIDTH, "bull: anchorWidth");
|
|
|
|
|
assertEq(dd, BULL_DISCOVERY, "bull: discoveryDepth");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Bear cases ----
|
2026-02-21 07:48:43 +00:00
|
|
|
|
|
|
|
|
function testAlwaysBearAt0Percent() public view {
|
|
|
|
|
for (uint256 t = 0; t < 30; t++) {
|
2026-03-10 23:13:57 +00:00
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(0, _norm(t)));
|
|
|
|
|
_assertBear(ci, as_, aw, dd);
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function testAlwaysBearAt91Percent() public view {
|
|
|
|
|
for (uint256 t = 0; t < 30; t++) {
|
2026-03-10 23:13:57 +00:00
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(91), _norm(t)));
|
|
|
|
|
_assertBear(ci, as_, aw, dd);
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
// ---- Bull cases ----
|
|
|
|
|
|
2026-02-21 07:48:43 +00:00
|
|
|
function testBoundary92PercentLowestTax() public view {
|
|
|
|
|
// deltaS=8, effIdx=0 → penalty=0 < 50 → BULL
|
2026-03-10 23:13:57 +00:00
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(0)));
|
|
|
|
|
_assertBull(ci, as_, aw, dd);
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function testBoundary92PercentTaxIdx1() public view {
|
|
|
|
|
// deltaS=8, effIdx=1 → penalty=512*1/20=25 < 50 → BULL
|
2026-03-10 23:13:57 +00:00
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(1)));
|
|
|
|
|
_assertBull(ci, as_, aw, dd);
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
function testBoundary92PercentTaxIdx2Bear() public view {
|
2026-02-21 07:48:43 +00:00
|
|
|
// deltaS=8, effIdx=2 → penalty=512*2/20=51 >= 50 → BEAR
|
2026-03-10 23:13:57 +00:00
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(2)));
|
|
|
|
|
_assertBear(ci, as_, aw, dd);
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function testAt95PercentTaxIdx7() public view {
|
|
|
|
|
// deltaS=5, effIdx=7 → penalty=125*7/20=43 < 50 → BULL
|
2026-03-10 23:13:57 +00:00
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(95), _norm(7)));
|
|
|
|
|
_assertBull(ci, as_, aw, dd);
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
function testAt95PercentTaxIdx8Bear() public view {
|
2026-02-21 07:48:43 +00:00
|
|
|
// deltaS=5, effIdx=8 → penalty=125*8/20=50 NOT < 50 → BEAR
|
2026-03-10 23:13:57 +00:00
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(95), _norm(8)));
|
|
|
|
|
_assertBear(ci, as_, aw, dd);
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function testAt97PercentHighTax() public view {
|
|
|
|
|
// deltaS=3, effIdx=29 → penalty=27*29/20=39 < 50 → BULL
|
2026-03-10 23:13:57 +00:00
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(97), _norm(29)));
|
|
|
|
|
_assertBull(ci, as_, aw, dd);
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function testAt100PercentAlwaysBull() public view {
|
|
|
|
|
for (uint256 t = 0; t < 30; t++) {
|
2026-03-10 23:13:57 +00:00
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(1e18, _norm(t)));
|
|
|
|
|
_assertBull(ci, as_, aw, dd);
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function testEffIdxShiftAtBoundary() public view {
|
|
|
|
|
// taxIdx=13: effIdx=13, penalty=64*13/20=41 < 50 → BULL
|
2026-03-10 23:13:57 +00:00
|
|
|
{
|
|
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(96), _norm(13)));
|
|
|
|
|
_assertBull(ci, as_, aw, dd);
|
|
|
|
|
}
|
2026-02-21 07:48:43 +00:00
|
|
|
// taxIdx=14: effIdx=15 (shift!), penalty=64*15/20=48 < 50 → BULL
|
2026-03-10 23:13:57 +00:00
|
|
|
{
|
|
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(96), _norm(14)));
|
|
|
|
|
_assertBull(ci, as_, aw, dd);
|
|
|
|
|
}
|
2026-02-21 07:48:43 +00:00
|
|
|
// taxIdx=15: effIdx=16, penalty=64*16/20=51 >= 50 → BEAR
|
2026-03-10 23:13:57 +00:00
|
|
|
{
|
|
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(96), _norm(15)));
|
|
|
|
|
_assertBear(ci, as_, aw, dd);
|
|
|
|
|
}
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 23:13:57 +00:00
|
|
|
// ---- Unused slots are ignored ----
|
|
|
|
|
|
|
|
|
|
function testUnusedSlotsIgnored() public view {
|
|
|
|
|
// Populate slots 2-7 with arbitrary values; output should be unchanged.
|
|
|
|
|
OptimizerInput[8] memory inp;
|
|
|
|
|
inp[0] = OptimizerInput({mantissa: int256(_pct(92)), shift: 0});
|
|
|
|
|
inp[1] = OptimizerInput({mantissa: int256(_norm(0)), shift: 0});
|
|
|
|
|
inp[2] = OptimizerInput({mantissa: 12345678, shift: 0});
|
|
|
|
|
inp[3] = OptimizerInput({mantissa: 9876, shift: 0});
|
|
|
|
|
inp[4] = OptimizerInput({mantissa: 1e17, shift: 0});
|
|
|
|
|
inp[5] = OptimizerInput({mantissa: 3600, shift: 0});
|
|
|
|
|
inp[6] = OptimizerInput({mantissa: 5e17, shift: 0});
|
|
|
|
|
inp[7] = OptimizerInput({mantissa: 42, shift: 0});
|
|
|
|
|
|
|
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(inp);
|
|
|
|
|
_assertBull(ci, as_, aw, dd);
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Fuzz ----
|
|
|
|
|
|
|
|
|
|
function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view {
|
|
|
|
|
percentageStaked = bound(percentageStaked, 0, 1e18);
|
fix: Backtesting #5: Position tracking + P&L metrics (#319)
- Add PositionTracker.sol: tracks position lifecycle (open/close per
recenter), records tick ranges, liquidity, entry/exit blocks/timestamps,
token amounts (via LiquidityAmounts math), fees (proportional to
liquidity share), IL (LP exit value − HODL value at exit price), and
net P&L per position. Aggregates total fees, cumulative IL, net P&L,
rebalance count, Anchor time-in-range, and capital efficiency accumulators.
Logs with [TRACKER][TYPE] prefix; emits cumulative P&L every 500 blocks.
- Modify StrategyExecutor.sol: add IUniswapV3Pool + token0isWeth to
constructor (creates PositionTracker internally), call
tracker.notifyBlock() on every block for time-in-range, and call
tracker.recordRecenter() on each successful recenter. logSummary()
now delegates to tracker.logFinalSummary().
- Modify BacktestRunner.s.sol: pass sp.pool and token0isWeth to
StrategyExecutor constructor; log tracker address.
- forge fmt: reformat all backtesting scripts and affected src/test files
to project style (number_underscore=thousands, multiline_func_header=all).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 11:23:18 +00:00
|
|
|
averageTaxRate = bound(averageTaxRate, 0, 1e18);
|
2026-03-10 23:13:57 +00:00
|
|
|
push3.calculateParams(_inputs(percentageStaked, averageTaxRate));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function testFuzzOutputsAreAlwaysBearOrBull(uint256 percentageStaked, uint256 averageTaxRate) public view {
|
|
|
|
|
percentageStaked = bound(percentageStaked, 0, 1e18);
|
|
|
|
|
averageTaxRate = bound(averageTaxRate, 0, 1e18);
|
|
|
|
|
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) =
|
|
|
|
|
push3.calculateParams(_inputs(percentageStaked, averageTaxRate));
|
|
|
|
|
|
|
|
|
|
// CI is always 0 in the binary bear/bull model
|
|
|
|
|
assertEq(ci, 0, "ci always 0");
|
|
|
|
|
|
|
|
|
|
// Output is exactly BEAR or BULL
|
|
|
|
|
bool isBearOutput = (as_ == BEAR_ANCHOR_SHARE && aw == BEAR_ANCHOR_WIDTH && dd == BEAR_DISCOVERY);
|
|
|
|
|
bool isBullOutput = (as_ == BULL_ANCHOR_SHARE && aw == BULL_ANCHOR_WIDTH && dd == BULL_DISCOVERY);
|
|
|
|
|
assertTrue(isBearOutput || isBullOutput, "output must be bear or bull");
|
2026-02-21 07:48:43 +00:00
|
|
|
}
|
|
|
|
|
}
|