A simplified smart contract for splitting ERC20 tokens (like PYUSD) among multiple recipients based on predefined shares. This project demonstrates smart contract development practices and provides a clean educational example for PYUSD integration on Arbitrum.
✨ NEW: Factory Pattern for Frontend Integration - The repo now includes SimpleSplitterCloneable and SimpleSplitterFactory contracts that enable cheap, gas-efficient deployment of splitters via a factory pattern.
Think of SimpleSplitter like an automated accountant for splitting money:
Real-world example: You and two friends start a pizza delivery business. You agree that:
- You (the founder) get 50% of profits
- Friend A gets 30% of profits
- Friend B gets 20% of profits
Instead of manually calculating splits every day, SimpleSplitter:
- Stores your business income when tokens are sent to it
- Calculates each person's share based on your predefined agreement
- Distributes the tokens to everyone's wallet when someone calls the
distribute()function
Why use smart contracts? No arguments about math, no manual calculations, no trust issues - the code handles the splitting logic transparently and immutably on the blockchain.
Educational Note: This project is designed for learning smart contract development. It's not intended for production use without proper security audits.
Time Expectation: About 60 minutes to go from no smart contract knowledge to a deployed PYUSD splitter you can test on Arbitrum Sepolia.
- Programming concepts (variables, functions, if/else statements)
- Command line/terminal usage (running commands, navigating directories)
- Git and GitHub basics (cloning repositories, basic version control)
- A practical smart contract example with real-world use case
- Development workflow with Foundry
- Testing (unit tests and fork tests)
- Deployment to Arbitrum testnet (sepoloia) and mainnet (one)
- Security patterns for token handling
- Factory pattern for cheap contract cloning
- A working, deployed frontend
- Solidity concepts (inheritance, libraries, etc.)
- Full PYUSD token standards or implementation details
- Production-grade security practices (this is a simplified example)
Before diving in, here are key concepts you'll encounter:
Smart Contract: A program that runs on the blockchain. Once deployed, it executes automatically according to its code - no human intervention needed. Learn more about smart contracts
ERC20 Token: A standard for digital tokens (like digital coins). PYUSD is an ERC20 token representing US dollars on the blockchain. ERC20 Standard (EIP-20) | Ethereum.org Token Guide
Immutable: Once set during deployment, these values can never be changed. This provides security and predictability - no one can alter the rules later. Solidity Immutable Documentation
Gas: The fee paid to run operations on the blockchain. Think of it like postage for sending transactions. Ethereum.org Gas Guide
Arbitrum: A "Layer 2" network that is built on top of Ethereum, but is faster and cheaper to use. Arbitrum Documentation | Arbitrum Portal
Testnet: A practice version of the blockchain where you can experiment without real. Ethereum.org Testnets Guide
Mainnet: The real blockchain where actual money is involved. We won't touch this in this tutorial, but with a few config changes, you could deploy to mainnet just as easily as testnet.
Reentrancy: A type of attack where your contract calls a function of another contract that maliciously tries to call back into your contract before the first call finishes. SimpleSplitter uses OpenZeppelin's ReentrancyGuard to prevent this. OpenZeppelin ReentrancyGuard | Consensys Reentrancy Guide
SafeERC20: A library that adds extra safety checks when transferring tokens, preventing common mistakes. OpenZeppelin SafeERC20 Documentation
Foundry: Your smart contract development toolkit - compiles, tests, and deploys contracts. Foundry Docs
Solidity: The programming language for Ethereum smart contracts (similar to JavaScript or C++). Solidity Documentation | Solidity by Example
Arbiscan: A block explorer for Arbitrum, like Etherscan for Ethereum. It lets you view transactions, contracts, and balances on the Arbitrum network. Arbiscan | Arbitrum Sepolia Testnet Explorer
You'll need these tools installed on your computer:
Foundry - Your smart contract development toolkit
- What it does: Compiles Solidity code, runs tests, and deploys contracts
- Install: Follow the official installation guide
- Verify installation: Run
forge --version(should show version number likeforge 1.2.3)
just - Command runner (makes complex commands simple)
- What it does: Turns long, complex commands into short, memorable ones like
just test - Install: Follow the installation guide
- Verify installation: Run
just --version(should show version number likejust 1.42.0)
You can use mise-en-place to install both tools automatically if you prefer such an approach. There's already a mise.toml file in this repository.
Run these commands to make sure everything is installed correctly:
# Check if tools are installed
forge --version # Should show: forge 0.2.0 (or similar)
just --version # Should show: just 1.42.0 (or similar)
git --version # Should show: git version 2.x.xIf any command shows "command not found", revisit the installation guides above.
git clone https://github.com/mono-koto/pyusd-simple-splitter.git
cd pyusd-simple-splitter
forge installIf you don't have a wallet yet, you can generate a new one using the just command:
# Generate a new wallet
just generate-wallet
# This will output something like:
# Successfully created new keypair.
# Address: 0x1234567890123456789012345678901234567890
# Private key: 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890Create a .env file based on .env.example and fill in your details:
PYUSD_ARB_SEPOLIA=0x637A1259C6afd7E3AdF63993cA7E58BB438aB1B1
ARBITRUM_SEPOLIA_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
ETHERSCAN_API_KEY=
PRIVATE_KEY=Get an Etherscan API key at https://etherscan.io/apidashboard. You'll need to create a (free) account.
Use your generated wallet's private key in the PRIVATE_KEY field.
🧐 Security Note: See how we put the private key in a
.envfile like that? Don't do that with keys you'll use in production or with mainnet. Use a more secure approach like a keystore with secret, hardware wallet, or a secrets manager.
-
Arbitrum Sepolia ETH:
-
PYUSD on Arbitrum Sepolia:
You can check your balances with:
just eth-balance 0xYourWalletAddress
just pyusd-balance 0xYourWalletAddressWe've prepared deployment and operational processes using just commands. You can examine the justfile to see exactly how we're using Foundry to deploy and interact with the contracts.
If you don't have testnet PYUSD or if you just want to test with a mock token, you can deploy our mock PYUSD-like token using the following command:
# Deploy a mock PYUSD-like token
just mock-token-deploy "Mock PYUSD" "MYPYUSD"This will output the deployed contract address. Save this address for the next step.
You can also mint some tokens to your wallet or your deployed splitter for testing:
# Mint 1000 Mock PYUSD tokens to your wallet
just mock-token-mint "0xYourMockTokenAddress" "0xYourWalletAddress" 1000To deploy the SimpleSplitter contract, you need to provide the recipient addresses, and their corresponding shares.
# Deploy SimpleSplitter with PYUSD token and recipients
just splitter-deploy \
"0xRecipient1,0xRecipient2,0xRecipient3" \
"50,30,20"just splitter-deploy \
"0xAlice123...,0xBob456...,0xCharlie789..." \
"40,35,25"This creates a splitter where:
- Alice receives 40% of distributed tokens
- Bob receives 35% of distributed tokens
- Charlie receives 25% of distributed tokens
To use a token other than testnet PYUSD, you can specify the token address as follows:
# Deploy SimpleSplitter with your own token address
just splitter-deploy \
"0xRecipient1,0xRecipient2,0xRecipient3" \
"50,30,20" \
"0xYourTokenAddress"To distribute tokens, we use the distribute() function on the splitter.
# Distribute tokens to all recipients
just splitter-distribute "0xSplitterAddress"The main contract with the following key features:
Constructor Parameters:
token: ERC20 token contract addressrecipients: Array of recipient addressesshares: Array of corresponding share weights
Main Functions:
distribute(): Distributes current token balance among recipientscalculateRecipientAmount(index): Calculates amount for a specific recipient
View Functions:
token(): Returns the configured token addressrecipients(index): Returns recipient at indexshares(index): Returns shares at indextotalShares(): Returns total sharesrecipientCount(): Returns number of recipients
A 6-decimal ERC20 token for testing that mimics PYUSD characteristics:
- 6 decimal places (like PYUSD)
- Initial supply of 1M tokens
- Mintable for testing purposes
An interface that defines the SimpleSplitter contract's public API. This interface provides several benefits:
When other contracts need to interact with SimpleSplitter, they can import this interface instead of the implementation. Since the interface is only a type declaration, it does not contribute to the bytecode size of the contract that imports it. This helps keep the contract size smaller.
import {ISimpleSplitter} from "./ISimpleSplitter.sol";
contract MyContract {
ISimpleSplitter public splitter;
constructor(address splitterAddress) {
splitter = ISimpleSplitter(splitterAddress);
}
function triggerDistribution() external {
// Use the interface to interact with SimpleSplitter
uint256 balance = splitter.token().balanceOf(address(splitter));
if (balance > 0) {
splitter.distribute();
}
}
}- All view functions from SimpleSplitter
distribute()function for triggering distributions- Events:
TokensDistributed,RecipientPaid - Custom errors for better error handling
// Assume SimpleSplitter is deployed at splitterAddress
// and has been sent 1000 PYUSD tokens
SimpleSplitter splitter = SimpleSplitter(splitterAddress);
// Check distributable balance
uint256 balance = splitter.getDistributableBalance(); // Returns 1000 * 10^6
// Distribute tokens to all recipients
splitter.distribute();
// Recipients now have tokens according to their sharescontract MyContract {
ISimpleSplitter public splitter;
IERC20 public token;
constructor(address _splitter, address _token) {
splitter = ISimpleSplitter(_splitter);
token = IERC20(_token);
}
function distributeRevenue(uint256 amount) external {
// Transfer tokens to splitter
token.transfer(address(splitter), amount);
// Trigger distribution
splitter.distribute();
}
}The project includes test coverage:
- Constructor validation
- Distribution logic
- Edge cases and error conditions
- Fuzz testing for various amounts and configurations
- Integration testing against Arbitrum testnet
- Real ERC20 token interaction
- Gas usage validation
- End-to-end distribution scenarios
- Immutable: Recipients and shares cannot be changed after deployment
- Reentrancy: Uses OpenZeppelin's ReentrancyGuard
- Safe Transfers: Uses SafeERC20 for all token operations
- Input Validation: Validation in constructor
- Integer Division: Remainder tokens stay in contract (can be distributed in future calls)
We've added a justfile for easy command execution.
# Run all tests
just test
# Run with verbose output
just test-verbose
# Run only unit tests
just test-unit
# Run only fork tests (requires RPC access)
just test-fork
# Get all available commands
just helpBy default, all commands use Arbitrum Sepolia testnet. To switch networks:
# Use default (Arbitrum Sepolia)
just splitter-deploy "0xAddr1,0xAddr2" "50,50"
# Use Arbitrum mainnet
just rpc=arbitrum splitter-deploy "0xAddr1,0xAddr2" "50,50"The rpc variable can be set to any RPC endpoint defined in foundry.toml:
arbitrum_sepolia(default)arbitrum
You can go deeper with Foundry and smart contract development. Explore Foundry's other features such as
- Scripting (which allows you to write scripts in Solidity to automate tasks)
- Running a local dev node with Anvil
- Tracking gas usage
MIT License - see LICENSE file for details.