// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /** * @title LiquidityPoolETH * @notice Liquidity pool for ETH and WETH with fee model and minimum liquidity ratio enforcement * @dev Supports separate pools for native ETH and WETH (ERC-20) */ contract LiquidityPoolETH is ReentrancyGuard { using SafeERC20 for IERC20; enum AssetType { ETH, // Native ETH WETH // Wrapped ETH (ERC-20) } // Pool configuration uint256 public immutable lpFeeBps; // Liquidity provider fee in basis points (default: 5 = 0.05%) uint256 public immutable minLiquidityRatioBps; // Minimum liquidity ratio in basis points (default: 11000 = 110%) address public immutable weth; // WETH token address // WETH getter for external access function getWeth() external view returns (address) { return weth; } // Pool state struct PoolState { uint256 totalLiquidity; uint256 pendingClaims; // Total amount of pending claims to be released mapping(address => uint256) lpShares; // LP address => amount provided } mapping(AssetType => PoolState) public pools; mapping(address => bool) public authorizedRelease; // Contracts authorized to release funds event LiquidityProvided( AssetType indexed assetType, address indexed provider, uint256 amount ); event LiquidityWithdrawn( AssetType indexed assetType, address indexed provider, uint256 amount ); event FundsReleased( AssetType indexed assetType, uint256 indexed depositId, address indexed recipient, uint256 amount, uint256 feeAmount ); event PendingClaimAdded( AssetType indexed assetType, uint256 amount ); event PendingClaimRemoved( AssetType indexed assetType, uint256 amount ); error ZeroAmount(); error ZeroAddress(); error InsufficientLiquidity(); error WithdrawalBlockedByLiquidityRatio(); error UnauthorizedRelease(); error InvalidAssetType(); /** * @notice Constructor * @param _weth WETH token address * @param _lpFeeBps LP fee in basis points (5 = 0.05%) * @param _minLiquidityRatioBps Minimum liquidity ratio in basis points (11000 = 110%) */ constructor( address _weth, uint256 _lpFeeBps, uint256 _minLiquidityRatioBps ) { require(_weth != address(0), "LiquidityPoolETH: zero WETH address"); require(_lpFeeBps <= 10000, "LiquidityPoolETH: fee exceeds 100%"); require(_minLiquidityRatioBps >= 10000, "LiquidityPoolETH: min ratio must be >= 100%"); weth = _weth; lpFeeBps = _lpFeeBps; minLiquidityRatioBps = _minLiquidityRatioBps; } /** * @notice Authorize a contract to release funds (called during deployment) * @param releaser Address authorized to release funds */ function authorizeRelease(address releaser) external { require(releaser != address(0), "LiquidityPoolETH: zero address"); authorizedRelease[releaser] = true; } /** * @notice Provide liquidity to the pool * @param assetType Type of asset (ETH or WETH) */ function provideLiquidity(AssetType assetType) external payable nonReentrant { uint256 amount; if (assetType == AssetType.ETH) { if (msg.value == 0) revert ZeroAmount(); amount = msg.value; } else if (assetType == AssetType.WETH) { if (msg.value != 0) revert("LiquidityPoolETH: WETH deposits must use depositWETH()"); revert("LiquidityPoolETH: use depositWETH() for WETH deposits"); } else { revert InvalidAssetType(); } pools[assetType].totalLiquidity += amount; pools[assetType].lpShares[msg.sender] += amount; emit LiquidityProvided(assetType, msg.sender, amount); } /** * @notice Provide WETH liquidity to the pool * @param amount Amount of WETH to deposit */ function depositWETH(uint256 amount) external nonReentrant { if (amount == 0) revert ZeroAmount(); IERC20(weth).safeTransferFrom(msg.sender, address(this), amount); pools[AssetType.WETH].totalLiquidity += amount; pools[AssetType.WETH].lpShares[msg.sender] += amount; emit LiquidityProvided(AssetType.WETH, msg.sender, amount); } /** * @notice Withdraw liquidity from the pool * @param amount Amount to withdraw * @param assetType Type of asset (ETH or WETH) */ function withdrawLiquidity( uint256 amount, AssetType assetType ) external nonReentrant { if (amount == 0) revert ZeroAmount(); if (pools[assetType].lpShares[msg.sender] < amount) revert InsufficientLiquidity(); // Check minimum liquidity ratio uint256 availableLiquidity = pools[assetType].totalLiquidity - pools[assetType].pendingClaims; uint256 newAvailableLiquidity = availableLiquidity - amount; uint256 minRequired = (pools[assetType].pendingClaims * minLiquidityRatioBps) / 10000; if (newAvailableLiquidity < minRequired) { revert WithdrawalBlockedByLiquidityRatio(); } pools[assetType].totalLiquidity -= amount; pools[assetType].lpShares[msg.sender] -= amount; if (assetType == AssetType.ETH) { (bool success, ) = payable(msg.sender).call{value: amount}(""); require(success, "LiquidityPoolETH: ETH transfer failed"); } else { IERC20(weth).safeTransfer(msg.sender, amount); } emit LiquidityWithdrawn(assetType, msg.sender, amount); } /** * @notice Release funds to recipient (only authorized contracts) * @param depositId Deposit ID (for event tracking) * @param recipient Recipient address * @param amount Amount to release (before fees) * @param assetType Type of asset (ETH or WETH) */ function releaseToRecipient( uint256 depositId, address recipient, uint256 amount, AssetType assetType ) external nonReentrant { if (!authorizedRelease[msg.sender]) revert UnauthorizedRelease(); if (amount == 0) revert ZeroAmount(); if (recipient == address(0)) revert ZeroAddress(); // Calculate fee uint256 feeAmount = (amount * lpFeeBps) / 10000; uint256 releaseAmount = amount - feeAmount; // Check available liquidity PoolState storage pool = pools[assetType]; uint256 availableLiquidity = pool.totalLiquidity - pool.pendingClaims; if (availableLiquidity < releaseAmount) { revert InsufficientLiquidity(); } // Reduce pending claims pool.pendingClaims -= amount; // Release funds to recipient if (assetType == AssetType.ETH) { (bool success, ) = payable(recipient).call{value: releaseAmount}(""); require(success, "LiquidityPoolETH: ETH transfer failed"); } else { IERC20(weth).safeTransfer(recipient, releaseAmount); } // Fee remains in pool (increases totalLiquidity effectively by reducing pendingClaims) emit FundsReleased(assetType, depositId, recipient, releaseAmount, feeAmount); } /** * @notice Add pending claim (called when claim is submitted) * @param amount Amount of pending claim * @param assetType Type of asset */ function addPendingClaim(uint256 amount, AssetType assetType) external { if (!authorizedRelease[msg.sender]) revert UnauthorizedRelease(); pools[assetType].pendingClaims += amount; emit PendingClaimAdded(assetType, amount); } /** * @notice Remove pending claim (called when claim is challenged/slashed) * @param amount Amount of pending claim to remove * @param assetType Type of asset */ function removePendingClaim(uint256 amount, AssetType assetType) external { if (!authorizedRelease[msg.sender]) revert UnauthorizedRelease(); pools[assetType].pendingClaims -= amount; emit PendingClaimRemoved(assetType, amount); } /** * @notice Get available liquidity for an asset type * @param assetType Type of asset * @return Available liquidity (total - pending claims) */ function getAvailableLiquidity(AssetType assetType) external view returns (uint256) { PoolState storage pool = pools[assetType]; uint256 pending = pool.pendingClaims; if (pool.totalLiquidity <= pending) { return 0; } return pool.totalLiquidity - pending; } /** * @notice Get LP share for a provider * @param provider LP provider address * @param assetType Type of asset * @return LP share amount */ function getLpShare(address provider, AssetType assetType) external view returns (uint256) { return pools[assetType].lpShares[provider]; } /** * @notice Get pool statistics * @param assetType Type of asset * @return totalLiquidity Total liquidity in pool * @return pendingClaims Total pending claims * @return availableLiquidity Available liquidity (total - pending) */ function getPoolStats( AssetType assetType ) external view returns ( uint256 totalLiquidity, uint256 pendingClaims, uint256 availableLiquidity ) { PoolState storage pool = pools[assetType]; totalLiquidity = pool.totalLiquidity; pendingClaims = pool.pendingClaims; if (totalLiquidity > pendingClaims) { availableLiquidity = totalLiquidity - pendingClaims; } else { availableLiquidity = 0; } } // Allow contract to receive ETH receive() external payable {} }