Skip to content
Contract · Payments

Payment Splitter

Split incoming ETH or ERC20 payments across N payees weighted by shares.

All templates
Payments
~1.2M gas
Ethereum
Post-deploy checklist
  1. Verify constructor payees and shares match your term sheet
  2. Send a small test payment and confirm balances
  3. Share the release() flow with each payee
  4. Document total shares so future readers can audit percentages
Configure & deploy

One address per payee, comma-separated. Order must align with shares.

Relative weights, not percentages. 50,30,20 means 50% / 30% / 20% of total 100.

Use cases
  • Split royalties across a team
  • Distribute revenue to token holders
  • Fund multiple grant recipients
PaymentSplitter.solsolidity
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0 (Wizard / Contracts MCP patterns)
// Compiled with Solidity ^0.8.24 — Ethereum Toolset browser solc
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/// @notice Share-weighted ETH/ERC20 splitter (OZ PaymentSplitter pattern for Contracts v5).
contract PaymentSplitter {
    using Address for address payable;
    using SafeERC20 for IERC20;

    event PayeeAdded(address account, uint256 shares);
    event PaymentReleased(address to, uint256 amount);
    event ERC20PaymentReleased(IERC20 indexed token, address to, uint256 amount);
    event PaymentReceived(address from, uint256 amount);

    uint256 private _totalShares;
    uint256 private _totalReleased;
    mapping(address => uint256) private _shares;
    mapping(address => uint256) private _released;
    mapping(IERC20 => uint256) private _erc20TotalReleased;
    mapping(IERC20 => mapping(address => uint256)) private _erc20Released;
    address[] private _payees;

    constructor(address[] memory payees, uint256[] memory shares_) payable {
        require(payees.length == shares_.length, "payees/shares length mismatch");
        require(payees.length > 0, "no payees");
        for (uint256 i = 0; i < payees.length; i++) {
            _addPayee(payees[i], shares_[i]);
        }
    }

    receive() external payable {
        emit PaymentReceived(msg.sender, msg.value);
    }

    function totalShares() public view returns (uint256) { return _totalShares; }
    function totalReleased() public view returns (uint256) { return _totalReleased; }
    function shares(address account) public view returns (uint256) { return _shares[account]; }
    function released(address account) public view returns (uint256) { return _released[account]; }
    function payee(uint256 index) public view returns (address) { return _payees[index]; }
    function releasable(address account) public view returns (uint256) {
        uint256 totalReceived = address(this).balance + _totalReleased;
        return _pendingPayment(account, totalReceived, _released[account]);
    }

    function release(address payable account) public {
        require(_shares[account] > 0, "account has no shares");
        uint256 payment = releasable(account);
        require(payment != 0, "account is not due payment");
        _released[account] += payment;
        _totalReleased += payment;
        account.sendValue(payment);
        emit PaymentReleased(account, payment);
    }

    function release(IERC20 token, address account) public {
        require(_shares[account] > 0, "account has no shares");
        uint256 totalReceived = token.balanceOf(address(this)) + _erc20TotalReleased[token];
        uint256 payment = _pendingPayment(account, totalReceived, _erc20Released[token][account]);
        require(payment != 0, "account is not due payment");
        _erc20Released[token][account] += payment;
        _erc20TotalReleased[token] += payment;
        token.safeTransfer(account, payment);
        emit ERC20PaymentReleased(token, account, payment);
    }

    function _pendingPayment(address account, uint256 totalReceived, uint256 alreadyReleased) private view returns (uint256) {
        return (totalReceived * _shares[account]) / _totalShares - alreadyReleased;
    }

    function _addPayee(address account, uint256 shares_) private {
        require(account != address(0), "zero address");
        require(shares_ > 0, "shares are 0");
        require(_shares[account] == 0, "account already has shares");
        _payees.push(account);
        _shares[account] = shares_;
        _totalShares += shares_;
        emit PayeeAdded(account, shares_);
    }
}