Merge pull request 'fix: Remove recenterAccess — make recenter() public with TWAP enforcement (#706)' (#713) from fix/issue-706 into master

This commit is contained in:
johba 2026-03-14 10:48:59 +01:00
commit 6ff8282a7e
19 changed files with 241 additions and 330 deletions

View file

@ -41,7 +41,7 @@ contract EthScarcityAbundance is Test {
// Default params: CI=50%, AS=50%, AW=50, DD=50%
optimizer = new ConfigurableOptimizer(5e17, 5e17, 50, 5e17);
(factory, pool, weth, kraiken,, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, true, fees, address(optimizer));
(factory, pool, weth, kraiken,, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, true, address(optimizer));
swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, true);
@ -231,7 +231,7 @@ contract EthScarcityAbundance is Test {
// Bull optimizer: high anchorShare, wide anchor, deep discovery
ConfigurableOptimizer bullOpt = new ConfigurableOptimizer(3e17, 8e17, 80, 8e17);
(,,,,, LiquidityManager bullLm,,) = testEnv.setupEnvironmentWithExistingFactory(factory, true, fees, address(bullOpt));
(,,,,, LiquidityManager bullLm,,) = testEnv.setupEnvironmentWithExistingFactory(factory, true, address(bullOpt));
vm.deal(address(bullLm), 200 ether);
vm.prank(address(bullLm));
@ -252,7 +252,7 @@ contract EthScarcityAbundance is Test {
// Bear optimizer: low anchorShare, moderate anchor, thin discovery
ConfigurableOptimizer bearOpt = new ConfigurableOptimizer(8e17, 1e17, 40, 2e17);
(,,,,, LiquidityManager bearLm,,) = testEnv.setupEnvironmentWithExistingFactory(factory, true, fees, address(bearOpt));
(,,,,, LiquidityManager bearLm,,) = testEnv.setupEnvironmentWithExistingFactory(factory, true, address(bearOpt));
vm.deal(address(bearLm), 200 ether);
vm.prank(address(bearLm));

View file

@ -165,7 +165,7 @@ contract FitnessEvaluator is Test {
/// @dev Account 8 adversary (10 000 ETH in Anvil; funded via vm.deal here)
uint256 internal constant ADV_PK = 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97;
/// @dev Account 2 recenter caller (granted recenterAccess in bootstrap)
/// @dev Account 2 recenter caller (recenter() is now permissionless)
uint256 internal constant RECENTER_PK = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a;
// Runtime state
@ -250,7 +250,7 @@ contract FitnessEvaluator is Test {
bytes32 ERC1967_IMPL = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
vm.store(optProxy, ERC1967_IMPL, bytes32(uint256(uint160(IMPL_SLOT))));
// Bootstrap: fund LM, set recenterAccess, initial recenter.
// Bootstrap: fund LM, initial recenter.
if (!_bootstrap()) {
console.log(string.concat('{"candidate_id":"', candidateId, '","fitness":0,"error":"bootstrap_failed"}'));
continue;
@ -369,18 +369,14 @@ contract FitnessEvaluator is Test {
* @notice Bootstrap LM state for a candidate evaluation (mirrors fitness.sh bootstrap).
*
* Steps (same order as fitness.sh):
* a. Grant recenterAccess to recenterAddr (impersonate feeDestination).
* b. Fund adversary account and wrap ETH WETH.
* c. Transfer 1000 WETH to LM.
* d. Wrap 9000 WETH for adversary trades + set approvals.
* e. Initial recenter (succeeds immediately: recenterAccess set, no ANCHOR liquidity yet).
* a. Fund adversary account and wrap ETH WETH.
* b. Transfer 1000 WETH to LM.
* c. Wrap 9000 WETH for adversary trades + set approvals.
* d. Initial recenter (callable by anyone: cooldown passes because block.timestamp on a
* Base fork is a large value >> 60; TWAP passes because the pool has existing history).
*/
function _bootstrap() internal returns (bool) {
// a. Grant recenterAccess (feeDestination call, no ETH needed with gas_price=0).
vm.prank(FEE_DEST);
LiquidityManager(payable(lmAddr)).setRecenterAccess(recenterAddr);
// b. Fund adversary with ETH.
// a. Fund adversary with ETH.
vm.deal(advAddr, 10_000 ether);
// c. Wrap 1000 ETH WETH and send to LM.
@ -399,8 +395,8 @@ contract FitnessEvaluator is Test {
IERC20(krkAddr).approve(NPM_ADDR, type(uint256).max);
vm.stopPrank();
// e. Initial recenter: no ANCHOR position exists yet so amplitude check is skipped;
// recenterAccess is set so TWAP stability check is also skipped.
// d. Initial recenter: no ANCHOR position exists yet so amplitude check is skipped.
// Cooldown passes (Base fork timestamp >> 60). TWAP passes (existing pool history).
// If all retries fail, revert with a clear message silent failure would make every
// candidate score identically (all lm_eth_total = free WETH only, no positions).
bool recentered = false;

View file

@ -37,7 +37,7 @@ contract FuzzingAnalyzerBugs is Test {
// Bear market params: CI=0.8e18, AS=0.1e18, AW=40, DD=0.2e18
optimizer = new ConfigurableOptimizer(8e17, 1e17, 40, 2e17);
(factory, pool, weth, kraiken,, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, true, fees, address(optimizer));
(factory, pool, weth, kraiken,, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, true, address(optimizer));
swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, true);

View file

@ -148,7 +148,7 @@ contract LiquidityManagerTest is UniSwapHelper {
LiquidityManager _lm,
Optimizer _optimizer,
bool _token0isWeth
) = testEnv.setupEnvironment(token0shouldBeWeth, RECENTER_CALLER);
) = testEnv.setupEnvironment(token0shouldBeWeth);
// Assign to state variables
factory = _factory;
@ -406,12 +406,6 @@ contract LiquidityManagerTest is UniSwapHelper {
_skipAutoSetup = true;
}
/// @notice Grant recenter access for testing (commonly needed)
function _grantRecenterAccess() internal {
vm.prank(feeDestination);
lm.setRecenterAccess(RECENTER_CALLER);
}
/// @notice Setup with custom parameters but standard flow
function _setupCustom(bool token0IsWeth, uint256 accountBalance) internal {
disableAutoSetup();
@ -450,10 +444,6 @@ contract LiquidityManagerTest is UniSwapHelper {
vm.prank(account);
weth.deposit{ value: 15_000 ether }();
// Grant recenter access
vm.prank(feeDestination);
lm.setRecenterAccess(RECENTER_CALLER);
// Setup approvals without creating blocking positions
vm.startPrank(account);
weth.approve(address(lm), type(uint256).max);
@ -947,58 +937,25 @@ contract LiquidityManagerTest is UniSwapHelper {
}
// =========================================================
// COVERAGE TESTS: onlyFeeDestination, revokeRecenterAccess,
// open recenter path, VWAP else branch,
// COVERAGE TESTS: cooldown check, TWAP oracle path, VWAP else branch,
// optimizer fallback, _getKraikenToken/_getWethToken
// =========================================================
/**
* @notice Calling an onlyFeeDestination function from a non-fee address must revert
*/
function testOnlyFeeDestinationReverts() public {
address nonFee = makeAddr("notFeeDestination");
vm.expectRevert("only callable by feeDestination");
vm.prank(nonFee);
lm.setRecenterAccess(nonFee);
}
/**
* @notice feeDestination can revoke recenter access (covers revokeRecenterAccess body)
*/
function testRevokeRecenterAccess() public {
assertEq(lm.recenterAccess(), RECENTER_CALLER, "precondition: access should be set");
vm.prank(feeDestination);
lm.revokeRecenterAccess();
assertEq(lm.recenterAccess(), address(0), "recenterAccess should be revoked");
}
/**
* @notice Open recenter (no access restriction) must fail with cooldown if called too soon
* @notice recenter() must fail with cooldown if called too soon after the last recenter
*/
function testOpenRecenterCooldown() public {
vm.prank(feeDestination);
lm.revokeRecenterAccess();
// Immediately try to recenter without waiting should hit cooldown check
vm.expectRevert("recenter cooldown");
lm.recenter();
}
/**
* @notice After cooldown, open recenter calls _isPriceStable (covering _getPool) then
* hits amplitude check (covers the open-recenter else branch, lines 141-142, 265-266)
* @dev PriceOracle._isPriceStable has a 60,000-second fallback interval.
* setUp warps ~18,000s so the pool's history is only ~18,000s.
* We warp an additional 61,000s so pool history > 60,000s for the fallback to succeed.
* @notice After cooldown, recenter() calls _isPriceStable (covering _getPool) then
* hits amplitude check when price has not moved since last recenter
*/
function testOpenRecenterOracleCheck() public {
vm.prank(feeDestination);
lm.revokeRecenterAccess();
// Warp enough seconds so pool.observe([300,0]) and its fallback ([60000,0]) both succeed.
// Pool was initialized at timestamp 1; after setUp + this warp: ~79,001s of history.
// Warp enough seconds for cooldown + TWAP window (300s).
vm.warp(block.timestamp + 61_000);
// _isPriceStable ( _getPool) is called; price unchanged stable.
@ -1072,7 +1029,7 @@ contract LiquidityManagerTest is UniSwapHelper {
function testOptimizerFallback() public {
RevertingOptimizer revertingOpt = new RevertingOptimizer();
TestEnvironment env = new TestEnvironment(feeDestination);
(,,,,, LiquidityManager _lm,,) = env.setupEnvironmentWithOptimizer(DEFAULT_TOKEN0_IS_WETH, RECENTER_CALLER, address(revertingOpt));
(,,,,, LiquidityManager _lm,,) = env.setupEnvironmentWithOptimizer(DEFAULT_TOKEN0_IS_WETH, address(revertingOpt));
// Recenter uses the fallback params from the catch block
vm.prank(RECENTER_CALLER);
@ -1108,7 +1065,7 @@ contract LiquidityManagerTest is UniSwapHelper {
LiquidityManager _lm,
Optimizer _optimizer,
bool _token0isWeth
) = selfFeeEnv.setupEnvironmentWithSelfFeeDestination(DEFAULT_TOKEN0_IS_WETH, RECENTER_CALLER);
) = selfFeeEnv.setupEnvironmentWithSelfFeeDestination(DEFAULT_TOKEN0_IS_WETH);
// Wire state variables used by buy/sell/recenter helpers
factory = _factory;
@ -1133,6 +1090,9 @@ contract LiquidityManagerTest is UniSwapHelper {
// Move price up with a buy so the second recenter satisfies amplitude requirement
buyRaw(10 ether);
// Warp past cooldown interval; also lets TWAP settle at the post-buy price.
vm.warp(block.timestamp + 301);
// Second recenter: _scrapePositions() burns positions and collects principal KRK
// into the LM's balance. _setPositions() then calls _getOutstandingSupply().
// Without the fix: outstandingSupply() already excludes balanceOf(lm), and
@ -1183,7 +1143,6 @@ contract LiquidityManagerTest is UniSwapHelper {
,
) = clampTestEnv.setupEnvironmentWithOptimizer(
DEFAULT_TOKEN0_IS_WETH,
RECENTER_CALLER,
address(highWidthOptimizer)
);

View file

@ -36,7 +36,7 @@ contract ReplayProfitableScenario is Test {
BullMarketOptimizer optimizer = new BullMarketOptimizer();
// Use seed 1 setup (odd seed = false for first param)
(, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithOptimizer(false, feeDestination, address(optimizer));
(, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithOptimizer(false, address(optimizer));
// Fund exactly as in the recorded scenario
vm.deal(address(lm), 200 ether);

View file

@ -40,7 +40,7 @@ contract SupplyCorruptionTest is UniSwapHelper {
LiquidityManager _lm,
Optimizer _optimizer,
bool _token0isWeth
) = testEnv.setupEnvironment(false, RECENTER_CALLER);
) = testEnv.setupEnvironment(false);
factory = _factory;
pool = _pool;
@ -83,6 +83,7 @@ contract SupplyCorruptionTest is UniSwapHelper {
performSwap(5 ether, true);
console.log("Performed 5 ETH swap to move price");
vm.warp(block.timestamp + 301); // TWAP catches up to post-swap price; cooldown passes
// Call recenter
vm.prank(RECENTER_CALLER);
@ -126,6 +127,7 @@ contract SupplyCorruptionTest is UniSwapHelper {
console.log("Initial supply:", initialTotalSupply);
// Perform multiple recenter cycles
uint256 ts = block.timestamp; // track time explicitly to avoid Forge block.timestamp reset
for (uint256 i = 0; i < 3; i++) {
// Swap to move price
vm.deal(account, 2 ether);
@ -133,6 +135,8 @@ contract SupplyCorruptionTest is UniSwapHelper {
weth.deposit{ value: 2 ether }();
performSwap(2 ether, true);
ts += 301; // TWAP catches up; cooldown passes
vm.warp(ts);
vm.prank(RECENTER_CALLER);
lm.recenter();

View file

@ -34,7 +34,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
function setUp() public {
testEnv = new TestEnvironment(feeDestination);
(,pool, weth, harberg, , lm, , token0isWeth) =
testEnv.setupEnvironment(false, RECENTER_CALLER);
testEnv.setupEnvironment(false);
vm.deal(address(lm), LM_ETH);
@ -69,6 +69,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
// ---- step 2: first buy + recenter bootstrap ----
buyRaw(25 ether); // push price up enough to satisfy amplitude check
vm.warp(block.timestamp + 301); // TWAP catches up to post-buy price; cooldown passes
vm.prank(RECENTER_CALLER);
lm.recenter(); // cumulativeVolume == 0 shouldRecordVWAP = true (bootstrap path)
@ -78,8 +79,11 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
// ---- step 3: continued buy-only cycles ----
uint256 successfulBuyCycles;
uint256 ts = block.timestamp; // track explicitly to avoid Forge block.timestamp reset
for (uint256 i = 0; i < 10; i++) {
buyRaw(25 ether);
ts += 301; // TWAP catches up; cooldown passes
vm.warp(ts);
vm.prank(RECENTER_CALLER);
// Recenter may fail if amplitude isn't reached; that's fine.
try lm.recenter() {
@ -114,12 +118,16 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
// Bootstrap via first buy-recenter
buyRaw(25 ether);
vm.warp(block.timestamp + 301); // TWAP catches up; cooldown passes
vm.prank(RECENTER_CALLER);
lm.recenter();
// Run several buy cycles
uint256 ts = block.timestamp; // track explicitly to avoid Forge block.timestamp reset
for (uint256 i = 0; i < 6; i++) {
buyRaw(25 ether);
ts += 301; // TWAP catches up; cooldown passes
vm.warp(ts);
vm.prank(RECENTER_CALLER);
try lm.recenter() { } catch { }
}
@ -160,6 +168,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
assertEq(lm.cumulativeVolume(), 0, "no VWAP data before first fees");
buyRaw(25 ether);
vm.warp(block.timestamp + 301); // TWAP catches up to post-buy price; cooldown passes
vm.prank(RECENTER_CALLER);
lm.recenter();
@ -188,7 +197,10 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
vm.prank(RECENTER_CALLER);
lm.recenter();
uint256 ts = block.timestamp; // track explicitly to avoid Forge block.timestamp reset
buyRaw(25 ether);
ts += 301; // TWAP catches up to post-buy price; cooldown passes
vm.warp(ts);
vm.prank(RECENTER_CALLER);
lm.recenter();
@ -199,6 +211,8 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
}
// Recenter with price now lower (sell direction) must not revert
ts += 301; // TWAP catches up to post-sell price; cooldown passes
vm.warp(ts);
vm.prank(RECENTER_CALLER);
try lm.recenter() {
// success sell-direction recenter works
@ -239,6 +253,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
// 25 ether against a 100 ETH LM pool reliably satisfies the amplitude check
// (same amount used across other bootstrap tests in this file).
buyRaw(25 ether);
vm.warp(block.timestamp + 301); // TWAP catches up to post-buy price; cooldown passes
// Step 3: Second recenter bootstrap path records VWAP.
vm.prank(RECENTER_CALLER);

View file

@ -91,7 +91,6 @@ contract TestEnvironment is TestConstants {
/**
* @notice Deploy all contracts and set up the environment
* @param token0shouldBeWeth Whether WETH should be token0
* @param recenterCaller Address that will be granted recenter access
* @return _factory The deployed Uniswap factory
* @return _pool The created Uniswap pool
* @return _weth The WETH token contract
@ -101,10 +100,7 @@ contract TestEnvironment is TestConstants {
* @return _optimizer The optimizer contract
* @return _token0isWeth Whether token0 is WETH
*/
function setupEnvironment(
bool token0shouldBeWeth,
address recenterCaller
)
function setupEnvironment(bool token0shouldBeWeth)
external
returns (
IUniswapV3Factory _factory,
@ -132,10 +128,6 @@ contract TestEnvironment is TestConstants {
// Configure permissions
_configurePermissions();
// Grant recenter access to specified caller
vm.prank(feeDestination);
lm.setRecenterAccess(recenterCaller);
return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth);
}
@ -172,11 +164,14 @@ contract TestEnvironment is TestConstants {
/**
* @notice Create and initialize the Uniswap pool
* @dev Warp 301 seconds after pool init so _isPriceStable()'s 300-second TWAP window
* has sufficient history for any subsequent recenter() call.
*/
function _createAndInitializePool() internal {
pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE));
token0isWeth = address(weth) < address(harberg);
pool.initializePoolFor1Cent(token0isWeth);
vm.warp(block.timestamp + 301);
}
/**
@ -202,7 +197,6 @@ contract TestEnvironment is TestConstants {
/**
* @notice Setup environment with specific optimizer
* @param token0shouldBeWeth Whether WETH should be token0
* @param recenterCaller Address that will be granted recenter access
* @param optimizerAddress Address of the optimizer to use
* @return _factory The deployed Uniswap factory
* @return _pool The created Uniswap pool
@ -213,11 +207,7 @@ contract TestEnvironment is TestConstants {
* @return _optimizer The optimizer contract
* @return _token0isWeth Whether token0 is WETH
*/
function setupEnvironmentWithOptimizer(
bool token0shouldBeWeth,
address recenterCaller,
address optimizerAddress
)
function setupEnvironmentWithOptimizer(bool token0shouldBeWeth, address optimizerAddress)
external
returns (
IUniswapV3Factory _factory,
@ -248,10 +238,6 @@ contract TestEnvironment is TestConstants {
// Configure permissions
_configurePermissions();
// Grant recenter access to specified caller
vm.prank(feeDestination);
lm.setRecenterAccess(recenterCaller);
return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth);
}
@ -262,12 +248,8 @@ contract TestEnvironment is TestConstants {
* _getOutstandingSupply skips the feeDestination KRK subtraction (already excluded
* by outstandingSupply()).
* @param token0shouldBeWeth Whether WETH should be token0
* @param recenterCaller Address that will be granted recenter access
*/
function setupEnvironmentWithSelfFeeDestination(
bool token0shouldBeWeth,
address recenterCaller
)
function setupEnvironmentWithSelfFeeDestination(bool token0shouldBeWeth)
external
returns (
IUniswapV3Factory _factory,
@ -299,10 +281,6 @@ contract TestEnvironment is TestConstants {
harberg.setLiquidityManager(address(lm));
vm.deal(address(lm), INITIAL_LM_ETH_BALANCE);
// feeDestination IS address(lm), so prank as lm to grant recenter access
vm.prank(address(lm));
lm.setRecenterAccess(recenterCaller);
return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth);
}
@ -310,7 +288,6 @@ contract TestEnvironment is TestConstants {
* @notice Setup environment with existing factory and specific optimizer
* @param existingFactory The existing Uniswap factory to use
* @param token0shouldBeWeth Whether WETH should be token0
* @param recenterCaller Address that will be granted recenter access
* @param optimizerAddress Address of the optimizer to use
* @return _factory The existing Uniswap factory
* @return _pool The created Uniswap pool
@ -324,7 +301,6 @@ contract TestEnvironment is TestConstants {
function setupEnvironmentWithExistingFactory(
IUniswapV3Factory existingFactory,
bool token0shouldBeWeth,
address recenterCaller,
address optimizerAddress
)
external
@ -357,10 +333,6 @@ contract TestEnvironment is TestConstants {
// Configure permissions
_configurePermissions();
// Grant recenter access to specified caller
vm.prank(feeDestination);
lm.setRecenterAccess(recenterCaller);
return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth);
}
}