fix: No Foundry test for OptimizerV3 calculateParams correctness (#607)
Add table-driven Foundry tests for OptimizerV3.calculateParams covering: - Bear regime at 0%, 1%, 50%, 91% staking (all tax rates) - Bull/bear boundary at 92% with tax index transitions - Bull/bear at 95% with penalty=50 exact boundary - EffIdx shift behavior at 96% (taxIdx 13→14 discontinuity) - Bull at 97% with max tax, 100% always bull - Edge cases: all-zero inputs, zero tax at high staking - Mantissa overflow guard - Unused slots ignored - Fuzz: no reverts, output always exactly bear or bull Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a3de10bf1d
commit
87912b06da
1 changed files with 234 additions and 16 deletions
|
|
@ -7,18 +7,80 @@ import "forge-std/Test.sol";
|
|||
|
||||
/**
|
||||
* @title OptimizerV3Test
|
||||
* @notice Unit tests for OptimizerV3.calculateParams input validation guards
|
||||
* (shift and negative-mantissa) and basic bear/bull output correctness.
|
||||
* @notice Table-driven unit tests for OptimizerV3.calculateParams covering
|
||||
* bull/bear boundaries, edge inputs, and input validation guards.
|
||||
*
|
||||
* 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 deltaS^3 * effIdx / 20 < 50
|
||||
* where deltaS = 100 - stakedPct, effIdx = taxIdx (+ 1 if taxIdx >= 14, capped at 29)
|
||||
*/
|
||||
contract OptimizerV3Test is Test {
|
||||
OptimizerV3 v3;
|
||||
|
||||
/// @dev Normalised tax-rate thresholds used by the transpiler to map
|
||||
/// continuous averageTaxRate → discrete effective index 0..29.
|
||||
/// These are the raw bracket values from the 30-bracket schedule.
|
||||
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
|
||||
];
|
||||
uint256 constant MAX_TAX = 9700;
|
||||
|
||||
// Expected bear/bull outputs
|
||||
uint256 constant BEAR_CI = 0;
|
||||
uint256 constant BEAR_AS = 3e17;
|
||||
uint24 constant BEAR_AW = 100;
|
||||
uint256 constant BEAR_DD = 3e17;
|
||||
|
||||
uint256 constant BULL_CI = 0;
|
||||
uint256 constant BULL_AS = 1e18;
|
||||
uint24 constant BULL_AW = 20;
|
||||
uint256 constant BULL_DD = 1e18;
|
||||
|
||||
function setUp() public {
|
||||
v3 = new OptimizerV3();
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function _inputs(uint256 percentageStaked, uint256 averageTaxRate)
|
||||
internal
|
||||
pure
|
||||
|
|
@ -28,6 +90,20 @@ contract OptimizerV3Test is Test {
|
|||
inp[1] = OptimizerInput({mantissa: int256(averageTaxRate), shift: 0});
|
||||
}
|
||||
|
||||
function _assertBear(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure {
|
||||
assertEq(ci, BEAR_CI, "bear: ci");
|
||||
assertEq(as_, BEAR_AS, "bear: anchorShare");
|
||||
assertEq(aw, BEAR_AW, "bear: anchorWidth");
|
||||
assertEq(dd, BEAR_DD, "bear: discoveryDepth");
|
||||
}
|
||||
|
||||
function _assertBull(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure {
|
||||
assertEq(ci, BULL_CI, "bull: ci");
|
||||
assertEq(as_, BULL_AS, "bull: anchorShare");
|
||||
assertEq(aw, BULL_AW, "bull: anchorWidth");
|
||||
assertEq(dd, BULL_DD, "bull: discoveryDepth");
|
||||
}
|
||||
|
||||
// ---- Shift guard ----
|
||||
|
||||
function testNonZeroShiftReverts() public {
|
||||
|
|
@ -63,23 +139,165 @@ contract OptimizerV3Test is Test {
|
|||
v3.calculateParams(inp);
|
||||
}
|
||||
|
||||
// ---- Basic output correctness ----
|
||||
// ---- Mantissa overflow guard ----
|
||||
|
||||
function testBearAtLowStaking() public view {
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) =
|
||||
v3.calculateParams(_inputs(50e16, 0));
|
||||
assertEq(ci, 0);
|
||||
assertEq(as_, 3e17);
|
||||
assertEq(aw, 100);
|
||||
assertEq(dd, 3e17);
|
||||
function testMantissaOverflowReverts() public {
|
||||
OptimizerInput[8] memory inp;
|
||||
inp[0] = OptimizerInput({mantissa: int256(1e18 + 1), shift: 0});
|
||||
vm.expectRevert("mantissa overflow");
|
||||
v3.calculateParams(inp);
|
||||
}
|
||||
|
||||
function testBullAtHighStaking() public view {
|
||||
// ---- Bear: staking at or below 91% boundary ----
|
||||
|
||||
function testAlwaysBearAt0Percent() public view {
|
||||
for (uint256 t = 0; t < 30; t++) {
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(0, _norm(t)));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
}
|
||||
|
||||
function testAlwaysBearAt50Percent() public view {
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(50), 0));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
function testAlwaysBearAt91Percent() public view {
|
||||
for (uint256 t = 0; t < 30; t++) {
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(91), _norm(t)));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
}
|
||||
|
||||
function testBearAt1Percent() public view {
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(1), 0));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
// ---- Bull: boundary at 92% staking ----
|
||||
|
||||
function testBull92PercentTaxIdx0() public view {
|
||||
// deltaS=8, effIdx=0 → penalty=0 < 50 → BULL
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(92), _norm(0)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
function testBull92PercentTaxIdx1() public view {
|
||||
// deltaS=8, effIdx=1 → penalty=512*1/20=25 < 50 → BULL
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(92), _norm(1)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
function testBear92PercentTaxIdx2() public view {
|
||||
// deltaS=8, effIdx=2 → penalty=512*2/20=51 >= 50 → BEAR
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(92), _norm(2)));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
// ---- Bull/Bear at 95% staking ----
|
||||
|
||||
function testBull95PercentTaxIdx7() public view {
|
||||
// deltaS=5, effIdx=7 → penalty=125*7/20=43 < 50 → BULL
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(95), _norm(7)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
function testBear95PercentTaxIdx8() public view {
|
||||
// deltaS=5, effIdx=8 → penalty=125*8/20=50 NOT < 50 → BEAR
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(95), _norm(8)));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
// ---- Bull/Bear at 96% with effIdx shift ----
|
||||
|
||||
function testEffIdxShiftAt96Percent() public view {
|
||||
// taxIdx=13: effIdx=13, penalty=64*13/20=41 < 50 → BULL
|
||||
{
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(96), _norm(13)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
// taxIdx=14: effIdx=15 (shift! +1 for idx>=14), penalty=64*15/20=48 < 50 → BULL
|
||||
{
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(96), _norm(14)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
// taxIdx=15: effIdx=16, penalty=64*16/20=51 >= 50 → BEAR
|
||||
{
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(96), _norm(15)));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Bull at 97% with max tax ----
|
||||
|
||||
function testBull97PercentMaxTax() public view {
|
||||
// deltaS=3, effIdx=29 → penalty=27*29/20=39 < 50 → BULL
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(97), _norm(29)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
// ---- 100% staking always bull ----
|
||||
|
||||
function testAlwaysBullAt100Percent() public view {
|
||||
for (uint256 t = 0; t < 30; t++) {
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(1e18, _norm(t)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Edge: zero inputs ----
|
||||
|
||||
function testAllZeroInputs() public view {
|
||||
OptimizerInput[8] memory inp;
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(inp);
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
// ---- Edge: zero tax at high staking ----
|
||||
|
||||
function testZeroTaxAt99Percent() public view {
|
||||
// deltaS=1, effIdx=0 → penalty=0 < 50 → BULL
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = v3.calculateParams(_inputs(_pct(99), 0));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
// ---- Unused slots are ignored ----
|
||||
|
||||
function testUnusedSlotsIgnored() public view {
|
||||
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) = v3.calculateParams(inp);
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
// ---- Fuzz: no unexpected reverts ----
|
||||
|
||||
function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view {
|
||||
percentageStaked = bound(percentageStaked, 0, 1e18);
|
||||
averageTaxRate = bound(averageTaxRate, 0, 1e18);
|
||||
v3.calculateParams(_inputs(percentageStaked, averageTaxRate));
|
||||
}
|
||||
|
||||
// ---- Fuzz: output always bear or bull ----
|
||||
|
||||
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) =
|
||||
v3.calculateParams(_inputs(96e16, 0));
|
||||
assertEq(ci, 0);
|
||||
assertEq(as_, 1e18);
|
||||
assertEq(aw, 20);
|
||||
assertEq(dd, 1e18);
|
||||
v3.calculateParams(_inputs(percentageStaked, averageTaxRate));
|
||||
|
||||
assertEq(ci, 0, "ci always 0");
|
||||
|
||||
bool isBear = (as_ == BEAR_AS && aw == BEAR_AW && dd == BEAR_DD);
|
||||
bool isBull = (as_ == BULL_AS && aw == BULL_AW && dd == BULL_DD);
|
||||
assertTrue(isBear || isBull, "output must be bear or bull");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue