diff --git a/onchain/script/DeployBase.sol b/onchain/script/DeployBase.sol index 5a87d62..de9065c 100644 --- a/onchain/script/DeployBase.sol +++ b/onchain/script/DeployBase.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "../src/Kraiken.sol"; @@ -6,61 +7,14 @@ import { LiquidityManager } from "../src/LiquidityManager.sol"; import "../src/Optimizer.sol"; import "../src/Stake.sol"; import "../src/helpers/UniswapHelpers.sol"; -import { IWETH9 } from "../src/interfaces/IWETH9.sol"; import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "forge-std/Script.sol"; +import "./DeployCommon.sol"; uint24 constant FEE = uint24(10_000); -/** - * @title SeedSwapper - * @notice One-shot helper deployed during DeployBase.run() to perform the initial seed buy. - * Executing a small buy before the protocol opens eliminates the cumulativeVolume==0 - * front-run window: after the seed recenter, VWAP has a real anchor and the bootstrap - * path in LiquidityManager.recenter() is never reachable by external users. - */ -contract SeedSwapper { - IWETH9 private immutable weth; - IUniswapV3Pool private immutable pool; - bool private immutable token0isWeth; - - constructor(address _weth, address _pool, bool _token0isWeth) { - weth = IWETH9(_weth); - pool = IUniswapV3Pool(_pool); - token0isWeth = _token0isWeth; - } - - /// @notice Wraps msg.value ETH to WETH and swaps it for KRK (buying KRK). - /// The KRK output is sent to `recipient`. The fee generated by the swap - /// is captured in the LM's positions, so the subsequent recenter() call - /// will collect a non-zero ethFee and record VWAP. - function executeSeedBuy(address recipient) external payable { - weth.deposit{ value: msg.value }(); - - // zeroForOne=true when WETH is token0: sell token0(WETH) → token1(KRK) - // zeroForOne=false when WETH is token1: sell token1(WETH) → token0(KRK) - bool zeroForOne = token0isWeth; - - // Price limits: allow the swap to reach the extreme of the range. - uint160 priceLimit = zeroForOne - ? 4295128740 // TickMath.MIN_SQRT_RATIO + 1 - : 1461446703485210103287273052203988822378723970341; // TickMath.MAX_SQRT_RATIO - 1 - - pool.swap(recipient, zeroForOne, int256(msg.value), priceLimit, ""); - } - - /// @notice Uniswap V3 callback: pay the WETH owed for the seed buy. - function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external { - require(msg.sender == address(pool), "only pool"); - int256 wethDelta = token0isWeth ? amount0Delta : amount1Delta; - if (wethDelta > 0) { - weth.transfer(msg.sender, uint256(wethDelta)); - } - } -} - contract DeployBase is Script { using UniswapHelpers for IUniswapV3Pool; diff --git a/onchain/script/DeployCommon.sol b/onchain/script/DeployCommon.sol new file mode 100644 index 0000000..5a5557b --- /dev/null +++ b/onchain/script/DeployCommon.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import { IWETH9 } from "../src/interfaces/IWETH9.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; + +/** + * @title SeedSwapper + * @notice One-shot helper deployed during Deploy*.run() to perform the initial seed buy. + * Executing a small buy before the protocol opens eliminates the cumulativeVolume==0 + * front-run window: after the seed recenter, VWAP has a real anchor and the bootstrap + * path in LiquidityManager.recenter() is never reachable by external users. + * + * @dev Deployed by both DeployLocal and DeployBase; extracted here to avoid duplicate + * Foundry artifacts and drift between the two deploy scripts. + */ +contract SeedSwapper { + // TickMath sentinel values — the pool will consume as much input as available in-range. + uint160 private constant SQRT_PRICE_LIMIT_MIN = 4295128740; // TickMath.MIN_SQRT_RATIO + 1 + uint160 private constant SQRT_PRICE_LIMIT_MAX = // TickMath.MAX_SQRT_RATIO - 1 + 1461446703485210103287273052203988822378723970341; + + IWETH9 private immutable weth; + IUniswapV3Pool private immutable pool; + bool private immutable token0isWeth; + + constructor(address _weth, address _pool, bool _token0isWeth) { + weth = IWETH9(_weth); + pool = IUniswapV3Pool(_pool); + token0isWeth = _token0isWeth; + } + + /// @notice Wraps msg.value ETH to WETH, swaps it for KRK (buying KRK), and sweeps any + /// unconsumed WETH back to `recipient`. The fee generated by the swap is captured + /// in the LM's anchor position so the subsequent recenter() call will collect a + /// non-zero ethFee and record VWAP. + /// + /// @dev If the pool exhausts in-range liquidity before spending all WETH (price-limit + /// sentinel reached), `wethDelta < msg.value` in the callback and the remainder + /// stays as WETH in this contract. The post-swap sweep returns it to `recipient` + /// so no funds are stranded. + function executeSeedBuy(address recipient) external payable { + weth.deposit{ value: msg.value }(); + + // zeroForOne=true when WETH is token0: sell token0(WETH) -> token1(KRK) + // zeroForOne=false when WETH is token1: sell token1(WETH) -> token0(KRK) + bool zeroForOne = token0isWeth; + uint160 priceLimit = zeroForOne ? SQRT_PRICE_LIMIT_MIN : SQRT_PRICE_LIMIT_MAX; + + pool.swap(recipient, zeroForOne, int256(msg.value), priceLimit, ""); + + // Sweep any unconsumed WETH to recipient (occurs when price limit is reached + // before the full input is spent). + uint256 leftover = weth.balanceOf(address(this)); + if (leftover > 0) { + require(weth.transfer(recipient, leftover), "SeedSwapper: WETH sweep failed"); + } + } + + /// @notice Uniswap V3 callback: pay the WETH owed for the seed buy. + function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external { + require(msg.sender == address(pool), "SeedSwapper: only pool"); + int256 wethDelta = token0isWeth ? amount0Delta : amount1Delta; + if (wethDelta > 0) { + require(weth.transfer(msg.sender, uint256(wethDelta)), "SeedSwapper: WETH transfer failed"); + } + } +} diff --git a/onchain/script/DeployLocal.sol b/onchain/script/DeployLocal.sol index 01bd841..d294974 100644 --- a/onchain/script/DeployLocal.sol +++ b/onchain/script/DeployLocal.sol @@ -7,59 +7,11 @@ import { LiquidityManager } from "../src/LiquidityManager.sol"; import "../src/Optimizer.sol"; import "../src/Stake.sol"; import "../src/helpers/UniswapHelpers.sol"; -import { IWETH9 } from "../src/interfaces/IWETH9.sol"; import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "forge-std/Script.sol"; - -/** - * @title SeedSwapper - * @notice One-shot helper deployed during DeployLocal.run() to perform the initial seed buy. - * Executing a small buy before the protocol opens eliminates the cumulativeVolume==0 - * front-run window: after the seed recenter, VWAP has a real anchor and the bootstrap - * path in LiquidityManager.recenter() is never reachable by external users. - */ -contract SeedSwapper { - IWETH9 private immutable weth; - IUniswapV3Pool private immutable pool; - bool private immutable token0isWeth; - - constructor(address _weth, address _pool, bool _token0isWeth) { - weth = IWETH9(_weth); - pool = IUniswapV3Pool(_pool); - token0isWeth = _token0isWeth; - } - - /// @notice Wraps msg.value ETH to WETH and swaps it for KRK (buying KRK). - /// The KRK output is sent to `recipient`. The fee generated by the swap - /// is captured in the LM's positions, so the subsequent recenter() call - /// will collect a non-zero ethFee and record VWAP. - function executeSeedBuy(address recipient) external payable { - weth.deposit{ value: msg.value }(); - - // zeroForOne=true when WETH is token0: sell token0(WETH) → token1(KRK) - // zeroForOne=false when WETH is token1: sell token1(WETH) → token0(KRK) - bool zeroForOne = token0isWeth; - - // Price limits: allow the swap to reach the extreme of the range. - // TickMath.MIN_SQRT_RATIO + 1 and MAX_SQRT_RATIO - 1 are the standard sentinels. - uint160 priceLimit = zeroForOne - ? 4295128740 // TickMath.MIN_SQRT_RATIO + 1 - : 1461446703485210103287273052203988822378723970341; // TickMath.MAX_SQRT_RATIO - 1 - - pool.swap(recipient, zeroForOne, int256(msg.value), priceLimit, ""); - } - - /// @notice Uniswap V3 callback: pay the WETH owed for the seed buy. - function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external { - require(msg.sender == address(pool), "only pool"); - int256 wethDelta = token0isWeth ? amount0Delta : amount1Delta; - if (wethDelta > 0) { - weth.transfer(msg.sender, uint256(wethDelta)); - } - } -} +import "./DeployCommon.sol"; /** * @title DeployLocal diff --git a/scripts/bootstrap-common.sh b/scripts/bootstrap-common.sh index 35aaf00..16b396f 100755 --- a/scripts/bootstrap-common.sh +++ b/scripts/bootstrap-common.sh @@ -130,6 +130,9 @@ call_recenter() { local cumvol cumvol="$(cast call --rpc-url "$ANVIL_RPC" \ "$LIQUIDITY_MANAGER" "cumulativeVolume()(uint256)" 2>/dev/null || echo "0")" + # Normalise to decimal: cast returns decimal for typed calls but cast to-dec handles + # both decimal and hex (0x...) inputs, guarding against future cast output changes. + cumvol="$(cast to-dec "$cumvol" 2>/dev/null || echo "0")" if [[ "$cumvol" != "0" && -n "$cumvol" ]]; then bootstrap_log "VWAP already bootstrapped by deploy script (cumulativeVolume=$cumvol) -- skipping initial recenter" return 0