fix: address AI review findings for #706 recenterAccess removal
- DeployBase.sol: remove broken inline second recenter() (would always revert with 'recenter cooldown' in same Forge broadcast); replace with operator instructions to run the new BootstrapVWAPPhase2.s.sol script at least 60 s after deployment - BootstrapVWAPPhase2.s.sol: new script for the second VWAP bootstrap recenter on Base mainnet deployments - StrategyExecutor.sol: update stale docstring that still described the removed recenterAccess bypass; reflect permissionless model with vm.warp - TestBase.sol: remove vestigial recenterCaller parameter from all four setupEnvironment* functions (parameter was silently ignored after setRecenterAccess was removed); update all callers across six test files - bootstrap-common.sh: fix misleading retry recenter in seed_application_state() — add evm_increaseTime 61 before evm_mine so the recenter cooldown actually clears and the retry can succeed All 210 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9b53f409b7
commit
0d3aee15b4
11 changed files with 71 additions and 42 deletions
45
onchain/script/BootstrapVWAPPhase2.s.sol
Normal file
45
onchain/script/BootstrapVWAPPhase2.s.sol
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
pragma solidity ^0.8.19;
|
||||||
|
|
||||||
|
import { LiquidityManager } from "../src/LiquidityManager.sol";
|
||||||
|
import "forge-std/Script.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title BootstrapVWAPPhase2
|
||||||
|
* @notice Second phase of the VWAP bootstrap for Base mainnet deployments.
|
||||||
|
*
|
||||||
|
* Run this script >= 60 seconds after DeployBase (or DeployBaseMainnet/DeployBaseSepolia)
|
||||||
|
* finishes. The first recenter() sets lastRecenterTime; the 60-second cooldown must
|
||||||
|
* elapse before this second recenter() can succeed.
|
||||||
|
*
|
||||||
|
* What this does:
|
||||||
|
* - Calls liquidityManager.recenter() a second time.
|
||||||
|
* - At this point cumulativeVolume == 0 (bootstrap path) and the seed buy has
|
||||||
|
* generated ethFee > 0, so recenter() records the VWAP anchor.
|
||||||
|
* - Asserts cumulativeVolume > 0 to confirm bootstrap success.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* export LM_ADDRESS=<deployed LiquidityManager address>
|
||||||
|
* forge script script/BootstrapVWAPPhase2.s.sol --tc BootstrapVWAPPhase2 \
|
||||||
|
* --fork-url $BASE_RPC --broadcast
|
||||||
|
*/
|
||||||
|
contract BootstrapVWAPPhase2 is Script {
|
||||||
|
function run() public {
|
||||||
|
address lmAddress = vm.envAddress("LM_ADDRESS");
|
||||||
|
LiquidityManager lm = LiquidityManager(payable(lmAddress));
|
||||||
|
|
||||||
|
string memory seedPhrase = vm.readFile(".secret");
|
||||||
|
uint256 privateKey = vm.deriveKey(seedPhrase, 0);
|
||||||
|
vm.startBroadcast(privateKey);
|
||||||
|
|
||||||
|
console.log("Running VWAP bootstrap phase 2 on LiquidityManager:", lmAddress);
|
||||||
|
|
||||||
|
lm.recenter();
|
||||||
|
|
||||||
|
uint256 cumVol = lm.cumulativeVolume();
|
||||||
|
require(cumVol > 0, "VWAP bootstrap failed: cumulativeVolume is still 0");
|
||||||
|
console.log("VWAP bootstrapped successfully. cumulativeVolume:", cumVol);
|
||||||
|
|
||||||
|
vm.stopBroadcast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -137,12 +137,9 @@ contract DeployBase is Script {
|
||||||
console.log("Seed buy executed -> fee generated in anchor position");
|
console.log("Seed buy executed -> fee generated in anchor position");
|
||||||
|
|
||||||
// Step 4: Second recenter records VWAP (bootstrap path + ethFee > 0).
|
// Step 4: Second recenter records VWAP (bootstrap path + ethFee > 0).
|
||||||
// NOTE: Must be called >= 60s after the first recenter (cooldown) AND
|
// Cannot be called in the same Forge broadcast as Step 2 — recenter() enforces a
|
||||||
// >= 300s after pool init so TWAP has settled at post-buy price.
|
// 60-second cooldown and there is no time-warp mechanism in a live broadcast.
|
||||||
// On Base mainnet, mine/wait ~5 min between Step 2 and Step 4.
|
// Run BootstrapVWAPPhase2.s.sol at least 60 seconds after this script completes.
|
||||||
liquidityManager.recenter();
|
|
||||||
require(liquidityManager.cumulativeVolume() > 0, "VWAP bootstrap failed: cumulativeVolume is 0");
|
|
||||||
console.log("VWAP bootstrapped -> cumulativeVolume:", liquidityManager.cumulativeVolume());
|
|
||||||
|
|
||||||
console.log("\n=== Deployment Complete ===");
|
console.log("\n=== Deployment Complete ===");
|
||||||
console.log("Kraiken:", address(kraiken));
|
console.log("Kraiken:", address(kraiken));
|
||||||
|
|
@ -151,8 +148,11 @@ contract DeployBase is Script {
|
||||||
console.log("LiquidityManager:", address(liquidityManager));
|
console.log("LiquidityManager:", address(liquidityManager));
|
||||||
console.log("Optimizer:", optimizerAddress);
|
console.log("Optimizer:", optimizerAddress);
|
||||||
console.log("\nPost-deploy steps:");
|
console.log("\nPost-deploy steps:");
|
||||||
console.log(" 1. Fund LiquidityManager with operational ETH (VWAP already bootstrapped)");
|
console.log(" 1. Wait >= 60 s after this script finishes.");
|
||||||
console.log(" 2. recenter() is now permissionless - any address (e.g. txnBot) can call it.");
|
console.log(" 2. Run: forge script script/BootstrapVWAPPhase2.s.sol --tc BootstrapVWAPPhase2 --fork-url <RPC> --broadcast");
|
||||||
|
console.log(" This performs the second recenter that records cumulativeVolume > 0.");
|
||||||
|
console.log(" 3. Fund LiquidityManager with operational ETH.");
|
||||||
|
console.log(" 4. recenter() is permissionless - any address (e.g. txnBot) can call it.");
|
||||||
|
|
||||||
vm.stopBroadcast();
|
vm.stopBroadcast();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ import { console2 } from "forge-std/console2.sol";
|
||||||
* notified on every block (for time-in-range) and on each successful recenter
|
* notified on every block (for time-in-range) and on each successful recenter
|
||||||
* (for position lifecycle and fee/IL accounting).
|
* (for position lifecycle and fee/IL accounting).
|
||||||
*
|
*
|
||||||
* Access model: StrategyExecutor must be set as recenterAccess on the LM so that
|
* Access model: recenter() is permissionless — no special access grant is required.
|
||||||
* the cooldown and TWAP price-stability checks are bypassed in the simulation
|
* EventReplayer advances block.timestamp via vm.warp, so the 60-second cooldown and
|
||||||
* (vm.warp advances simulated time, not real oracle state).
|
* the 300-second TWAP window pass normally during simulation.
|
||||||
*
|
*
|
||||||
* TODO(#319): The negligible-impact assumption means we replay historical events
|
* TODO(#319): The negligible-impact assumption means we replay historical events
|
||||||
* as-is without accounting for KrAIken's own liquidity affecting swap outcomes.
|
* as-is without accounting for KrAIken's own liquidity affecting swap outcomes.
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ contract EthScarcityAbundance is Test {
|
||||||
// Default params: CI=50%, AS=50%, AW=50, DD=50%
|
// Default params: CI=50%, AS=50%, AW=50, DD=50%
|
||||||
optimizer = new ConfigurableOptimizer(5e17, 5e17, 50, 5e17);
|
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);
|
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
|
// Bull optimizer: high anchorShare, wide anchor, deep discovery
|
||||||
ConfigurableOptimizer bullOpt = new ConfigurableOptimizer(3e17, 8e17, 80, 8e17);
|
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.deal(address(bullLm), 200 ether);
|
||||||
vm.prank(address(bullLm));
|
vm.prank(address(bullLm));
|
||||||
|
|
@ -252,7 +252,7 @@ contract EthScarcityAbundance is Test {
|
||||||
// Bear optimizer: low anchorShare, moderate anchor, thin discovery
|
// Bear optimizer: low anchorShare, moderate anchor, thin discovery
|
||||||
ConfigurableOptimizer bearOpt = new ConfigurableOptimizer(8e17, 1e17, 40, 2e17);
|
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.deal(address(bearLm), 200 ether);
|
||||||
vm.prank(address(bearLm));
|
vm.prank(address(bearLm));
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ contract FuzzingAnalyzerBugs is Test {
|
||||||
// Bear market params: CI=0.8e18, AS=0.1e18, AW=40, DD=0.2e18
|
// Bear market params: CI=0.8e18, AS=0.1e18, AW=40, DD=0.2e18
|
||||||
optimizer = new ConfigurableOptimizer(8e17, 1e17, 40, 2e17);
|
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);
|
swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, true);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ contract LiquidityManagerTest is UniSwapHelper {
|
||||||
LiquidityManager _lm,
|
LiquidityManager _lm,
|
||||||
Optimizer _optimizer,
|
Optimizer _optimizer,
|
||||||
bool _token0isWeth
|
bool _token0isWeth
|
||||||
) = testEnv.setupEnvironment(token0shouldBeWeth, RECENTER_CALLER);
|
) = testEnv.setupEnvironment(token0shouldBeWeth);
|
||||||
|
|
||||||
// Assign to state variables
|
// Assign to state variables
|
||||||
factory = _factory;
|
factory = _factory;
|
||||||
|
|
@ -1029,7 +1029,7 @@ contract LiquidityManagerTest is UniSwapHelper {
|
||||||
function testOptimizerFallback() public {
|
function testOptimizerFallback() public {
|
||||||
RevertingOptimizer revertingOpt = new RevertingOptimizer();
|
RevertingOptimizer revertingOpt = new RevertingOptimizer();
|
||||||
TestEnvironment env = new TestEnvironment(feeDestination);
|
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
|
// Recenter uses the fallback params from the catch block
|
||||||
vm.prank(RECENTER_CALLER);
|
vm.prank(RECENTER_CALLER);
|
||||||
|
|
@ -1065,7 +1065,7 @@ contract LiquidityManagerTest is UniSwapHelper {
|
||||||
LiquidityManager _lm,
|
LiquidityManager _lm,
|
||||||
Optimizer _optimizer,
|
Optimizer _optimizer,
|
||||||
bool _token0isWeth
|
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
|
// Wire state variables used by buy/sell/recenter helpers
|
||||||
factory = _factory;
|
factory = _factory;
|
||||||
|
|
@ -1143,7 +1143,6 @@ contract LiquidityManagerTest is UniSwapHelper {
|
||||||
,
|
,
|
||||||
) = clampTestEnv.setupEnvironmentWithOptimizer(
|
) = clampTestEnv.setupEnvironmentWithOptimizer(
|
||||||
DEFAULT_TOKEN0_IS_WETH,
|
DEFAULT_TOKEN0_IS_WETH,
|
||||||
RECENTER_CALLER,
|
|
||||||
address(highWidthOptimizer)
|
address(highWidthOptimizer)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ contract ReplayProfitableScenario is Test {
|
||||||
BullMarketOptimizer optimizer = new BullMarketOptimizer();
|
BullMarketOptimizer optimizer = new BullMarketOptimizer();
|
||||||
|
|
||||||
// Use seed 1 setup (odd seed = false for first param)
|
// 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
|
// Fund exactly as in the recorded scenario
|
||||||
vm.deal(address(lm), 200 ether);
|
vm.deal(address(lm), 200 ether);
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ contract SupplyCorruptionTest is UniSwapHelper {
|
||||||
LiquidityManager _lm,
|
LiquidityManager _lm,
|
||||||
Optimizer _optimizer,
|
Optimizer _optimizer,
|
||||||
bool _token0isWeth
|
bool _token0isWeth
|
||||||
) = testEnv.setupEnvironment(false, RECENTER_CALLER);
|
) = testEnv.setupEnvironment(false);
|
||||||
|
|
||||||
factory = _factory;
|
factory = _factory;
|
||||||
pool = _pool;
|
pool = _pool;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
|
||||||
function setUp() public {
|
function setUp() public {
|
||||||
testEnv = new TestEnvironment(feeDestination);
|
testEnv = new TestEnvironment(feeDestination);
|
||||||
(,pool, weth, harberg, , lm, , token0isWeth) =
|
(,pool, weth, harberg, , lm, , token0isWeth) =
|
||||||
testEnv.setupEnvironment(false, RECENTER_CALLER);
|
testEnv.setupEnvironment(false);
|
||||||
|
|
||||||
vm.deal(address(lm), LM_ETH);
|
vm.deal(address(lm), LM_ETH);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,6 @@ contract TestEnvironment is TestConstants {
|
||||||
/**
|
/**
|
||||||
* @notice Deploy all contracts and set up the environment
|
* @notice Deploy all contracts and set up the environment
|
||||||
* @param token0shouldBeWeth Whether WETH should be token0
|
* @param token0shouldBeWeth Whether WETH should be token0
|
||||||
* @param recenterCaller Address that will be granted recenter access
|
|
||||||
* @return _factory The deployed Uniswap factory
|
* @return _factory The deployed Uniswap factory
|
||||||
* @return _pool The created Uniswap pool
|
* @return _pool The created Uniswap pool
|
||||||
* @return _weth The WETH token contract
|
* @return _weth The WETH token contract
|
||||||
|
|
@ -101,10 +100,7 @@ contract TestEnvironment is TestConstants {
|
||||||
* @return _optimizer The optimizer contract
|
* @return _optimizer The optimizer contract
|
||||||
* @return _token0isWeth Whether token0 is WETH
|
* @return _token0isWeth Whether token0 is WETH
|
||||||
*/
|
*/
|
||||||
function setupEnvironment(
|
function setupEnvironment(bool token0shouldBeWeth)
|
||||||
bool token0shouldBeWeth,
|
|
||||||
address recenterCaller
|
|
||||||
)
|
|
||||||
external
|
external
|
||||||
returns (
|
returns (
|
||||||
IUniswapV3Factory _factory,
|
IUniswapV3Factory _factory,
|
||||||
|
|
@ -201,7 +197,6 @@ contract TestEnvironment is TestConstants {
|
||||||
/**
|
/**
|
||||||
* @notice Setup environment with specific optimizer
|
* @notice Setup environment with specific optimizer
|
||||||
* @param token0shouldBeWeth Whether WETH should be token0
|
* @param token0shouldBeWeth Whether WETH should be token0
|
||||||
* @param recenterCaller Address that will be granted recenter access
|
|
||||||
* @param optimizerAddress Address of the optimizer to use
|
* @param optimizerAddress Address of the optimizer to use
|
||||||
* @return _factory The deployed Uniswap factory
|
* @return _factory The deployed Uniswap factory
|
||||||
* @return _pool The created Uniswap pool
|
* @return _pool The created Uniswap pool
|
||||||
|
|
@ -212,11 +207,7 @@ contract TestEnvironment is TestConstants {
|
||||||
* @return _optimizer The optimizer contract
|
* @return _optimizer The optimizer contract
|
||||||
* @return _token0isWeth Whether token0 is WETH
|
* @return _token0isWeth Whether token0 is WETH
|
||||||
*/
|
*/
|
||||||
function setupEnvironmentWithOptimizer(
|
function setupEnvironmentWithOptimizer(bool token0shouldBeWeth, address optimizerAddress)
|
||||||
bool token0shouldBeWeth,
|
|
||||||
address recenterCaller,
|
|
||||||
address optimizerAddress
|
|
||||||
)
|
|
||||||
external
|
external
|
||||||
returns (
|
returns (
|
||||||
IUniswapV3Factory _factory,
|
IUniswapV3Factory _factory,
|
||||||
|
|
@ -257,12 +248,8 @@ contract TestEnvironment is TestConstants {
|
||||||
* _getOutstandingSupply skips the feeDestination KRK subtraction (already excluded
|
* _getOutstandingSupply skips the feeDestination KRK subtraction (already excluded
|
||||||
* by outstandingSupply()).
|
* by outstandingSupply()).
|
||||||
* @param token0shouldBeWeth Whether WETH should be token0
|
* @param token0shouldBeWeth Whether WETH should be token0
|
||||||
* @param recenterCaller Address that will be granted recenter access
|
|
||||||
*/
|
*/
|
||||||
function setupEnvironmentWithSelfFeeDestination(
|
function setupEnvironmentWithSelfFeeDestination(bool token0shouldBeWeth)
|
||||||
bool token0shouldBeWeth,
|
|
||||||
address recenterCaller
|
|
||||||
)
|
|
||||||
external
|
external
|
||||||
returns (
|
returns (
|
||||||
IUniswapV3Factory _factory,
|
IUniswapV3Factory _factory,
|
||||||
|
|
@ -301,7 +288,6 @@ contract TestEnvironment is TestConstants {
|
||||||
* @notice Setup environment with existing factory and specific optimizer
|
* @notice Setup environment with existing factory and specific optimizer
|
||||||
* @param existingFactory The existing Uniswap factory to use
|
* @param existingFactory The existing Uniswap factory to use
|
||||||
* @param token0shouldBeWeth Whether WETH should be token0
|
* @param token0shouldBeWeth Whether WETH should be token0
|
||||||
* @param recenterCaller Address that will be granted recenter access
|
|
||||||
* @param optimizerAddress Address of the optimizer to use
|
* @param optimizerAddress Address of the optimizer to use
|
||||||
* @return _factory The existing Uniswap factory
|
* @return _factory The existing Uniswap factory
|
||||||
* @return _pool The created Uniswap pool
|
* @return _pool The created Uniswap pool
|
||||||
|
|
@ -315,7 +301,6 @@ contract TestEnvironment is TestConstants {
|
||||||
function setupEnvironmentWithExistingFactory(
|
function setupEnvironmentWithExistingFactory(
|
||||||
IUniswapV3Factory existingFactory,
|
IUniswapV3Factory existingFactory,
|
||||||
bool token0shouldBeWeth,
|
bool token0shouldBeWeth,
|
||||||
address recenterCaller,
|
|
||||||
address optimizerAddress
|
address optimizerAddress
|
||||||
)
|
)
|
||||||
external
|
external
|
||||||
|
|
|
||||||
|
|
@ -196,8 +196,8 @@ seed_application_state() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bootstrap_log "Swap returned 0 KRK — recentering and retrying"
|
bootstrap_log "Swap returned 0 KRK — recentering and retrying"
|
||||||
# Mine a few blocks to advance time, then recenter
|
# Advance 61 s to clear the 60-second recenter cooldown, then mine a block.
|
||||||
cast rpc --rpc-url "$ANVIL_RPC" evm_mine >>"$LOG_FILE" 2>&1 || true
|
cast rpc --rpc-url "$ANVIL_RPC" evm_increaseTime 61 >>"$LOG_FILE" 2>&1 || true
|
||||||
cast rpc --rpc-url "$ANVIL_RPC" evm_mine >>"$LOG_FILE" 2>&1 || true
|
cast rpc --rpc-url "$ANVIL_RPC" evm_mine >>"$LOG_FILE" 2>&1 || true
|
||||||
local recenter_pk="${TXNBOT_PRIVATE_KEY:-$DEPLOYER_PK}"
|
local recenter_pk="${TXNBOT_PRIVATE_KEY:-$DEPLOYER_PK}"
|
||||||
cast send --rpc-url "$ANVIL_RPC" --private-key "$recenter_pk" \
|
cast send --rpc-url "$ANVIL_RPC" --private-key "$recenter_pk" \
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue