// SPDX-License-Identifier: MIT pragma solidity 0.8.28; interface IERC20 { function balanceOf(address) external view returns (uint256); function approve(address, uint256) external returns (bool); function transfer(address, uint256) external returns (bool); } interface IPermit2 { function approve(address token, address spender, uint160 amount, uint48 expiration) external; } interface IUniversalRouter { function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable; } interface IPositionManager { function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable; function nextTokenId() external view returns (uint256); } interface IStateView { function getSlot0(bytes32 poolId) external view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee); } /// @title ArcGrow — put USDC to work in the APEX/USDC pool on Arc, from your own wallet, in one transaction /// @notice Send USDC (native) with `grow`. Half is swapped for APEX on the pool, both halves become a full-range liquidity /// position, and the position NFT is minted straight to YOU. This contract never keeps anything: leftovers go back /// to you in the same transaction. Nobody but the NFT owner can remove the liquidity, and the owner can remove it at /// any time through the PositionManager. No custody, no promises, no owner, no fees to us beyond the pool's own swap fee. contract ArcGrow { struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; } struct ExactInputSingleParams { PoolKey poolKey; bool zeroForOne; uint128 amountIn; uint128 amountOutMinimum; bytes hookData; } IUniversalRouter public immutable router; IPositionManager public immutable posm; IPermit2 public immutable permit2; IStateView public immutable stateView; IERC20 public immutable apex; uint24 public immutable fee; int24 public immutable tickSpacing; bytes32 public immutable poolId; int24 public constant TICK_LOWER = -887220; int24 public constant TICK_UPPER = 887220; uint256 constant Q96 = 2 ** 96; uint160 constant SQRT_LOWER = 4306310044; uint160 constant SQRT_UPPER = 1457652066949847389969617340386294118487833376468; uint256 public grows; uint256 public usdcInTotal; event Grew(address indexed owner, uint256 indexed tokenId, uint256 usdcIn, uint256 usdcToPool, uint256 apexToPool, uint256 liquidity); constructor(address router_, address posm_, address permit2_, address stateView_, address apex_, uint24 fee_, int24 tickSpacing_) { router = IUniversalRouter(router_); posm = IPositionManager(posm_); permit2 = IPermit2(permit2_); stateView = IStateView(stateView_); apex = IERC20(apex_); fee = fee_; tickSpacing = tickSpacing_; poolId = keccak256(abi.encode(PoolKey(address(0), apex_, fee_, tickSpacing_, address(0)))); IERC20(apex_).approve(permit2_, type(uint256).max); IPermit2(permit2_).approve(apex_, posm_, type(uint160).max, type(uint48).max); } receive() external payable {} function key() public view returns (PoolKey memory) { return PoolKey(address(0), address(apex), fee, tickSpacing, address(0)); } /// @param minApexOut slippage guard for the swap half (from a quote, minus a few percent) function grow(uint256 minApexOut) external payable returns (uint256 tokenId) { require(msg.value >= 0.05 ether, "min 0.05 USDC"); uint256 half = msg.value / 2; // 1. swap half of the USDC for APEX { bytes memory actions = abi.encodePacked(uint8(0x06), uint8(0x0c), uint8(0x0f)); bytes[] memory p = new bytes[](3); p[0] = abi.encode(ExactInputSingleParams(key(), true, uint128(half), uint128(minApexOut), bytes(""))); p[1] = abi.encode(address(0), half); p[2] = abi.encode(address(apex), minApexOut); bytes[] memory inputs = new bytes[](1); inputs[0] = abi.encode(actions, p); router.execute{value: half}(abi.encodePacked(uint8(0x10)), inputs, block.timestamp + 60); } uint256 apexAmt = apex.balanceOf(address(this)); uint256 usdcAmt = address(this).balance; // 2. liquidity for those amounts at the current price (full range) (uint160 sqrtP,,,) = stateView.getSlot0(poolId); uint256 l0 = usdcAmt * (uint256(sqrtP) * SQRT_UPPER / Q96) / (SQRT_UPPER - sqrtP); uint256 l1 = apexAmt * Q96 / (sqrtP - SQRT_LOWER); uint256 liquidity = l0 < l1 ? l0 : l1; require(liquidity > 0, "no liquidity"); // 3. mint the position to msg.sender, settle both, sweep leftovers to this contract tokenId = posm.nextTokenId(); { bytes memory actions = abi.encodePacked(uint8(0x02), uint8(0x0d), uint8(0x14)); bytes[] memory p = new bytes[](3); p[0] = abi.encode(key(), TICK_LOWER, TICK_UPPER, liquidity, uint128(usdcAmt), uint128(apexAmt), msg.sender, bytes("")); p[1] = abi.encode(address(0), address(apex)); p[2] = abi.encode(address(0), address(this)); posm.modifyLiquidities{value: usdcAmt}(abi.encode(actions, p), block.timestamp + 60); } // 4. give back whatever was not used uint256 restApex = apex.balanceOf(address(this)); if (restApex > 0) apex.transfer(msg.sender, restApex); uint256 restUsdc = address(this).balance; if (restUsdc > 0) { (bool ok, ) = payable(msg.sender).call{value: restUsdc}(""); require(ok, "refund"); } grows += 1; usdcInTotal += msg.value; emit Grew(msg.sender, tokenId, msg.value, usdcAmt - restUsdc, apexAmt - restApex, liquidity); } }