// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @notice Splits independently earned ETH into immutable treasury/operator claims. /// @dev Routes receipts; cannot generate revenue or access treasury principal. contract FoldRevenueRouter is ReentrancyGuard { address payable public immutable treasury; address payable public immutable operator; uint256 public immutable treasuryBps; mapping(address => uint256) public claimable; uint256 public liabilities; uint256 public totalRevenue; error InvalidConfiguration(); error InvalidRecipient(); error EmptyClaim(); error TransferFailed(); event RevenueReceived(address indexed sender, uint256 amount, uint256 treasuryShare, uint256 operatorShare); event Released(address indexed recipient, uint256 amount); constructor(address payable treasury_, address payable operator_, uint256 treasuryBps_) { if (treasury_ == address(0) || operator_ == address(0) || treasury_ == operator_ || treasuryBps_ == 0 || treasuryBps_ > 10_000) revert InvalidConfiguration(); treasury = treasury_; operator = operator_; treasuryBps = treasuryBps_; } receive() external payable { _credit(msg.value); } /// @notice Account for forced ETH without touching claims already allocated. function sync() external { _credit(address(this).balance - liabilities); } function _credit(uint256 amount) private { if (amount == 0) return; uint256 share = Math.mulDiv(amount, treasuryBps, 10_000); claimable[treasury] += share; claimable[operator] += amount - share; liabilities += amount; totalRevenue += amount; emit RevenueReceived(msg.sender, amount, share, amount - share); } /// @notice Anyone may release, but the immutable beneficiary receives the ETH. function release(address payable recipient) external nonReentrant { if (recipient != treasury && recipient != operator) revert InvalidRecipient(); uint256 amount = claimable[recipient]; if (amount == 0) revert EmptyClaim(); claimable[recipient] = 0; liabilities -= amount; (bool ok,) = recipient.call{value: amount}(""); if (!ok) revert TransferFailed(); emit Released(recipient, amount); } }