233 lines
10 KiB
Solidity
233 lines
10 KiB
Solidity
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
|
pragma solidity ^0.8.19;
|
||
|
|
|
||
|
|
import { OptimizerV3 } from "../src/OptimizerV3.sol";
|
||
|
|
import { Stake } from "../src/Stake.sol";
|
||
|
|
import "forge-std/Test.sol";
|
||
|
|
|
||
|
|
contract OptimizerV3Test is Test {
|
||
|
|
OptimizerV3 optimizer;
|
||
|
|
|
||
|
|
// TAX_RATES from Stake.sol
|
||
|
|
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;
|
||
|
|
|
||
|
|
function setUp() public {
|
||
|
|
// Deploy without initialization (we only test pure functions)
|
||
|
|
optimizer = new OptimizerV3();
|
||
|
|
}
|
||
|
|
|
||
|
|
function _normalizedTaxRate(uint256 taxRateIndex) internal view returns (uint256) {
|
||
|
|
return TAX_RATES[taxRateIndex] * 1e18 / MAX_TAX;
|
||
|
|
}
|
||
|
|
|
||
|
|
function _percentageStaked(uint256 pct) internal pure returns (uint256) {
|
||
|
|
return pct * 1e18 / 100;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== Always Bear (staked <= 91%) ====================
|
||
|
|
|
||
|
|
function testAlwaysBearAt0Percent() public view {
|
||
|
|
for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) {
|
||
|
|
assertFalse(optimizer.isBullMarket(0, _normalizedTaxRate(taxIdx)), "0% staked should always be bear");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function testAlwaysBearAt50Percent() public view {
|
||
|
|
for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) {
|
||
|
|
assertFalse(optimizer.isBullMarket(_percentageStaked(50), _normalizedTaxRate(taxIdx)), "50% staked should always be bear");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function testAlwaysBearAt91Percent() public view {
|
||
|
|
for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) {
|
||
|
|
assertFalse(optimizer.isBullMarket(_percentageStaked(91), _normalizedTaxRate(taxIdx)), "91% staked should always be bear");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function testAlwaysBearAt39Percent() public view {
|
||
|
|
assertFalse(optimizer.isBullMarket(_percentageStaked(39), _normalizedTaxRate(0)), "39% staked should be bear");
|
||
|
|
}
|
||
|
|
|
||
|
|
function testAlwaysBearAt80Percent() public view {
|
||
|
|
assertFalse(optimizer.isBullMarket(_percentageStaked(80), _normalizedTaxRate(0)), "80% staked should be bear even with lowest tax");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 92% Boundary ====================
|
||
|
|
|
||
|
|
function testBoundary92PercentLowestTax() public view {
|
||
|
|
// deltaS=8, effIdx=0 → penalty = 512*0/20 = 0 < 50 → BULL
|
||
|
|
assertTrue(optimizer.isBullMarket(_percentageStaked(92), _normalizedTaxRate(0)), "92% staked, lowest tax should be bull");
|
||
|
|
}
|
||
|
|
|
||
|
|
function testBoundary92PercentTaxIdx1() public view {
|
||
|
|
// deltaS=8, effIdx=1 → penalty = 512*1/20 = 25 < 50 → BULL
|
||
|
|
assertTrue(optimizer.isBullMarket(_percentageStaked(92), _normalizedTaxRate(1)), "92% staked, taxIdx=1 should be bull");
|
||
|
|
}
|
||
|
|
|
||
|
|
function testBoundary92PercentTaxIdx2() public view {
|
||
|
|
// deltaS=8, effIdx=2 → penalty = 512*2/20 = 51 >= 50 → BEAR
|
||
|
|
assertFalse(optimizer.isBullMarket(_percentageStaked(92), _normalizedTaxRate(2)), "92% staked, taxIdx=2 should be bear");
|
||
|
|
}
|
||
|
|
|
||
|
|
function testBoundary92PercentHighTax() public view {
|
||
|
|
// deltaS=8, effIdx=29 → penalty = 512*29/20 = 742 → BEAR
|
||
|
|
assertFalse(optimizer.isBullMarket(_percentageStaked(92), _normalizedTaxRate(29)), "92% staked, max tax should be bear");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 95% Staked ====================
|
||
|
|
|
||
|
|
function testAt95PercentLowTax() public view {
|
||
|
|
// deltaS=5, effIdx=0 → penalty = 125*0/20 = 0 < 50 → BULL
|
||
|
|
assertTrue(optimizer.isBullMarket(_percentageStaked(95), _normalizedTaxRate(0)), "95% staked, lowest tax should be bull");
|
||
|
|
}
|
||
|
|
|
||
|
|
function testAt95PercentTaxIdx7() public view {
|
||
|
|
// deltaS=5, effIdx=7 → penalty = 125*7/20 = 43 < 50 → BULL
|
||
|
|
assertTrue(optimizer.isBullMarket(_percentageStaked(95), _normalizedTaxRate(7)), "95% staked, taxIdx=7 should be bull");
|
||
|
|
}
|
||
|
|
|
||
|
|
function testAt95PercentTaxIdx8() public view {
|
||
|
|
// deltaS=5, effIdx=8 → penalty = 125*8/20 = 50, NOT < 50 → BEAR
|
||
|
|
assertFalse(optimizer.isBullMarket(_percentageStaked(95), _normalizedTaxRate(8)), "95% staked, taxIdx=8 should be bear");
|
||
|
|
}
|
||
|
|
|
||
|
|
function testAt95PercentTaxIdx9() public view {
|
||
|
|
// deltaS=5, effIdx=9 → penalty = 125*9/20 = 56 → BEAR
|
||
|
|
assertFalse(optimizer.isBullMarket(_percentageStaked(95), _normalizedTaxRate(9)), "95% staked, taxIdx=9 should be bear");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 97% Staked ====================
|
||
|
|
|
||
|
|
function testAt97PercentLowTax() public view {
|
||
|
|
// deltaS=3, effIdx=0 → penalty = 27*0/20 = 0 < 50 → BULL
|
||
|
|
assertTrue(optimizer.isBullMarket(_percentageStaked(97), _normalizedTaxRate(0)), "97% staked, lowest tax should be bull");
|
||
|
|
}
|
||
|
|
|
||
|
|
function testAt97PercentHighTax() public view {
|
||
|
|
// deltaS=3, effIdx=29 → penalty = 27*29/20 = 39 < 50 → BULL
|
||
|
|
assertTrue(optimizer.isBullMarket(_percentageStaked(97), _normalizedTaxRate(29)), "97% staked, max tax should still be bull");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 100% Staked ====================
|
||
|
|
|
||
|
|
function testAt100PercentAlwaysBull() public view {
|
||
|
|
// deltaS=0 → penalty = 0 → always BULL
|
||
|
|
for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) {
|
||
|
|
assertTrue(optimizer.isBullMarket(1e18, _normalizedTaxRate(taxIdx)), "100% staked should always be bull");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 96% Sweep ====================
|
||
|
|
|
||
|
|
function testAt96PercentSweep() public view {
|
||
|
|
// deltaS=4, cubic=64
|
||
|
|
// penalty = 64 * effIdx / 20
|
||
|
|
// Bull when penalty < 50, i.e., 64 * effIdx / 20 < 50 → effIdx < 15.625
|
||
|
|
// effIdx 0-15: bull (penalty 0..48). effIdx 16+: bear (penalty 51+)
|
||
|
|
for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) {
|
||
|
|
bool result = optimizer.isBullMarket(_percentageStaked(96), _normalizedTaxRate(taxIdx));
|
||
|
|
// Compute expected: effIdx from the tax rate
|
||
|
|
uint256 effIdx = taxIdx;
|
||
|
|
if (taxIdx >= 14) {
|
||
|
|
effIdx = taxIdx + 1;
|
||
|
|
if (effIdx > 29) effIdx = 29;
|
||
|
|
}
|
||
|
|
uint256 penalty = 64 * effIdx / 20;
|
||
|
|
bool expectedBull = penalty < 50;
|
||
|
|
assertEq(result, expectedBull, string.concat("96% sweep mismatch at taxIdx=", vm.toString(taxIdx)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 94% Sweep ====================
|
||
|
|
|
||
|
|
function testAt94PercentSweep() public view {
|
||
|
|
// deltaS=6, cubic=216
|
||
|
|
// penalty = 216 * effIdx / 20
|
||
|
|
// Bull when penalty < 50, i.e., 216 * effIdx / 20 < 50 → effIdx < 4.629
|
||
|
|
// effIdx 0-4: bull. effIdx 5+: bear.
|
||
|
|
for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) {
|
||
|
|
bool result = optimizer.isBullMarket(_percentageStaked(94), _normalizedTaxRate(taxIdx));
|
||
|
|
uint256 effIdx = taxIdx;
|
||
|
|
if (taxIdx >= 14) {
|
||
|
|
effIdx = taxIdx + 1;
|
||
|
|
if (effIdx > 29) effIdx = 29;
|
||
|
|
}
|
||
|
|
uint256 penalty = 216 * effIdx / 20;
|
||
|
|
bool expectedBull = penalty < 50;
|
||
|
|
assertEq(result, expectedBull, string.concat("94% sweep mismatch at taxIdx=", vm.toString(taxIdx)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== Revert on Invalid Input ====================
|
||
|
|
|
||
|
|
function testRevertsAbove100Percent() public {
|
||
|
|
vm.expectRevert("Invalid percentage staked");
|
||
|
|
optimizer.isBullMarket(1e18 + 1, 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 93% Staked ====================
|
||
|
|
|
||
|
|
function testAt93PercentSweep() public view {
|
||
|
|
// deltaS=7, cubic=343
|
||
|
|
// penalty = 343 * effIdx / 20
|
||
|
|
// Bull when penalty < 50, i.e., 343 * effIdx / 20 < 50 → effIdx < 2.915
|
||
|
|
// effIdx 0-2: bull. effIdx 3+: bear.
|
||
|
|
for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) {
|
||
|
|
bool result = optimizer.isBullMarket(_percentageStaked(93), _normalizedTaxRate(taxIdx));
|
||
|
|
uint256 effIdx = taxIdx;
|
||
|
|
if (taxIdx >= 14) {
|
||
|
|
effIdx = taxIdx + 1;
|
||
|
|
if (effIdx > 29) effIdx = 29;
|
||
|
|
}
|
||
|
|
uint256 penalty = 343 * effIdx / 20;
|
||
|
|
bool expectedBull = penalty < 50;
|
||
|
|
assertEq(result, expectedBull, string.concat("93% sweep mismatch at taxIdx=", vm.toString(taxIdx)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== 99% Staked ====================
|
||
|
|
|
||
|
|
function testAt99PercentAlwaysBull() public view {
|
||
|
|
// deltaS=1, cubic=1 → penalty = effIdx/20, always < 50 for effIdx <= 29
|
||
|
|
for (uint256 taxIdx = 0; taxIdx < 30; taxIdx++) {
|
||
|
|
assertTrue(optimizer.isBullMarket(_percentageStaked(99), _normalizedTaxRate(taxIdx)), "99% staked should always be bull");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== EffIdx Shift at Boundary (taxIdx 13 vs 14) ====================
|
||
|
|
|
||
|
|
function testEffIdxShiftAtBoundary() public view {
|
||
|
|
// At 96% staked, deltaS=4, cubic=64
|
||
|
|
// taxIdx=13: effIdx=13, penalty = 64*13/20 = 41 < 50 → BULL
|
||
|
|
assertTrue(optimizer.isBullMarket(_percentageStaked(96), _normalizedTaxRate(13)), "taxIdx=13 should be bull at 96%");
|
||
|
|
|
||
|
|
// taxIdx=14: effIdx=15 (shift!), penalty = 64*15/20 = 48 < 50 → BULL
|
||
|
|
assertTrue(optimizer.isBullMarket(_percentageStaked(96), _normalizedTaxRate(14)), "taxIdx=14 should be bull at 96% (effIdx shift)");
|
||
|
|
|
||
|
|
// taxIdx=15: effIdx=16, penalty = 64*16/20 = 51 >= 50 → BEAR
|
||
|
|
assertFalse(optimizer.isBullMarket(_percentageStaked(96), _normalizedTaxRate(15)), "taxIdx=15 should be bear at 96%");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ==================== Fuzz Tests ====================
|
||
|
|
|
||
|
|
function testFuzzBearBelow92(uint256 percentageStaked, uint256 taxIdx) public view {
|
||
|
|
percentageStaked = bound(percentageStaked, 0, 91e18 / 100);
|
||
|
|
taxIdx = bound(taxIdx, 0, 29);
|
||
|
|
assertFalse(optimizer.isBullMarket(percentageStaked, _normalizedTaxRate(taxIdx)), "Should always be bear below 92%");
|
||
|
|
}
|
||
|
|
|
||
|
|
function testFuzz100PercentAlwaysBull(uint256 taxIdx) public view {
|
||
|
|
taxIdx = bound(taxIdx, 0, 29);
|
||
|
|
assertTrue(optimizer.isBullMarket(1e18, _normalizedTaxRate(taxIdx)), "100% staked should always be bull");
|
||
|
|
}
|
||
|
|
|
||
|
|
function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view {
|
||
|
|
percentageStaked = bound(percentageStaked, 0, 1e18);
|
||
|
|
averageTaxRate = bound(averageTaxRate, 0, 1e18);
|
||
|
|
// Should not revert
|
||
|
|
optimizer.isBullMarket(percentageStaked, averageTaxRate);
|
||
|
|
}
|
||
|
|
}
|