You are on page 1of 13

10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO,

NFT, DAO, DeFi, Dogecoin, …

Create ERC20 token payment split smart contract


CoinYuppie • 2021-09-17 22:30 • Ethereum News

In almost every field of cryptocurrency, payment is a recurring topic, especially the provision of
payment to multiple pledgers. For example, DAO wants to fund multiple programs, DEX wants to
merge transaction fees to certain participants, or the team wants to distribute tokens to team
members as a monthly salary.

Smart contracts allow us to automate these types of payment functions, which limits the potential
errors caused by manual payment management and allows us to spend precious time on other
productive tasks.

Today, we will learn how to create our own ERC20 token payment splitter, which can be
incorporated into any project!

Prerequisites and settings

The following content requires you to be familiar with Solidity, but anyone can learn.

Project structure
We will create two contracts. The first will be an ERC20 token payment split smart contract, and the
second will be a simulated pool smart contract. The ERC20 token payment splitter smart contract
will be abstract and hold the logic and data used to manage the payer and their respective
payment parts.  The simulation pool will inherit the ERC20 token payment splitter so that we can
automatically distribute payments to multiple pledgers.  There are two reasons for splitting the
payment function in the two contracts:

Demonstrate the use of token payment split contracts in real-world use cases

Ensure that the token payment splitting contract is flexible enough that anyone can choose
and integrate into their own projects

OpenZeppelin already has a smart contract called PaymentSplitter.sol. Used for Ethereum payment


split. We will take advantage of this existing feature and customize it so that it can work with ERC20
tokens.

Set up the development environment

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 1/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

Tools in this tutorial:

Safety helmet-smart contract development environment

OpenZeppelin-Audited smart contract template

Now use NPM init -y in an empty directory to start an NPM project

After setting up the project, install Hardhat using the following command:

After installing Hardhat, enter npx Hardhat and select the option to create a basic example
project.  This will include a convenient file structure to easily create, test, and deploy your own
contracts.

Choose to create a basic sample project

You can delete the Greeter.sol file in the contract folder, and delete the sample-test.js file from the
test folder.

We will also install a library of hardhat plugins, which are Hardhat plugins.  They allow us to add
tools for testing and deploying smart contracts.

At the top of the hardhat.config.js file, add

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 2/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

A package called chai needs to be installed to test our smart contract.

The OpenZeppelin contract library needs to be installed.

Create a token payment splitter

This token payment split smart contract will provide logic to set up and store data related to the
payee list and the share of each payee. The number of shares held by each payee is equal to the
proportion of funds they should receive (for example, if there are 4 payees and each person holds 5
shares, then each of them will receive 25% of any expenditure).

To start this contract, we will create a new file in our contract folder and name it
TokenPaymentSplitter.sol.

Set the pragma line and contract shell.

pragma solidity ^0.8.0;

