Building a Dynamic Payment Splitter on Conflux Network
Introduction
Payment splitting is a common requirement in blockchain applications, especially for scenarios like revenue sharing, team payouts, or multi-party transactions. In this technical deep-dive, we’ll explore a Conflux Payment Splitter smart contract that enables dynamic, trustless distribution of CFX (Conflux’s native token) among multiple recipients based on configurable shares.
This contract is particularly useful for:
- Revenue sharing among team members
- Automated royalty distribution
- Multi-party escrow services
- Decentralized organization payouts
Project Overview
The Conflux Payment Splitter is a Solidity smart contract deployed on the Conflux blockchain that allows:
- Dynamic payee management: Add recipients after contract deployment
- Proportional distribution: Automatically split payments based on assigned shares
- On-demand releases: Recipients can claim their portion at any time
- Owner-controlled: Admin can add new payees with specific share allocations
Architecture & Design
Core Components
The contract uses a simple but effective architecture:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ConfluxPaymentSplitter {
address public owner;
uint256 private _totalShares;
uint256 private _totalReleased;
mapping(address => uint256) private _shares;
mapping(address => uint256) private _released;
address[] private _payees;
}
Key Design Decisions
-
Dynamic Payee Addition: Unlike many payment splitters that require all payees at deployment, this contract allows adding recipients post-deployment, providing flexibility for evolving teams or partnerships.
-
Share-Based Distribution: Uses a proportional share system where each payee receives
(totalReceived * shares) / totalShares. This ensures fair distribution regardless of when funds arrive. -
Pull Payment Model: Recipients must call
release()to claim their funds, reducing gas costs and giving them control over when to withdraw. -
Owner-Only Administration: Only the contract owner can add new payees, preventing unauthorized modifications.
Code Walkthrough
Contract Initialization
constructor() {
owner = msg.sender;
}
The constructor simply sets the deployer as the owner, who will manage payee additions.
Receiving Payments
The contract accepts CFX through two mechanisms:
// Automatic receipt via receive() function
receive() external payable {
emit PaymentReceived(msg.sender, msg.value);
}
// Explicit deposit function
function deposit() external payable {
require(msg.value > 0, "No CFX sent");
emit PaymentReceived(msg.sender, msg.value);
}
Both methods emit a PaymentReceived event for transparency. The receive() function allows direct transfers, while deposit() provides an explicit interface.
Adding Payees
function addPayee(address account, uint256 shares_) external onlyOwner {
require(account != address(0), "zero address");
require(shares_ > 0, "shares = 0");
require(_shares[account] == 0, "already added");
_payees.push(account);
_shares[account] = shares_;
_totalShares += shares_;
emit PayeeAdded(account, shares_);
}
Key validations:
- Prevents zero address additions
- Ensures shares are greater than zero
- Prevents duplicate payee entries
- Updates total shares for accurate calculations
Calculating Pending Payments
function pending(address account) public view returns (uint256) {
uint256 totalReceived = address(this).balance + _totalReleased;
return _pendingPayment(account, totalReceived, _released[account]);
}
function _pendingPayment(
address account,
uint256 totalReceived,
uint256 alreadyReleased
) private view returns (uint256) {
return (totalReceived * _shares[account]) / _totalShares - alreadyReleased;
}
The pending() function calculates how much CFX a payee can claim:
-
totalReceived= current balance + previously released funds - Payment =
(totalReceived × shares) / totalShares - alreadyReleased
This formula ensures:
- Proportional distribution based on shares
- Accurate tracking of what’s been released
- Fair calculation even if funds arrive at different times
Releasing Payments
function release(address payable account) public {
require(_shares[account] > 0, "no shares");
uint256 totalReceived = address(this).balance + _totalReleased;
uint256 payment = _pendingPayment(account, totalReceived, _released[account]);
require(payment > 0, "nothing to release");
_released[account] += payment;
_totalReleased += payment;
(bool ok, ) = account.call{value: payment}("");
require(ok, "ETH transfer failed");
emit PaymentReleased(account, payment);
}
Process flow:
- Validates the account has shares
- Calculates pending payment
- Updates released amounts
- Transfers CFX to the recipient
- Emits event for transparency
The use of call{value: payment}("") is the recommended way to send native tokens in modern Solidity.
Usage Examples
Scenario 1: Team Revenue Sharing
Imagine a team of 3 developers sharing revenue from a project:
// After deployment, owner adds payees
splitter.addPayee(0xDeveloper1..., 40); // 40% share
splitter.addPayee(0xDeveloper2..., 35); // 35% share
splitter.addPayee(0xDeveloper3..., 25); // 25% share
// Total: 100 shares
// Revenue arrives
splitter.deposit{value: 1000 ether}();
// Each developer can claim their portion
splitter.release(0xDeveloper1...); // Receives 400 CFX
splitter.release(0xDeveloper2...); // Receives 350 CFX
splitter.release(0xDeveloper3...); // Receives 250 CFX
Scenario 2: Adding New Team Members
The dynamic nature allows adding members later:
// Initial setup
splitter.addPayee(0xFounder..., 50);
splitter.addPayee(0xCoFounder..., 50);
// Later, a new team member joins
splitter.addPayee(0xNewMember..., 20);
// Now: Founder 50, CoFounder 50, NewMember 20 = 120 total shares
// New distribution:
// Founder: 50/120 = 41.67%
// CoFounder: 50/120 = 41.67%
// NewMember: 20/120 = 16.67%
Scenario 3: Checking Pending Balances
Before claiming, payees can check their pending amount:
uint256 myPending = splitter.pending(0xMyAddress...);
// Returns the amount available to claim
Security Considerations
Strengths
- Reentrancy Protection: Uses the checks-effects-interactions pattern, updating state before external calls
- Access Control: Owner-only functions prevent unauthorized modifications
- Input Validation: Comprehensive checks prevent invalid operations
-
Safe Transfers: Uses
call()with proper error handling
Potential Improvements
-
Reentrancy Guard: Consider adding OpenZeppelin’s
ReentrancyGuardfor additional protection - Payee Removal: Current design doesn’t allow removing payees (by design for immutability)
- Share Modification: Shares are immutable once set (prevents manipulation)
- Emergency Pause: Could add pause functionality for critical situations
Gas Optimization
The contract is optimized for gas efficiency:
-
View Functions:
pending()and other view functions are free to call - Pull Payments: Recipients pay gas for their own withdrawals
- Minimal Storage: Only stores essential data
- Efficient Calculations: Simple arithmetic operations
Events & Transparency
The contract emits three key events:
event PayeeAdded(address account, uint256 shares);
event PaymentReleased(address to, uint256 amount);
event PaymentReceived(address from, uint256 amount);
These events enable:
- Off-chain monitoring and analytics
- Frontend integration
- Audit trails
- Real-time notifications
Testing Recommendations
When testing this contract, consider:
-
Unit Tests:
- Adding payees with various share configurations
- Depositing funds and verifying calculations
- Releasing payments to multiple recipients
- Edge cases (zero shares, zero balance, etc.)
-
Integration Tests:
- Multiple deposits over time
- Adding payees after deposits
- Concurrent release calls
- Large number of payees
-
Security Tests:
- Unauthorized access attempts
- Reentrancy attacks
- Integer overflow/underflow
- Zero address handling
Deployment Considerations
Conflux Network Specifics
- Network: Deploy on Conflux mainnet or testnet
- Gas Costs: CFX is used for gas (similar to ETH on Ethereum)
- Block Time: Conflux has faster block times (~1 second)
- Compatibility: Uses standard Solidity, compatible with Conflux EVM
Deployment Steps
- Compile the contract with Solidity 0.8.19+
- Deploy to Conflux network
- Verify contract on ConfluxScan
- Initialize by adding payees
- Share contract address with recipients
Use Cases
- SaaS Revenue Sharing: Automatically distribute subscription revenue among team members
- NFT Royalties: Split royalty payments between creators and platform
- DAO Treasury: Distribute funds to multiple stakeholders
- Escrow Services: Hold and split funds based on milestones
- Affiliate Programs: Automate commission payments
Conclusion
The Conflux Payment Splitter provides a robust, flexible solution for automated payment distribution on the Conflux blockchain. Its dynamic payee management, proportional share system, and pull-payment model make it suitable for various use cases while maintaining security and gas efficiency.
The contract’s simplicity is its strength—it does one thing well: fairly and transparently split payments among multiple recipients based on configurable shares.
Full Contract Code
For reference, here’s the complete contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/**
* @title ConfluxPaymentSplitter (Dynamic Version)
* @notice Very simple ETH-only payment splitter WITHOUT constructor.
* Admin manually adds payees + shares after deployment.
*/
contract ConfluxPaymentSplitter {
address public owner;
event PayeeAdded(address account, uint256 shares);
event PaymentReleased(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;
address[] private _payees;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
}
// Accept CFX from wallets
receive() external payable {
emit PaymentReceived(msg.sender, msg.value);
}
// Accept CFX by calling this function
function deposit() external payable {
require(msg.value > 0, "No CFX sent");
emit PaymentReceived(msg.sender, msg.value);
}
/* ----------------------------- VIEW FUNCTIONS ---------------------------- */
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 pending(address account) public view returns (uint256) {
uint256 totalReceived = address(this).balance + _totalReleased;
return _pendingPayment(account, totalReceived, _released[account]);
}
/* ----------------------------- ADMIN FUNCTIONS ---------------------------- */
/// @notice Add a payee and assign shares (for dynamic list)
function addPayee(address account, uint256 shares_) external onlyOwner {
require(account != address(0), "zero address");
require(shares_ > 0, "shares = 0");
require(_shares[account] == 0, "already added");
_payees.push(account);
_shares[account] = shares_;
_totalShares += shares_;
emit PayeeAdded(account, shares_);
}
/* ------------------------------ PAYMENT LOGIC ----------------------------- */
function release(address payable account) public {
require(_shares[account] > 0, "no shares");
uint256 totalReceived = address(this).balance + _totalReleased;
uint256 payment = _pendingPayment(account, totalReceived, _released[account]);
require(payment > 0, "nothing to release");
_released[account] += payment;
_totalReleased += payment;
(bool ok, ) = account.call{value: payment}("");
require(ok, "ETH transfer failed");
emit PaymentReleased(account, payment);
}
/* ----------------------------- INTERNAL LOGIC ----------------------------- */
function _pendingPayment(
address account,
uint256 totalReceived,
uint256 alreadyReleased
) private view returns (uint256) {
return (totalReceived * _shares[account]) / _totalShares - alreadyReleased;
}
}
Resources
Demo Video:https://youtu.be/BKbMU5ys0vI
GitHub repo: https://github.com/Vikash-8090-Yadav/Cfxpaymentsplit
- Conflux Network: https://confluxnetwork.org
- Solidity Documentation: https://docs.soliditylang.org
- OpenZeppelin Contracts: https://docs.openzeppelin.com/contracts