// SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; interface IPositionManagerV2 { struct IncreaseLiquidityParams { uint256 tokenId; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; uint256 deadline; } struct Position { uint96 nonce; address operator; address token0; address token1; uint24 fee; int24 tickLower; int24 tickUpper; uint128 liquidity; uint256 feeGrowthInside0LastX128; uint256 feeGrowthInside1LastX128; uint128 tokensOwed0; uint128 tokensOwed1; } function increaseLiquidity(IncreaseLiquidityParams calldata p) external payable returns (uint128, uint256, uint256); function positions(uint256 id) external view returns (Position memory); function ownerOf(uint256 id) external view returns (address); } /// @notice Inventory sale, NOT token issuance. All customer funds settle atomically. /// 333.333 USDC (6 decimals) buys 1 W3 (18 decimals). Unused USDC remains in this contract. /// Reuses one immutable full-range 0.3% NFT owned by the project wallet. /// Does not take NFT custody or approval; cannot remove liquidity or collect fees. contract W3InventoryMintV2 is ReentrancyGuard { using SafeERC20 for IERC20; uint256 public constant PRICE = 333333000; address public immutable owner; IERC20 public immutable usdc; IERC20 public immutable w3; IPositionManagerV2 public immutable positionManager; uint256 public immutable lpTokenId; bool public paused; event Purchased(address indexed buyer, uint256 usdcSpent, uint256 w3Received, uint256 w3Paired, uint256 indexed tokenId, uint256 retained); event Paused(bool paused); event InventoryWithdrawn(uint256 amount); event RetainedUSDCWithdrawn(uint256 amount); modifier onlyOwner() { require(msg.sender == owner, "Only owner"); _; } constructor(address owner_, address usdc_, address w3_, address manager_, uint256 lpTokenId_) { require(owner_ != address(0) && usdc_ != w3_, "Invalid address"); require(usdc_.code.length > 0 && w3_.code.length > 0 && manager_.code.length > 0, "Missing code"); owner = owner_; usdc = IERC20(usdc_); w3 = IERC20(w3_); positionManager = IPositionManagerV2(manager_); require(positionManager.ownerOf(lpTokenId_) == owner_, "LP owner mismatch"); IPositionManagerV2.Position memory pos = positionManager.positions(lpTokenId_); require(pos.token0 == (usdc_ < w3_ ? usdc_ : w3_) && pos.token1 == (usdc_ < w3_ ? w3_ : usdc_), "LP token mismatch"); require(pos.fee == 3000 && pos.tickLower == -887220 && pos.tickUpper == 887220, "LP range mismatch"); lpTokenId = lpTokenId_; } function setPaused(bool value) external onlyOwner { paused = value; emit Paused(value); } function withdrawInventory(uint256 amount) external onlyOwner nonReentrant { require(paused, "Pause first"); w3.safeTransfer(owner, amount); emit InventoryWithdrawn(amount); } function withdrawRetainedUSDC(uint256 amount) external onlyOwner nonReentrant { require(paused, "Pause first"); usdc.safeTransfer(owner, amount); emit RetainedUSDCWithdrawn(amount); } function purchase(uint256 usdcIn, uint256 maxW3ForLP, uint256 minUsdcUsed, uint256 minW3Out, uint256 deadline) external nonReentrant returns (uint256 tokenId, uint256 received) { require(!paused, "Paused"); require(positionManager.ownerOf(lpTokenId) == owner, "LP owner mismatch"); require(block.timestamp <= deadline && deadline <= block.timestamp + 1800, "Deadline"); require(usdcIn > 0 && maxW3ForLP > 0, "Zero amount"); // Bound project inventory exposure independently of caller-provided slippage. require(maxW3ForLP <= usdcIn * 1e18 / PRICE * 102 / 100, "LP inventory cap"); require(minUsdcUsed > 0 && minUsdcUsed >= usdcIn * 995 / 1000 && minUsdcUsed <= usdcIn && minW3Out > 0, "Invalid minimum"); require(w3.balanceOf(address(this)) >= usdcIn * 1e18 / PRICE + maxW3ForLP, "Insufficient inventory"); uint256 beforeBalance = usdc.balanceOf(address(this)); usdc.safeTransferFrom(msg.sender, address(this), usdcIn); require(usdc.balanceOf(address(this)) == beforeBalance + usdcIn, "Unsupported token"); usdc.forceApprove(address(positionManager), usdcIn); w3.forceApprove(address(positionManager), maxW3ForLP); bool usdcFirst = address(usdc) < address(w3); uint256 a0; uint256 a1; uint128 liquidity; tokenId = lpTokenId; (liquidity, a0, a1) = positionManager.increaseLiquidity(IPositionManagerV2.IncreaseLiquidityParams({ tokenId: tokenId, amount0Desired: usdcFirst ? usdcIn : maxW3ForLP, amount1Desired: usdcFirst ? maxW3ForLP : usdcIn, amount0Min: usdcFirst ? minUsdcUsed : 0, amount1Min: usdcFirst ? 0 : minUsdcUsed, deadline: deadline })); uint256 used = usdcFirst ? a0 : a1; uint256 paired = usdcFirst ? a1 : a0; require(liquidity > 0 && used >= minUsdcUsed && used <= usdcIn && paired > 0 && paired <= maxW3ForLP, "Invalid liquidity"); received = usdcIn * 1e18 / PRICE; require(received >= minW3Out, "Minimum W3"); usdc.forceApprove(address(positionManager), 0); w3.forceApprove(address(positionManager), 0); w3.safeTransfer(msg.sender, received); uint256 retained = usdcIn - used; require(usdc.balanceOf(address(this)) == beforeBalance + retained, "USDC settlement"); emit Purchased(msg.sender, used, received, paired, tokenId, retained); } }