// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; import {console} from "forge-std/console.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; import {SqrtPriceMath} from "@uniswap/v4-core/src/libraries/SqrtPriceMath.sol"; import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; import {IPyth} from "@pythnetwork/pyth-sdk-solidity/IPyth.sol"; import {MockToken} from "../src/mocks/MockToken.sol"; import {PriceMath} from "../src/libraries/PriceMath.sol"; import {PriceSource} from "./lib/PriceSource.sol"; /// @notice Moves every spot pool to the live onchain Pyth price (exact-in swap capped at the target sqrtPrice). /// Pools are full-range and seeded with MockTokens, so the swap input is minted to the broadcaster. /// Env: PYTH (required), SPOT_CONFIG (default ../backend/config/spot.json), SYNC_TOLERANCE_BPS (default 5). contract SyncPools is PriceSource { using StateLibrary for IPoolManager; using PoolIdLibrary for PoolKey; /// @dev Upper bound on the dynamic LP fee a swap may pay (VaultFeeHook public fee = 3000 pips). uint256 internal constant MAX_FEE_PIPS = 3_000; struct Result { uint256 before6; uint256 target6; uint256 after6; bool swapped; } function run() external virtual { string memory cfg = vm.readFile(vm.envOr("SPOT_CONFIG", string("../backend/config/spot.json"))); IPoolManager pm = IPoolManager(vm.parseJsonAddress(cfg, ".poolManager")); PoolSwapTest router = PoolSwapTest(vm.parseJsonAddress(cfg, ".swapRouter")); IPyth pyth = IPyth(vm.envAddress("PYTH")); uint256 tol = vm.envOr("SYNC_TOLERANCE_BPS", uint256(5)); Feed[4] memory f = feeds(); vm.startBroadcast(); (, address me,) = vm.readCallers(); for (uint256 i; i < 4; i++) { // feed without a listed pool (ZEC delisted) if (!vm.keyExistsJson(cfg, string.concat(".pools.", f[i].symbol))) continue; (PoolKey memory key, address base) = poolKey(cfg, f[i].symbol); (bool fresh, uint256 target) = tryPythPrice6(pyth, f[i].id); if (!fresh) { console.log(f[i].symbol, "skipped: onchain Pyth stale (run `make pyth-push`; feed entitled?)"); continue; } Result memory r = sync(pm, router, key, base, target, tol, me); console.log(f[i].symbol, r.swapped ? "synced" : "in band"); console.log(" before / target / after (USDC 6dp):", r.before6, r.target6, r.after6); } vm.stopBroadcast(); } /// @dev Swap `key` so its price == target6 (USDC 6dp per 1 base), unless already within `tolBps`. function sync( IPoolManager pm, PoolSwapTest router, PoolKey memory key, address base, uint256 target6, uint256 tolBps, address payer ) public returns (Result memory r) { uint8 dec = MockToken(base).decimals(); bool base0 = Currency.unwrap(key.currency0) == base; (uint160 cur,,,) = pm.getSlot0(key.toId()); r.before6 = PriceMath.fromSqrtPriceX96(cur, dec, base0); r.target6 = target6; r.after6 = r.before6; if (deviationBps(r.before6, target6) <= tolBps) return r; swapTo(pm, router, key, cur, PriceMath.toSqrtPriceX96(target6, dec, base0), payer); (cur,,,) = pm.getSlot0(key.toId()); r.after6 = PriceMath.fromSqrtPriceX96(cur, dec, base0); r.swapped = true; } /// @dev Exact-in swap capped at `limit`; input = exact liquidity delta grossed up for the max LP fee. function swapTo(IPoolManager pm, PoolSwapTest router, PoolKey memory key, uint160 cur, uint160 limit, address payer) internal { bool zeroForOne = limit < cur; // selling currency0 lowers sqrtPrice uint128 liq = pm.getLiquidity(key.toId()); uint256 net = zeroForOne ? SqrtPriceMath.getAmount0Delta(limit, cur, liq, true) : SqrtPriceMath.getAmount1Delta(cur, limit, liq, true); uint256 gross = net * 1e6 / (1e6 - MAX_FEE_PIPS) + 1; MockToken input = MockToken(Currency.unwrap(zeroForOne ? key.currency0 : key.currency1)); input.mint(payer, gross); input.approve(address(router), gross); router.swap( key, SwapParams({zeroForOne: zeroForOne, amountSpecified: -int256(gross), sqrtPriceLimitX96: limit}), PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), "" ); } function poolKey(string memory cfg, string memory sym) internal pure returns (PoolKey memory key, address base) { string memory p = string.concat(".pools.", sym); key = PoolKey({ currency0: Currency.wrap(vm.parseJsonAddress(cfg, string.concat(p, ".currency0"))), currency1: Currency.wrap(vm.parseJsonAddress(cfg, string.concat(p, ".currency1"))), fee: uint24(vm.parseJsonUint(cfg, string.concat(p, ".fee"))), tickSpacing: int24(vm.parseJsonInt(cfg, string.concat(p, ".tickSpacing"))), hooks: IHooks(vm.parseJsonAddress(cfg, string.concat(p, ".hooks"))) }); base = vm.parseJsonAddress(cfg, string.concat(p, ".base")); } function deviationBps(uint256 a, uint256 b) internal pure returns (uint256) { return (a > b ? a - b : b - a) * 1e4 / b; } }