Auto Payment Splitter: Split Incoming ETH Automatically in Solidity

This post walks through a minimal Solidity contract that splits any incoming ETH across multiple receivers by fixed percentages. Send ETH to the contract once—each receiver gets their share automatically.


What it does

AutoPaymentSplitter is a passive payment router:

  • You configure receivers and percentages once at deploy time.
  • Anyone can send ETH to the contract (receive()).
  • On each payment, the contract loops through receivers and transfers each share with .transfer().

No owner, no withdraw button—just automatic splitting on every incoming transfer.

Use cases:

  • Revenue share between co-founders
  • Team payouts from a single deposit address
  • Charity + treasury split (e.g. 70% / 30%)

Full contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract AutoPaymentSplitter {

    address[] public receivers;
    uint256[] public percentages;

    constructor(
        address[] memory _receivers,
        uint256[] memory _percentages
    ) {
        require(
            _receivers.length == _percentages.length,
            "Length mismatch"
        );

        uint256 total;

        for (uint256 i = 0; i < _percentages.length; i++) {
            total += _percentages[i];
        }

        require(total == 100, "Total must be 100%");

        receivers = _receivers;
        percentages = _percentages;
    }

    receive() external payable {

        for (uint256 i = 0; i < receivers.length; i++) {

            uint256 amount = (msg.value * percentages[i]) / 100;

            payable(receivers[i]).transfer(amount);
        }
    }
}

How it works

                    ┌─────────────────────────┐
   Sender ──ETH──►  │  AutoPaymentSplitter    │
                    │                         │
                    │  receive() payable      │
                    └───────────┬─────────────┘
                                │
            ┌───────────────────┼───────────────────┐
            ▼                   ▼                   ▼
      Receiver A (40%)    Receiver B (35%)    Receiver C (25%)
Step What happens
1 Contract is deployed with receivers[] and percentages[]
2 Someone sends ETH to the contract address
3 receive() runs automatically
4 For each receiver: amount = msg.value * percentage / 100
5 ETH is forwarded with transfer()

Percentages are stored as whole numbers that must sum to 100 (e.g. 40, 35, 25—not basis points).


Constructor validation

The constructor runs once and locks the split configuration forever (in this version).

Length check

require(
    _receivers.length == _percentages.length,
    "Length mismatch"
);

Every receiver needs exactly one percentage entry.

Total must equal 100%

uint256 total;

for (uint256 i = 0; i < _percentages.length; i++) {
    total += _percentages[i];
}

require(total == 100, "Total must be 100%");

Examples:

Percentages Result
[50, 50] :white_check_mark: Valid
[40, 35, 25] :white_check_mark: Valid
[50, 40] :x: Reverts — total is 90
[60, 50] :x: Reverts — total is 110

Storage

receivers = _receivers;
percentages = _percentages;

Both arrays are public, so Solidity generates getters: receivers(0), percentages(0), etc.


The receive() function

receive() external payable {

    for (uint256 i = 0; i < receivers.length; i++) {

        uint256 amount = (msg.value * percentages[i]) / 100;

        payable(receivers[i]).transfer(amount);
    }
}

Why receive()?

  • receive() is called when the contract gets plain ETH (no calldata).
  • It must be payable to accept ETH.
  • Perfect for “send to this address and split” behavior.

Amount calculation

uint256 amount = (msg.value * percentages[i]) / 100;

Example: msg.value = 1 ether, percentages[i] = 40
amount = 1e18 * 40 / 100 = 0.4 ether

Integer division truncates remainders (see Security & limitations).

transfer() vs call

payable(receivers[i]).transfer(amount);
  • Forwards 2300 gas to the receiver (legacy transfer behavior).
  • Reverts if the receiver is a contract that needs more gas (e.g. complex fallback).
  • Simple and fine for EOAs (normal wallets); risky for smart contract receivers.

Example setup

Three teammates split revenue 40% / 35% / 25%:

address[] memory receivers = new address[](3);
receivers[0] = 0x1111111111111111111111111111111111111111;
receivers[1] = 0x2222222222222222222222222222222222222222;
receivers[2] = 0x3333333333333333333333333333333333333333;

uint256[] memory percentages = new uint256[](3);
percentages[0] = 40;
percentages[1] = 35;
percentages[2] = 25;

// Deploy: new AutoPaymentSplitter(receivers, percentages)

If someone sends 10 ETH to the contract:

Receiver % Amount
0x1111… 40 4 ETH
0x2222… 35 3.5 ETH
0x3333… 25 2.5 ETH

Deploy in Remix

  1. Open https://remix.ethereum.org
  2. Create AutoPaymentSplitter.sol and paste the contract
  3. Compile with Solidity 0.8.20+
  4. Deploy & Run → choose environment (Remix VM or MetaMask)
  5. For constructor args, use array UI in Remix:
    • _receivers: ["0xAddr1","0xAddr2"]
    • _percentages: [60, 40]
  6. Send ETH to the deployed contract address (Low-level interactions → Receive or transfer from wallet)

Verify splits on a block explorer by checking each receiver’s balance change.


Security & limitations

1. Rounding dust

Integer math can leave wei dust in the contract when msg.value is small or splits don’t divide evenly.

Example: 1 wei split 33% / 33% / 34% — each amount may round down; leftover stays in the contract with no withdraw function in this version.

2. No way to update receivers

Configuration is immutable after deploy. Wrong address = redeploy a new splitter.

3. transfer() gas limit

Receivers that are contracts with heavy receive/fallback logic may cause reverts and block the entire split. Prefer call{value: amount}("") with proper checks in production.

4. No reentrancy guard

For this simple forward-only loop, risk is low, but call instead of transfer should use Checks-Effects-Interactions or ReentrancyGuard if logic grows.

5. No ERC-20 support

Only native ETH (or native chain currency on EVM L2s). Token splits need IERC20.transfer in a separate function.

6. Single failed transfer fails all

If one transfer reverts, the whole receive() reverts and no one gets paid.


Possible improvements

// 1. Safer send (production pattern)
(bool ok, ) = receivers[i].call{value: amount}("");
require(ok, "Transfer failed");

// 2. Send remainder to last receiver (fixes dust)
uint256 remaining = msg.value;
for (uint i = 0; i < receivers.length - 1; i++) {
    uint256 amount = (msg.value * percentages[i]) / 100;
    remaining -= amount;
    // send amount...
}
// last receiver gets `remaining`

// 3. Ownable + updateReceivers() for flexibility

// 4. Events for transparency
event PaymentSplit(address indexed sender, uint256 total);

Summary

AutoPaymentSplitter is a small, readable pattern for automatic ETH revenue sharing:

  • Constructor — validates array lengths and 100% total
  • receive() — splits every incoming payment by stored percentages
  • No admin — trustless once deployed

It’s ideal for learning receive(), array storage, and percentage math on-chain. For production, address rounding dust, use call safely, and consider immutability vs upgradeable config.