// SPDX-License-Identifier: MIT pragma solidity ^0.8.28; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; interface IComplianceRegistry { function isApproved(address wallet) external view returns (bool); } contract HideSettlement is Ownable, Pausable, ReentrancyGuard, EIP712 { using SafeERC20 for IERC20; uint16 public constant MAX_FEE_BPS = 100; uint256 private constant BPS_DENOMINATOR = 10_000; bytes32 private constant RFQ_ORDER_TYPEHASH = keccak256( "RfqOrder(address trader,address solver,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 nonce,uint256 deadline)" ); bytes32 private constant RFQ_QUOTE_TYPEHASH = keccak256( "RfqQuote(address trader,address solver,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,uint256 nonce,uint256 deadline,uint256 amountOut)" ); struct RfqOrder { address trader; address solver; address tokenIn; address tokenOut; uint256 amountIn; uint256 minAmountOut; uint256 nonce; uint256 deadline; } IComplianceRegistry public immutable complianceRegistry; address public feeRecipient; uint16 public feeBps; mapping(address token => bool allowed) public allowedAsset; mapping(address wallet => mapping(address token => uint256 amount)) public balanceOf; mapping(address wallet => mapping(uint256 nonce => bool used)) public usedNonce; mapping(address token => uint256 amount) public totalLiability; error WalletNotApproved(); error AssetNotAllowed(); error ZeroAmount(); error InsufficientBalance(); error InvalidSolver(); error OrderExpired(); error NonceAlreadyUsed(); error InvalidSignature(); error InvalidQuoteSignature(); error InsufficientOutput(); error InvalidFee(); error InvalidRecipient(); error InsufficientExcess(); event AssetPermissionUpdated(address indexed token, bool allowed); event Shielded(address indexed wallet, address indexed token, uint256 amount); event Unshielded(address indexed wallet, address indexed token, uint256 amount); event OrderSettled( address indexed trader, address indexed solver, address indexed tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, uint256 fee, uint256 nonce ); event FeeConfigurationUpdated(address indexed recipient, uint16 feeBps); event ExcessRescued(address indexed token, address indexed recipient, uint256 amount); constructor( address initialOwner, address registry, address initialFeeRecipient, uint16 initialFeeBps ) Ownable(initialOwner) EIP712("Hide Settlement", "1") { if (registry == address(0) || initialFeeRecipient == address(0)) revert InvalidRecipient(); complianceRegistry = IComplianceRegistry(registry); _setFeeConfiguration(initialFeeRecipient, initialFeeBps); } modifier onlyApproved(address wallet) { if (!complianceRegistry.isApproved(wallet)) revert WalletNotApproved(); _; } function shield(address token, uint256 amount) external nonReentrant whenNotPaused onlyApproved(msg.sender) { if (!allowedAsset[token]) revert AssetNotAllowed(); if (amount == 0) revert ZeroAmount(); IERC20(token).safeTransferFrom(msg.sender, address(this), amount); balanceOf[msg.sender][token] += amount; totalLiability[token] += amount; emit Shielded(msg.sender, token, amount); } function unshield(address token, uint256 amount) external nonReentrant { if (amount == 0) revert ZeroAmount(); if (balanceOf[msg.sender][token] < amount) revert InsufficientBalance(); balanceOf[msg.sender][token] -= amount; totalLiability[token] -= amount; IERC20(token).safeTransfer(msg.sender, amount); emit Unshielded(msg.sender, token, amount); } function settle( RfqOrder calldata order, uint256 amountOut, bytes calldata traderSignature, bytes calldata quoteSignature ) external nonReentrant whenNotPaused { _validateOrder(order, amountOut, traderSignature, quoteSignature); if (msg.sender != order.solver) revert InvalidSolver(); if (balanceOf[order.trader][order.tokenIn] < order.amountIn) revert InsufficientBalance(); usedNonce[order.trader][order.nonce] = true; balanceOf[order.trader][order.tokenIn] -= order.amountIn; balanceOf[order.trader][order.tokenOut] += amountOut; totalLiability[order.tokenIn] -= order.amountIn; totalLiability[order.tokenOut] += amountOut; uint256 fee = (order.amountIn * feeBps) / BPS_DENOMINATOR; IERC20(order.tokenOut).safeTransferFrom(order.solver, address(this), amountOut); IERC20(order.tokenIn).safeTransfer(order.solver, order.amountIn - fee); if (fee != 0) IERC20(order.tokenIn).safeTransfer(feeRecipient, fee); emit OrderSettled( order.trader, order.solver, order.tokenIn, order.tokenOut, order.amountIn, amountOut, fee, order.nonce ); } function _validateOrder( RfqOrder calldata order, uint256 amountOut, bytes calldata traderSignature, bytes calldata quoteSignature ) private view { if (!complianceRegistry.isApproved(order.trader) || !complianceRegistry.isApproved(msg.sender)) { revert WalletNotApproved(); } if (!allowedAsset[order.tokenIn] || !allowedAsset[order.tokenOut]) revert AssetNotAllowed(); if (block.timestamp > order.deadline) revert OrderExpired(); if (usedNonce[order.trader][order.nonce]) revert NonceAlreadyUsed(); if (amountOut < order.minAmountOut) revert InsufficientOutput(); bytes32 structHash = keccak256( abi.encode( RFQ_ORDER_TYPEHASH, order.trader, order.solver, order.tokenIn, order.tokenOut, order.amountIn, order.minAmountOut, order.nonce, order.deadline ) ); if (ECDSA.recover(_hashTypedDataV4(structHash), traderSignature) != order.trader) revert InvalidSignature(); bytes32 quoteHash = keccak256( abi.encode( RFQ_QUOTE_TYPEHASH, order.trader, order.solver, order.tokenIn, order.tokenOut, order.amountIn, order.minAmountOut, order.nonce, order.deadline, amountOut ) ); if (ECDSA.recover(_hashTypedDataV4(quoteHash), quoteSignature) != order.solver) { revert InvalidQuoteSignature(); } } function setAssetAllowed(address token, bool allowed) external onlyOwner { if (token == address(0)) revert AssetNotAllowed(); allowedAsset[token] = allowed; emit AssetPermissionUpdated(token, allowed); } function setFeeConfiguration(address recipient, uint16 newFeeBps) external onlyOwner { _setFeeConfiguration(recipient, newFeeBps); } function rescueExcess(address token, address recipient, uint256 amount) external onlyOwner nonReentrant { if (recipient == address(0)) revert InvalidRecipient(); uint256 tokenBalance = IERC20(token).balanceOf(address(this)); uint256 liability = totalLiability[token]; if (tokenBalance < liability || amount > tokenBalance - liability) revert InsufficientExcess(); IERC20(token).safeTransfer(recipient, amount); emit ExcessRescued(token, recipient, amount); } function pause() external onlyOwner { _pause(); } function unpause() external onlyOwner { _unpause(); } function _setFeeConfiguration(address recipient, uint16 newFeeBps) private { if (recipient == address(0)) revert InvalidRecipient(); if (newFeeBps > MAX_FEE_BPS) revert InvalidFee(); feeRecipient = recipient; feeBps = newFeeBps; emit FeeConfigurationUpdated(recipient, newFeeBps); } }