abstract contract TokenPaymentSplitter {

Note that this is an abstract contract and we will import it into the simulation pool contract
later. Making it abstract also allows us to easily import this contract into any other real project in
the future.

Now let’s import a useful tool from OpenZeppelin.

pragma solidity ^0.8.0;

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

abstract contract TokenPaymentSplitter {

   using SafeERC20 for IERC20;

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 3/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

SafeERC20.sol provides the ERC20 interface, which allows us to call standard functions from any
ERC20 smart contract and wrap these calls in additional functions to provide a safer way to transfer
tokens.

Now, we will create variables to store contract data.

abstract contract TokenPaymentSplitter {

   using SafeERC20 for IERC20;

   address internal paymentToken;

   uint256 internal _totalShares;

   uint256 internal _totalTokenReleased;

   address[] internal _payees;

   mapping(address => uint256) internal _shares;

   mapping(address => uint256) internal _tokenReleased;

paymentToken is the address of the ERC20 token we used for payment.

_totalShares provides the addition of shares from all payees.

_totalTokenReleased is the total amount of payment tokens that have been paid to all recipients.

_payees provides an array of all current payee addresses.

_shares is the mapping between the address of the payee and the number of shares allocated to
them.

_tokenReleased is the mapping from the payee address to the number of payment tokens.

Now place a constructor that accepts three parameters. The first parameter is the array of payees
that we want to initialize in the contract deployment. The second parameter is an array of shares
for each payee. The third is the address of the ERC20 token that will be used for payment.

pragma solidity 0.8.0

constructor(

   address[] memory payees,

   uint256[] memory shares_,

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 4/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

   address _paymentToken

) {

   require(

       payees.length == shares_.length,

       "TokenPaymentSplitter: payees and shares length mismatch"

   );

   require(payees.length > 0, "TokenPaymentSplitter: no payees");

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

       _addPayee(payees[i], shares_[i]);

   }

   paymentToken = _paymentToken;

The constructor contains a require statement to ensure that the two arrays have the same length so
that each payee has a share allocated to them. There is another require statement to ensure that
the contract is initialized and there is at least one payee.

There is also a for loop, which assigns each payee and its share to the variables we created
above. This is done through a function called _addPayee, which we will create soon.

After the constructor is ready, add a few more functions to call and get contract variables.

pragma solidity 0.8.0

function totalShares() public view returns (uint256) {

   return _totalShares;

function shares(address account) public view returns (uint256) {

   return _shares[account];

function payee(uint256 index) public view returns (address) {

   return _payees[index];

Now we will create a function to add a payee.

pragma solidity 0.8.0;

function _addPayee(address account, uint256 shares_) internal {

   require(

       account != address(0),

       "TokenPaymentSplitter: account is the zero address"

   );

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 5/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

   require(shares_ > 0, "TokenPaymentSplitter: shares are 0");

   require(

       _shares[account] == 0,

       "TokenPaymentSplitter: account already has shares"

   );

   _payees.push(account);

   _shares[account] = shares_;

   _totalShares = _totalShares + shares_;

_addPayee is the function we call in the constructor to set the payee array. This function has two
parameters, the payee’s account and the number of shares associated with it.  Then it checks
whether the account is a zero address, whether the share is greater than zero, and whether the
account has been registered as a payee. If all checks pass, then we add the data to the respective
variables.

Now let’s add a function to support the distribution of tokens to payees.

pragma solidity 0.8.0;

function release(address account) public virtual {

   require(

       _shares[account] > 0, "TokenPaymentSplitter: account has no shares"

   );

   uint256 tokenTotalReceived = IERC20(paymentToken).balanceOf(address(this)) + _totalTo


   uint256 payment = (tokenTotalReceived * _shares[account]) / _totalShares - _tokenRelea
   require(payment != 0, "TokenPaymentSplitter: account is not due payment");

   _tokenReleased[account] = _tokenReleased[account] + payment;

   _totalTokenReleased = _totalTokenReleased + payment;

   IERC20(paymentToken).safeTransfer(account, payment);

Release is a function that anyone can call. It accepts the parameters of an existing payee
account.  Let’s analyze what happened in this function.  First, it checks whether the account has
shares allocated to it. Then, it creates a variable called tokenTotalReceived, which adds the current
token balance of the contract to the total number of tokens previously released.  Create another
variable called payment, which determines how much of the total amount of tokens received is
owed to the account, and then subtracts how much has been released to the account.  Then, a
require statement checks whether the current payment amount is greater than zero (that is,
whether more tokens are currently owed). If the check passes, the tokenReleased of the account is

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 6/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

updated , and the totalTokenReleased is updated . Finally, the amount of tokens paid to the account


is transferred.

The function is now in place! But there is one more thing to do in this contract…event!

We will add two events to the contract. It is a good practice to add events to the top of the
contract.

pragma solidity 0.8.0;

event PayeeAdded(address account, uint256 shares);

event PaymentReleased(address to, uint256 amount);

After these events are included in the contract, we will emit them in the appropriate function.

pragma solidity 0.8.0;

function _addPayee(address account, uint256 shares_) internal {

   ///existingFunctionCode

   emit PayeeAdded(account, shares_);

function release(address account) public virtual {

   ///existingFunctionCode

   emit PaymentReleased(account, payment);

Now the token payment splitting contract has been established! To understand how this works in a
real scenario, let’s create a simulated pool contract that will import the token payment splitter.

Create a mock pool contract

This contract will not be very complicated, because we just want to demonstrate how to integrate a
token payment splitter.  This contract regularly receives specific ERC20 tokens that we want to
distribute to the payee list. This ERC20 token can be reached through different scenarios, such as
user deposits or redirection fees from another smart contract. In real life, depending on different
projects, there may be a more complex contract that contains more functions to meet the user’s
use case.

In the contract folder, create a new file named MockPool.sol. Then add the following code.

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 7/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

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

import "./TokenPaymentSplitter.sol";

contract MockPool is Ownable, TokenPaymentSplitter {

   using SafeERC20 for IERC20;

   constructor(

       address[] memory _payees,


       uint256[] memory _shares,
       address _paymentToken

   ) TokenPaymentSplitter(_payees, _shares, _paymentToken) {}

   function drainTo(address _transferTo, address _token) public onlyOwner {

       require(

       _token != paymentToken,

       "MockPool: Token to drain is PaymentToken"

       );

       uint256 balance = IERC20(_token).balanceOf(address(this));

       require(balance > 0, "MockPool: Token to drain balance is 0");

       IERC20(_token).safeTransfer(_transferTo, balance);

   }

In this contract, three things are imported. The first is OpenZeppelin’s Ownable utility, which uses
the only Owner modifier on some functions.  The second is SafeERC20, which allows safe ERC20
token transfers, as will be seen in the contract. The third is our TokenPaymentSplitter contract.

In the MockPool constructor, we need TokenPaymentSplitter to provide the same three parameters,
we just pass them to our inherited contract.

Another function is added to this contract, drainTo.  It actually has nothing to do with the
TokenPaymentSplitter contract.  It is just a security mechanism when another ERC20 token that is
not set as a payment token is sent to the pool, and then there is a way for the contract owner to
release the token.

Test contract

Testing smart contracts is as important as creating them.  The assets handled by these contracts
usually belong to other people, so as developers, we are responsible for ensuring that these assets
work the way they should, and our tests can cover almost all edge cases.

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 8/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

The tests that will be performed here are some examples to show that the TokenPaymentSplitter
smart contract works as expected. When working on your own project, you may want to create a
test specifically suited to your use case.

In order to support our testing, we want to include an ERC20 token. For this, we will create a new
solididity file, which will be imported into the OpenZepplin ERC20 template for our testing. In the
contract folder, create a new file named Imports.sol and include the following code:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";

contract Imports {}

Now, create a file named test.js in the test folder. At the top of this file, we will import the packages
that support our tests.

const { expect } = require('chai')

const { ethers } = require('hardhat')

Now, in order to set up the test, we will first create the necessary variables, create the beforeEach
function, which is called before each test, and create an empty describe function, which will soon
contain our test.

describe('TokenPaymentSplitter Tests', () => {

let deployer

let account1

let account2

let account3

let account4

let testPaymentToken

let mockPool

beforeEach(async () => {

   [deployer, account1, account2, account3, account4] = await ethers.getSigners()

   const TestPaymentToken = await ethers.getContractFactory('ERC20PresetMinterPauser')

   testPaymentToken = await TestPaymentToken.deploy('TestPaymentToken', 'TPT')

   await testPaymentToken.deployed()

})

describe('Add payees with varying amounts and distribute payments', async () => {}

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 9/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

With these parts in place, let’s get to the core part of these tests!

Payment tokens are evenly distributed to multiple payees


In our first test, we want to see what happens when we deploy a contract that contains a payee list
with an evenly distributed share. Below is the test code.

it('payment token is distributed evenly to multiple payees', async () => {

   payeeAddressArray = [account1.address, account2.address, account3.address, account4.ad


   payeeShareArray = [10, 10, 10, 10]

   const MockPool = await ethers.getContractFactory('MockPool')

   mockPool = await MockPool.deploy(

       payeeAddressArray,

       payeeShareArray,

       testPaymentToken.address

   )

   await mockPool.deployed()

   await testPaymentToken.mint(mockPool.address, 100000)

   await mockPool

       .connect(account1)

       .release(account1.address)

   await mockPool

       .connect(account2)

       .release(account2.address)

   await mockPool

       .connect(account3)

       .release(account3.address)

   await mockPool

       .connect(account4)

       .release(account4.address)

   const account1TokenBalance = await testPaymentToken.balanceOf(account1.address)

   const account2TokenBalance = await testPaymentToken.balanceOf(account2.address)

   const account3TokenBalance = await testPaymentToken.balanceOf(account3.address)

   const account4TokenBalance = await testPaymentToken.balanceOf(account4.address)

   expect(account1TokenBalance).to.equal(25000)

   expect(account2TokenBalance).to.equal(25000)

   expect(account3TokenBalance).to.equal(25000)

   expect(account4TokenBalance).to.equal(25000)

})

In this test, we assign the contract to 4 payees, each of whom has 10 equal shares. Then we send
100000 units of testPaymentToken to the contract and issue payment to each payee.  It can be
noticed in the test that each payee is calling a function to release tokens to himself.

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 10/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

Payment tokens are unevenly distributed to multiple payees


In the second test, we want to make sure that the mathematical calculations are still valid even if
the share of each payee is unevenly distributed.

it('payment token is distributed unevenly to multiple payees', async () => {

   payeeAddressArray = [account1.address, account2.address, account3.address, account4.ad


   payeeShareArray = [10, 5, 11, 7]

   const MockPool = await ethers.getContractFactory('MockPool')

   mockPool = await MockPool.deploy(

       payeeAddressArray,

       payeeShareArray,

       testPaymentToken.address

   )

   await mockPool.deployed()

   await testPaymentToken.mint(mockPool.address, 100000)

   await mockPool

       .connect(account1)

       .release(account1.address)

   await mockPool

       .connect(account2)

       .release(account2.address)

   await mockPool

       .connect(account3)

       .release(account3.address)

   await mockPool

       .connect(account4)

       .release(account4.address)

   const mockPoolTestPaymentTokenBalance = await testPaymentToken.balanceOf(

       mockPool.address

   )

   const account1TokenBalance = await testPaymentToken.balanceOf(account1.address)

   const account2TokenBalance = await testPaymentToken.balanceOf(account2.address)

   const account3TokenBalance = await testPaymentToken.balanceOf(account3.address)

   const account4TokenBalance = await testPaymentToken.balanceOf(account4.address)

   expect(mockPoolTestPaymentTokenBalance).to.equal(1)

   expect(account1TokenBalance).to.equal(30303)

   expect(account2TokenBalance).to.equal(15151)

   expect(account3TokenBalance).to.equal(33333)

   expect(account4TokenBalance).to.equal(21212)

})

It seems that the payee can still get the money, but have you noticed anything? There is one unit of
payment token left in the contract! Since Solidity has no decimals, when it reaches the lowest unit,
it will usually be rounded, which may cause contract dust to fly, as we have seen here.  But don’t

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 11/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

worry, because we expect payment tokens to flow into the contract in the future, so it will continue
to be distributed.

Payment tokens are distributed unevenly to multiple payees, and additional


payment tokens are sent to the pool
This is similar to the previous test, except that more payment tokens are added to the pool before
the funds are released to the recipient. This shows that as payment tokens continue to flow into the
simulated pool contract, mathematics can still ensure that the payee receives the correct amount.

it('payment token is distributed unevenly to multiple payees with additional payment toke
   payeeAddressArray = [account1.address, account2.address, account3.address, account4.ad
   payeeShareArray = [10, 5, 11, 7]

   const MockPool = await ethers.getContractFactory('MockPool')

   mockPool = await MockPool.deploy(

       payeeAddressArray,

       payeeShareArray,

       testPaymentToken.address

   )

   await mockPool.deployed()

   await testPaymentToken.mint(mockPool.address, 100000)

   await mockPool

       .connect(account1)

       .release(account1.address)

   await mockPool

       .connect(account2)

       .release(account2.address)

   await testPaymentToken.mint(mockPool.address, 100000)

   await mockPool

       .connect(account3)

       .release(account3.address)

   await mockPool

       .connect(account4)

       .release(account4.address)

   await mockPool

       .connect(account1)

       .release(account1.address)

   await mockPool

       .connect(account2)

       .release(account2.address)

   const mockPoolTestPaymentTokenBalance = await testPaymentToken.balanceOf(

       mockPool.address

           )

   const account1TokenBalance = await testPaymentToken.balanceOf(account1.address)

   const account2TokenBalance = await testPaymentToken.balanceOf(account2.address)

   const account3TokenBalance = await testPaymentToken.balanceOf(account3.address)

   const account4TokenBalance = await testPaymentToken.balanceOf(account4.address)

   expect(mockPoolTestPaymentTokenBalance).to.equal(1)

   expect(account1TokenBalance).to.equal(60606)

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 12/13
10/03/2022 12:48 Create ERC20 token payment split smart contract - CoinYuppie: Bitcoin, Ethereum, Metaverse, NFT, DAO, DeFi, Dogecoin, …

   expect(account2TokenBalance).to.equal(30303)

   expect(account3TokenBalance).to.equal(66666)

   expect(account4TokenBalance).to.equal(42424)

})

Now that all the tests are ready, it’s time to run them and see if they work! In the project root
folder, use npx hardhat test to start the tests. If everything is correct, then you should see all the
green squares as shown in the image below.

As mentioned above, we need to do more testing to ensure that the entire project/agreement
works as expected, and the payment splitter is an integrated part of it.  This will mean more unit
tests to cover all available functions, as well as more complex integration tests, depending on the
specific use case.

Summarize

Payments are a common aspect of many encryption protocols, and there are several ways to solve
them. Today we learned a way to manage payments, although users can even build on this contract
to meet your specific needs, such as enabling payments across multiple tokens, adding additional
payees, or removing payees , Or distribute all payments at the same time in one function call.

https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/ 13/13

You might also like