// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; interface IERC20Minimal { function transferFrom(address from, address to, uint256 amount) external returns (bool); function transfer(address to, uint256 amount) external returns (bool); } contract Vault0Factory { struct Vault { address creator; address token; uint256 amount; uint64 unlockTime; bool withdrawn; address[] recipients; uint16[] shares; } uint16 public feeBps; address public feeRecipient; address public owner; uint256 public vaultCount; bool private entered; mapping(uint256 => Vault) private vaults; mapping(address => uint256[]) private creatorVaults; mapping(address => uint256[]) private recipientVaults; event VaultCreated(uint256 indexed id, address indexed creator, address indexed token, uint256 amount, uint64 unlockTime); event VaultWithdrawn(uint256 indexed id, uint256 grossAmount, uint256 protocolFee); event FeeRecipientUpdated(address indexed previousRecipient, address indexed newRecipient); event FeeUpdated(uint16 previousFeeBps, uint16 newFeeBps); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); error Unauthorized(); error InvalidConfiguration(); error InvalidUnlockTime(); error ZeroAmount(); error VaultLocked(); error AlreadyWithdrawn(); error TransferFailed(); error Reentrancy(); modifier onlyOwner() { if (msg.sender != owner) revert Unauthorized(); _; } modifier nonReentrant() { if (entered) revert Reentrancy(); entered = true; _; entered = false; } constructor(address initialFeeRecipient, uint16 initialFeeBps) { if (initialFeeRecipient == address(0) || initialFeeBps > 500) revert InvalidConfiguration(); owner = msg.sender; feeRecipient = initialFeeRecipient; feeBps = initialFeeBps; emit OwnershipTransferred(address(0), msg.sender); } function createNativeVault( uint64 unlockTime, address[] calldata recipients, uint16[] calldata shares ) external payable returns (uint256 id) { if (msg.value == 0) revert ZeroAmount(); id = _create(address(0), msg.value, unlockTime, recipients, shares); } function createTokenVault( address token, uint256 amount, uint64 unlockTime, address[] calldata recipients, uint16[] calldata shares ) external nonReentrant returns (uint256 id) { if (token == address(0) || amount == 0) revert ZeroAmount(); _safeTransferFrom(token, msg.sender, address(this), amount); id = _create(token, amount, unlockTime, recipients, shares); } function _create( address token, uint256 amount, uint64 unlockTime, address[] calldata recipients, uint16[] calldata shares ) internal returns (uint256 id) { if (unlockTime <= block.timestamp) revert InvalidUnlockTime(); _validateRoute(recipients, shares); id = vaultCount++; Vault storage v = vaults[id]; v.creator = msg.sender; v.token = token; v.amount = amount; v.unlockTime = unlockTime; for (uint256 i; i < recipients.length; ++i) { v.recipients.push(recipients[i]); v.shares.push(shares[i]); recipientVaults[recipients[i]].push(id); } creatorVaults[msg.sender].push(id); emit VaultCreated(id, msg.sender, token, amount, unlockTime); } function withdraw(uint256 id) external nonReentrant { Vault storage v = vaults[id]; if (v.creator == address(0)) revert InvalidConfiguration(); if (v.withdrawn) revert AlreadyWithdrawn(); if (block.timestamp < v.unlockTime) revert VaultLocked(); bool allowed; for (uint256 i; i < v.recipients.length; ++i) { if (v.recipients[i] == msg.sender) { allowed = true; break; } } if (!allowed && msg.sender != v.creator) revert Unauthorized(); v.withdrawn = true; uint256 protocolFee = (v.amount * feeBps) / 10_000; uint256 distributable = v.amount - protocolFee; if (protocolFee > 0) _transferAsset(v.token, feeRecipient, protocolFee); uint256 sent; for (uint256 i; i < v.recipients.length; ++i) { uint256 part = i == v.recipients.length - 1 ? distributable - sent : (distributable * v.shares[i]) / 10_000; sent += part; _transferAsset(v.token, v.recipients[i], part); } emit VaultWithdrawn(id, v.amount, protocolFee); } function getVault(uint256 id) external view returns ( address creator, address token, uint256 amount, uint64 unlockTime, bool withdrawn, address[] memory recipients, uint16[] memory shares ) { Vault storage v = vaults[id]; return (v.creator, v.token, v.amount, v.unlockTime, v.withdrawn, v.recipients, v.shares); } function vaultsCreatedBy(address account) external view returns (uint256[] memory) { return creatorVaults[account]; } function vaultsForRecipient(address account) external view returns (uint256[] memory) { return recipientVaults[account]; } function updateFeeRecipient(address nextRecipient) external onlyOwner { if (nextRecipient == address(0)) revert InvalidConfiguration(); emit FeeRecipientUpdated(feeRecipient, nextRecipient); feeRecipient = nextRecipient; } function updateFee(uint16 nextFeeBps) external onlyOwner { if (nextFeeBps > 500) revert InvalidConfiguration(); emit FeeUpdated(feeBps, nextFeeBps); feeBps = nextFeeBps; } function transferOwnership(address nextOwner) external onlyOwner { if (nextOwner == address(0)) revert InvalidConfiguration(); emit OwnershipTransferred(owner, nextOwner); owner = nextOwner; } function _validateRoute(address[] calldata recipients, uint16[] calldata shares) private pure { if (recipients.length == 0 || recipients.length > 5 || recipients.length != shares.length) revert InvalidConfiguration(); uint256 total; for (uint256 i; i < recipients.length; ++i) { if (recipients[i] == address(0) || shares[i] == 0) revert InvalidConfiguration(); total += shares[i]; } if (total != 10_000) revert InvalidConfiguration(); } function _transferAsset(address token, address to, uint256 amount) private { if (token == address(0)) { (bool ok,) = payable(to).call{value: amount}(""); if (!ok) revert TransferFailed(); } else { (bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20Minimal.transfer.selector, to, amount)); if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TransferFailed(); } } function _safeTransferFrom(address token, address from, address to, uint256 amount) private { (bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20Minimal.transferFrom.selector, from, to, amount)); if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TransferFailed(); } receive() external payable { revert InvalidConfiguration(); } }