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] |
Valid |
[40, 35, 25] |
Valid |
[50, 40] |
Reverts — total is 90 |
[60, 50] |
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
payableto 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
transferbehavior). - 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
- Open https://remix.ethereum.org
- Create
AutoPaymentSplitter.soland paste the contract - Compile with Solidity 0.8.20+
- Deploy & Run → choose environment (Remix VM or MetaMask)
- For constructor args, use array UI in Remix:
-
_receivers:["0xAddr1","0xAddr2"] -
_percentages:[60, 40]
-
- 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.
Valid
Reverts — total is 90