// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "forge-std/Script.sol"; import "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import "@aperture/uni-v3-lib/TickMath.sol"; interface ILM { function positions(uint8 stage) external view returns (uint128 liquidity, int24 tickLower, int24 tickUpper); } interface IWETH { function balanceOf(address) external view returns (uint256); } /// @title LmTotalEth /// @notice Read-only script: prints total ETH controlled by LiquidityManager /// (free ETH + free WETH + ETH locked in all 3 Uni V3 positions). /// /// @dev **What is counted:** /// 1. Free native ETH balance of the LM contract /// 2. Free WETH (ERC-20) balance of the LM contract /// 3. ETH-side principal of all 3 Uniswap V3 positions (FLOOR, ANCHOR, DISCOVERY), /// computed via LiquidityAmounts.getAmountsForLiquidity at the current sqrtPrice. /// /// **What is NOT counted:** /// - Uncollected trading fees accrued inside positions (these only become visible /// after a recenter() calls pool.burn + pool.collect and rolls them into free WETH). /// - KRK held by the LM (either as free balance or as the KRK side of positions). /// KRK fees collected during recenter() are transferred to feeDestination and /// excluded from this measurement entirely. /// - KRK sent to feeDestination is also subtracted from outstandingSupply for floor /// calculation purposes (see LiquidityManager._getOutstandingSupply). /// /// **Implication for delta_bps:** Because uncollected fees are invisible, delta_bps /// measured between two LmTotalEth snapshots reflects position principal changes /// plus any fees that were materialized by a recenter() between snapshots. /// See evidence/README.md ยง "Fee-Income Calculation Model" for the full formula. /// /// forge script script/LmTotalEth.s.sol --rpc-url $RPC_URL /// Env: LM, WETH, POOL contract LmTotalEth is Script { function run() external view { address lm = vm.envAddress("LM"); address weth = vm.envAddress("WETH"); address pool = vm.envAddress("POOL"); // Free balances uint256 freeEth = lm.balance; uint256 freeWeth = IWETH(weth).balanceOf(lm); // Current sqrtPrice from pool (uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0(); // Determine which token is WETH (token0 or token1) bool wethIsToken0 = IUniswapV3Pool(pool).token0() == weth; // Sum ETH in all 3 positions: FLOOR=0, ANCHOR=1, DISCOVERY=2 uint256 positionEth = 0; for (uint8 stage = 0; stage < 3; stage++) { (uint128 liquidity, int24 tickLower, int24 tickUpper) = ILM(lm).positions(stage); if (liquidity == 0) continue; uint160 sqrtA = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtB = TickMath.getSqrtRatioAtTick(tickUpper); (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtA, sqrtB, liquidity); positionEth += wethIsToken0 ? amount0 : amount1; } uint256 total = freeEth + freeWeth + positionEth; // Output as plain number for easy bash consumption console2.log(total); } }