fix: Remove recenterAccess — make recenter() public with TWAP enforcement (#706)
This commit is contained in:
parent
860b56f216
commit
1a410a30b7
13 changed files with 94 additions and 180 deletions
|
|
@ -136,7 +136,6 @@ main() {
|
|||
extract_addresses
|
||||
write_contracts_env
|
||||
fund_liquidity_manager
|
||||
grant_recenter_access
|
||||
call_recenter
|
||||
seed_application_state
|
||||
write_deployments_json
|
||||
|
|
|
|||
|
|
@ -108,33 +108,42 @@ contract DeployBase is Script {
|
|||
// Fix: execute a small buy BEFORE handing control to users so that
|
||||
// cumulativeVolume>0 by the time the protocol is live.
|
||||
//
|
||||
// recenter() is now permissionless and always enforces TWAP stability.
|
||||
// For a fresh pool on Base mainnet this bootstrap must run at least
|
||||
// 300 seconds after pool initialisation (so the TWAP oracle has history).
|
||||
// If the pool was just created in this same script run, the first
|
||||
// recenter() will revert with "price deviated from oracle" — wait 5 min
|
||||
// and call the bootstrap as a separate transaction or script.
|
||||
//
|
||||
// Deployer must have SEED_LM_ETH + SEED_SWAP_ETH available (≈0.015 ETH).
|
||||
// =====================================================================
|
||||
console.log("\nBootstrapping VWAP with seed trade...");
|
||||
|
||||
// Step 1: Temporarily set deployer as feeDestination to call setRecenterAccess.
|
||||
liquidityManager.setFeeDestination(sender);
|
||||
liquidityManager.setRecenterAccess(sender);
|
||||
// Step 1: Set the real feeDestination before any recenter.
|
||||
liquidityManager.setFeeDestination(feeDest);
|
||||
console.log("feeDestination set to", feeDest);
|
||||
|
||||
// Step 2: Fund LM and place initial bootstrap positions.
|
||||
// NOTE: recenter() requires TWAP history (>= 300s since pool init).
|
||||
// On Base mainnet this call will revert if the pool is too fresh.
|
||||
(bool funded,) = address(liquidityManager).call{ value: SEED_LM_ETH }("");
|
||||
require(funded, "Failed to fund LM for seed bootstrap");
|
||||
liquidityManager.recenter();
|
||||
console.log("First recenter complete -> positions placed, cumulativeVolume still 0");
|
||||
|
||||
// Step 3: Seed buy -> generates a non-zero fee in the anchor position.
|
||||
SeedSwapper seedSwapper = new SeedSwapper(weth, address(pool), token0isWeth);
|
||||
seedSwapper.executeSeedBuy{ value: SEED_SWAP_ETH }(sender);
|
||||
console.log("Seed buy executed -> fee generated in anchor position");
|
||||
|
||||
// Step 4: Second recenter records VWAP (bootstrap path + ethFee > 0).
|
||||
// NOTE: Must be called >= 60s after the first recenter (cooldown) AND
|
||||
// >= 300s after pool init so TWAP has settled at post-buy price.
|
||||
// On Base mainnet, mine/wait ~5 min between Step 2 and Step 4.
|
||||
liquidityManager.recenter();
|
||||
require(liquidityManager.cumulativeVolume() > 0, "VWAP bootstrap failed: cumulativeVolume is 0");
|
||||
console.log("VWAP bootstrapped -> cumulativeVolume:", liquidityManager.cumulativeVolume());
|
||||
|
||||
// Step 5: Clean up -> revoke temporary access and set the real feeDestination.
|
||||
liquidityManager.revokeRecenterAccess();
|
||||
liquidityManager.setFeeDestination(feeDest);
|
||||
console.log("recenterAccess revoked, feeDestination set to", feeDest);
|
||||
|
||||
console.log("\n=== Deployment Complete ===");
|
||||
console.log("Kraiken:", address(kraiken));
|
||||
console.log("Stake:", address(stake));
|
||||
|
|
@ -143,8 +152,7 @@ contract DeployBase is Script {
|
|||
console.log("Optimizer:", optimizerAddress);
|
||||
console.log("\nPost-deploy steps:");
|
||||
console.log(" 1. Fund LiquidityManager with operational ETH (VWAP already bootstrapped)");
|
||||
console.log(" 2. Set recenterAccess to txnBot: lm.setRecenterAccess(txnBot) from feeDestination");
|
||||
console.log(" 3. txnBot can now call recenter()");
|
||||
console.log(" 2. recenter() is now permissionless - any address (e.g. txnBot) can call it.");
|
||||
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,24 +126,22 @@ contract DeployLocal is Script {
|
|||
// cumulativeVolume>0 by the time the protocol is live.
|
||||
//
|
||||
// Sequence:
|
||||
// 1. Temporarily make sender the feeDestination (deployer can do this
|
||||
// because setFeeDestination is gated on deployer, not feeDestination).
|
||||
// This allows sender to call setRecenterAccess.
|
||||
// 1. Warp 301 seconds forward so the pool's TWAP oracle has enough history
|
||||
// for _isPriceStable() to succeed (requires >= 300s of observations).
|
||||
// 2. Fund LM with SEED_LM_ETH and call recenter() -> places thin initial
|
||||
// positions; no fees collected yet, so cumulativeVolume stays 0.
|
||||
// 3. Execute seed buy via SeedSwapper -> generates a non-zero WETH fee
|
||||
// in the anchor position and moves the tick >400 (minimum amplitude).
|
||||
// 4. Call recenter() again -> cumulativeVolume==0 triggers the bootstrap
|
||||
// 4. Warp 301 more seconds so TWAP catches up to the post-buy price and
|
||||
// cooldown (60s) elapses between the two recenters.
|
||||
// 5. Call recenter() again -> cumulativeVolume==0 triggers the bootstrap
|
||||
// path (shouldRecordVWAP=true); ethFee>0 → _recordVolumeAndPrice fires
|
||||
// → cumulativeVolume>0. VWAP is now anchored to the real launch price.
|
||||
// 5. Revoke recenterAccess and restore the real feeDestination.
|
||||
// =====================================================================
|
||||
console.log("\n[7/7] Bootstrapping VWAP with seed trade...");
|
||||
|
||||
// Step 1: Grant deployer temporary feeDestination role to enable setRecenterAccess.
|
||||
liquidityManager.setFeeDestination(sender);
|
||||
liquidityManager.setRecenterAccess(sender);
|
||||
console.log(" Temporary recenterAccess granted to deployer");
|
||||
// Step 1: Advance time so TWAP oracle has sufficient history.
|
||||
vm.warp(block.timestamp + 301);
|
||||
|
||||
// Step 2: Fund LM and place initial bootstrap positions.
|
||||
(bool funded,) = address(liquidityManager).call{ value: SEED_LM_ETH }("");
|
||||
|
|
@ -156,17 +154,19 @@ contract DeployLocal is Script {
|
|||
seedSwapper.executeSeedBuy{ value: SEED_SWAP_ETH }(sender);
|
||||
console.log(" Seed buy executed -> fee generated in anchor position");
|
||||
|
||||
// Step 4: Second recenter records VWAP (bootstrap path + ethFee > 0).
|
||||
// Step 4: Warp forward so TWAP settles at post-buy price and cooldown elapses.
|
||||
vm.warp(block.timestamp + 301);
|
||||
|
||||
// Step 5: Second recenter records VWAP (bootstrap path + ethFee > 0).
|
||||
liquidityManager.recenter();
|
||||
require(liquidityManager.cumulativeVolume() > 0, "VWAP bootstrap failed: cumulativeVolume is 0");
|
||||
console.log(" Second recenter complete -> VWAP bootstrapped");
|
||||
console.log(" cumulativeVolume:", liquidityManager.cumulativeVolume());
|
||||
console.log(" VWAP (X96):", liquidityManager.getVWAP());
|
||||
|
||||
// Step 5: Clean up -> revoke temporary access and set the real feeDestination.
|
||||
liquidityManager.revokeRecenterAccess();
|
||||
// Set the real feeDestination now that bootstrap is complete.
|
||||
liquidityManager.setFeeDestination(feeDest);
|
||||
console.log(" recenterAccess revoked, feeDestination restored to", feeDest);
|
||||
console.log(" feeDestination set to", feeDest);
|
||||
|
||||
// Print deployment summary
|
||||
console.log("\n=== Deployment Summary ===");
|
||||
|
|
@ -180,9 +180,8 @@ contract DeployLocal is Script {
|
|||
console.log("VWAP is already bootstrapped. To go live:");
|
||||
console.log("1. Fund LiquidityManager with operational ETH (current balance includes seed):");
|
||||
console.log(" cast send", address(liquidityManager), "--value 10ether");
|
||||
console.log("2. Grant recenterAccess to txnBot (call from feeDestination):");
|
||||
console.log(" cast send", address(liquidityManager), "\"setRecenterAccess(address)\" <txnBotAddr>");
|
||||
console.log("3. txnBot can now call recenter() to rebalance positions.");
|
||||
console.log("2. recenter() is now permissionless - any address (e.g. txnBot) can call it.");
|
||||
console.log(" TWAP manipulation protection is always enforced (no bypass path).");
|
||||
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,14 +143,13 @@ contract BacktestRunner is Script {
|
|||
// ------------------------------------------------------------------
|
||||
KrAIkenSystem memory sys = KrAIkenDeployer.deploy(address(sp.factory), address(mockWeth), address(krk), sender, initialCapital);
|
||||
|
||||
// Deploy StrategyExecutor and grant it recenter access on the LM.
|
||||
// recenterAccess bypasses TWAP stability check and cooldown — correct
|
||||
// for simulation where vm.warp drives time, not a real oracle.
|
||||
// sender == feeDestination, so the onlyFeeDestination guard is satisfied.
|
||||
// Deploy StrategyExecutor — recenter() is now permissionless, so no
|
||||
// access grant is needed. StrategyExecutor.maybeRecenter() calls
|
||||
// recenter() via try/catch and logs "SKIP" on cooldown/TWAP failures.
|
||||
// vm.warp in EventReplayer drives time so TWAP and cooldown pass.
|
||||
bool token0isWeth = sp.token0 == address(mockWeth);
|
||||
StrategyExecutor executor =
|
||||
new StrategyExecutor(sys.lm, IERC20(address(mockWeth)), IERC20(address(krk)), sender, recenterInterval, sp.pool, token0isWeth);
|
||||
sys.lm.setRecenterAccess(address(executor));
|
||||
|
||||
// Deploy baseline strategies and initialize with the same capital as KrAIken.
|
||||
BaselineStrategies baselines =
|
||||
|
|
|
|||
|
|
@ -49,16 +49,15 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
|
||||
/// @notice Access control and fee management
|
||||
address private immutable deployer;
|
||||
address public recenterAccess;
|
||||
address public feeDestination;
|
||||
bool public feeDestinationLocked;
|
||||
|
||||
/// @notice Last recenter tick — used to determine net trade direction between recenters
|
||||
int24 public lastRecenterTick;
|
||||
|
||||
/// @notice Last recenter timestamp — rate limits open recenters.
|
||||
/// @notice Last recenter timestamp — rate limits recenters.
|
||||
uint256 public lastRecenterTime;
|
||||
/// @notice Minimum seconds between open recenters (when recenterAccess is unset)
|
||||
/// @notice Minimum seconds between recenters
|
||||
uint256 internal constant MIN_RECENTER_INTERVAL = 60;
|
||||
/// @notice Target observation cardinality requested from the pool during construction
|
||||
uint16 internal constant ORACLE_CARDINALITY = 100;
|
||||
|
|
@ -69,12 +68,6 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
/// @notice Custom errors
|
||||
error ZeroAddressInSetter();
|
||||
|
||||
/// @notice Access control modifier
|
||||
modifier onlyFeeDestination() {
|
||||
require(msg.sender == address(feeDestination), "only callable by feeDestination");
|
||||
_;
|
||||
}
|
||||
|
||||
/// @notice Constructor initializes all contract references and pool configuration
|
||||
/// @param _factory The address of the Uniswap V3 factory
|
||||
/// @param _WETH9 The address of the WETH contract
|
||||
|
|
@ -142,26 +135,15 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
}
|
||||
}
|
||||
|
||||
/// @notice Sets recenter access for testing/emergency purposes
|
||||
/// @param addr Address to grant recenter access
|
||||
function setRecenterAccess(address addr) external onlyFeeDestination {
|
||||
recenterAccess = addr;
|
||||
}
|
||||
|
||||
/// @notice Revokes recenter access
|
||||
function revokeRecenterAccess() external onlyFeeDestination {
|
||||
recenterAccess = address(0);
|
||||
}
|
||||
|
||||
/// @notice Adjusts liquidity positions in response to price movements.
|
||||
/// Callable by anyone. Always enforces cooldown and TWAP price stability.
|
||||
/// This function either completes a full recenter (removing all positions,
|
||||
/// recording VWAP where applicable, and redeploying liquidity) or reverts —
|
||||
/// it never returns silently without acting.
|
||||
///
|
||||
/// @dev Revert conditions (no silent false return for failure):
|
||||
/// - "access denied" — recenterAccess is set and caller is not that address
|
||||
/// - "recenter cooldown" — recenterAccess is unset and MIN_RECENTER_INTERVAL has not elapsed
|
||||
/// - "price deviated from oracle" — recenterAccess is unset and price is outside TWAP bounds
|
||||
/// - "recenter cooldown" — MIN_RECENTER_INTERVAL has not elapsed since last recenter
|
||||
/// - "price deviated from oracle" — price is outside TWAP bounds (manipulation guard)
|
||||
/// - "amplitude not reached." — anchor position exists but price has not moved far enough
|
||||
/// from the anchor centre to warrant repositioning
|
||||
///
|
||||
|
|
@ -173,13 +155,9 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|||
function recenter() external returns (bool isUp) {
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
// Validate access and price stability
|
||||
if (recenterAccess != address(0)) {
|
||||
require(msg.sender == recenterAccess, "access denied");
|
||||
} else {
|
||||
// Always enforce cooldown and TWAP price stability — no bypass path
|
||||
require(block.timestamp >= lastRecenterTime + MIN_RECENTER_INTERVAL, "recenter cooldown");
|
||||
require(_isPriceStable(currentTick), "price deviated from oracle");
|
||||
}
|
||||
lastRecenterTime = block.timestamp;
|
||||
|
||||
// Check if price movement is sufficient for recentering
|
||||
|
|
|
|||
|
|
@ -163,7 +163,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 ────────────────────────────────────────────────────────
|
||||
|
|
@ -248,7 +248,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;
|
||||
|
|
@ -367,18 +367,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.
|
||||
|
|
@ -397,8 +393,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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -132,10 +132,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 +168,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -248,10 +247,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);
|
||||
}
|
||||
|
||||
|
|
@ -299,10 +294,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);
|
||||
}
|
||||
|
||||
|
|
@ -357,10 +348,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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,21 +100,6 @@ fund_liquidity_manager() {
|
|||
"$LIQUIDITY_MANAGER" --value 10ether >>"$LOG_FILE" 2>&1
|
||||
}
|
||||
|
||||
grant_recenter_access() {
|
||||
bootstrap_log "Granting recenter access to deployer"
|
||||
cast rpc --rpc-url "$ANVIL_RPC" anvil_impersonateAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1
|
||||
cast send --rpc-url "$ANVIL_RPC" --from "$FEE_DEST" --unlocked \
|
||||
"$LIQUIDITY_MANAGER" "setRecenterAccess(address)" "$DEPLOYER_ADDR" >>"$LOG_FILE" 2>&1
|
||||
cast rpc --rpc-url "$ANVIL_RPC" anvil_stopImpersonatingAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1
|
||||
|
||||
if [[ -n "${TXNBOT_ADDRESS:-}" ]]; then
|
||||
bootstrap_log "Granting recenter access to txnBot ($TXNBOT_ADDRESS)"
|
||||
cast rpc --rpc-url "$ANVIL_RPC" anvil_impersonateAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1
|
||||
cast send --rpc-url "$ANVIL_RPC" --from "$FEE_DEST" --unlocked \
|
||||
"$LIQUIDITY_MANAGER" "setRecenterAccess(address)" "$TXNBOT_ADDRESS" >>"$LOG_FILE" 2>&1
|
||||
cast rpc --rpc-url "$ANVIL_RPC" anvil_stopImpersonatingAccount "$FEE_DEST" >>"$LOG_FILE" 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
call_recenter() {
|
||||
local recenter_pk="$DEPLOYER_PK"
|
||||
|
|
|
|||
|
|
@ -57,9 +57,6 @@ cat "$ONCHAIN_DIR/deployments-local.json"
|
|||
echo "=== Funding LiquidityManager ==="
|
||||
fund_liquidity_manager
|
||||
|
||||
echo "=== Granting recenter access ==="
|
||||
grant_recenter_access
|
||||
|
||||
echo "=== Calling recenter() to seed liquidity ==="
|
||||
call_recenter
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ DEPLOYMENTS="$REPO_ROOT/onchain/deployments-local.json"
|
|||
# ── Anvil accounts ─────────────────────────────────────────────────────────────
|
||||
# Account 8 — adversary (10k ETH, 0 KRK)
|
||||
ADV_PK=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97
|
||||
# Account 2 — recenter caller (granted recenterAccess by bootstrap)
|
||||
# Account 2 — recenter caller (recenter() is permissionless; any account can call it)
|
||||
RECENTER_PK=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
|
||||
|
||||
# ── Infrastructure constants ───────────────────────────────────────────────────
|
||||
|
|
@ -116,21 +116,8 @@ POOL=$("$CAST" call "$V3_FACTORY" "getPool(address,address,uint24)(address)" \
|
|||
"$WETH" "$KRK" "$POOL_FEE" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
log " Pool: $POOL"
|
||||
|
||||
# ── 3a. Grant recenterAccess FIRST (while original feeDestination is still set) ──
|
||||
FEE_DEST=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL") \
|
||||
|| die "Failed to read feeDestination() from LM"
|
||||
FEE_DEST=$(echo "$FEE_DEST" | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
log "Granting recenterAccess to account 2 ($RECENTER_ADDR) via feeDestination ($FEE_DEST) ..."
|
||||
"$CAST" rpc --rpc-url "$RPC_URL" anvil_impersonateAccount "$FEE_DEST" \
|
||||
|| die "anvil_impersonateAccount $FEE_DEST failed"
|
||||
"$CAST" send --rpc-url "$RPC_URL" --from "$FEE_DEST" --unlocked \
|
||||
"$LM" "setRecenterAccess(address)" "$RECENTER_ADDR" >/dev/null 2>&1 \
|
||||
|| die "setRecenterAccess($RECENTER_ADDR) failed"
|
||||
"$CAST" rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST" \
|
||||
|| die "anvil_stopImpersonatingAccount $FEE_DEST failed"
|
||||
log " recenterAccess granted"
|
||||
|
||||
# ── 3b. Set feeDestination to LM itself (fees accrue as liquidity) ─────────────
|
||||
# ── 3a. Set feeDestination to LM itself (fees accrue as liquidity) ─────────────
|
||||
# recenter() is now permissionless — no setRecenterAccess() call needed.
|
||||
# setFeeDestination allows repeated EOA sets; setting to a contract locks it permanently.
|
||||
# The deployer (Anvil account 0) deployed LiquidityManager and may call setFeeDestination again.
|
||||
# DEPLOYER_PK is Anvil's deterministic account-0 key — valid ONLY against a local ephemeral
|
||||
|
|
@ -144,7 +131,7 @@ VERIFY=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL" | s
|
|||
log " feeDestination set to: $VERIFY"
|
||||
[[ "${VERIFY,,}" == "${LM,,}" ]] || die "feeDestination verification failed: expected $LM, got $VERIFY"
|
||||
|
||||
# ── 3c. Fund LM with 1000 ETH and deploy into positions via recenter ───────────
|
||||
# ── 3b. Fund LM with 1000 ETH and deploy into positions via recenter ───────────
|
||||
# Send ETH as WETH (LM uses WETH internally), then recenter to deploy into positions.
|
||||
# Without recenter, the ETH sits idle and the first recenter mints massive KRK.
|
||||
log "Funding LM with 1000 ETH ..."
|
||||
|
|
@ -174,7 +161,7 @@ LM_ETH=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]
|
|||
LM_WETH=$("$CAST" call "$WETH" "balanceOf(address)(uint256)" "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
log " LM after recenter: ETH=$LM_ETH WETH=$LM_WETH"
|
||||
|
||||
# ── 4. Take Anvil snapshot (clean baseline, includes recenterAccess grant) ─────
|
||||
# ── 4. Take Anvil snapshot (clean baseline) ────────────────────────────────────
|
||||
log "Taking Anvil snapshot..."
|
||||
SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
|
||||
log " Snapshot ID: $SNAP"
|
||||
|
|
@ -419,7 +406,7 @@ CAST binary: /home/debian/.foundry/bin/cast
|
|||
### Recenter caller — Anvil account 2
|
||||
- Address: ${RECENTER_ADDR}
|
||||
- Private key: ${RECENTER_PK}
|
||||
- Has recenterAccess on LiquidityManager
|
||||
- Can call recenter() (permissionless — 60s cooldown + TWAP check enforced)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -446,7 +433,7 @@ to rebalance, then re-deploys positions at the current price. It:
|
|||
- Can mint NEW KRK (increasing supply → decreasing floor)
|
||||
- Can burn KRK (decreasing supply → increasing floor)
|
||||
- Moves ETH between positions
|
||||
Only recenterAccess account can call it.
|
||||
recenter() is permissionless — any account can call it (subject to 60s cooldown and TWAP check).
|
||||
|
||||
### Staking
|
||||
\`Stake.snatch(assets, receiver, taxRateIndex, positionsToSnatch)\`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue