// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { IPaymentSplitter } from "../interfaces/finance/IPaymentSplitter.sol"; import { ContractMetadata } from "../metadata/ContractMetadata.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import { JsonUtil } from "../utils/JsonUtil.sol"; contract PaymentSplitter is Ownable2Step, ContractMetadata, ReentrancyGuard, IPaymentSplitter { uint256 private _totalShares; uint256 private _totalReleased; mapping(address => uint256) private _shares; mapping(address => uint256) private _released; address[] private _payees; constructor( string memory _name, address _initialOwner, address[] memory _initialPayees, uint256[] memory _initialShares ) Ownable(_initialOwner) { require(_initialPayees.length == _initialShares.length, "PaymentSplitter: payees and shares length mismatch"); require(_initialPayees.length > 0, "PaymentSplitter: no payees"); for (uint256 i = 0; i < _initialPayees.length; i++) { _addPayee(_initialPayees[i], _initialShares[i]); } _setContractMetadata(JsonUtil.set("{}", "name", _name)); } receive() external payable virtual { emit IPaymentSplitter.PaymentReceived(_msgSender(), msg.value); } /** * @dev Getter for the total shares held by payees. */ function totalShares() public view returns (uint256) { return _totalShares; } /** * @dev Getter for the total amount of Ether already released. */ function totalReleased() public view returns (uint256) { return _totalReleased; } /** * @dev Getter for the amount of shares held by an account. */ function shares(address _account) public view returns (uint256) { return _shares[_account]; } /** * @dev Getter for the amount of Ether already released to a payee. */ function released(address _account) public view returns (uint256) { return _released[_account]; } /** * @dev Getter for the amount of payee's releasable Ether. */ function releasable(address _account) public view returns (uint256) { uint256 totalReceived = address(this).balance + totalReleased(); return _pendingPayment(_account, totalReceived, released(_account)); } /** * @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the * total shares and their previous withdrawals. */ function release(address payable _account) public virtual { require(_shares[_account] > 0, "PaymentSplitter: account has no shares"); uint256 payment = releasable(_account); require(payment != 0, "PaymentSplitter: account is not due payment"); // _totalReleased is the sum of all values in _released. // If "_totalReleased += payment" does not overflow, then "_released[account] += payment" cannot overflow. _totalReleased += payment; unchecked { _released[_account] += payment; } Address.sendValue(_account, payment); emit IPaymentSplitter.PaymentReleased(_account, payment); } /** * @dev Release the owed amount of token to all of the payees. */ function distribute() public virtual nonReentrant { for (uint256 i = 0; i < _payees.length; i++) { address payable payee = payable(_payees[i]); uint256 payment = releasable(payee); if (payment == 0) { continue; } release(payee); } } /** * @dev internal logic for computing the pending payment of an `account` given the token historical balances and * already released amounts. */ function _pendingPayment( address _account, uint256 _totalReceived, uint256 _alreadyReleased ) private view returns (uint256) { return (_totalReceived * _shares[_account]) / _totalShares - _alreadyReleased; } /** * @dev Add a new payee to the contract. * @param _newAccount The address of the payee to add. * @param _accountShares The number of shares owned by the payee. */ function _addPayee(address _newAccount, uint256 _accountShares) private { require(_newAccount != address(0), "PaymentSplitter: account is the zero address"); require(_accountShares > 0, "PaymentSplitter: shares are 0"); require(_shares[_newAccount] == 0, "PaymentSplitter: account already has shares"); _payees.push(_newAccount); _shares[_newAccount] = _accountShares; _totalShares = _totalShares + _accountShares; emit IPaymentSplitter.PayeeAdded(_newAccount, _accountShares); } function _canSetContractMetadata() internal view override returns (bool) { return owner() == _msgSender(); } }