Create ERC20 token payment split smart contract

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

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:

Create ERC20 token payment split smart contract

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.

Create ERC20 token payment split smart contract

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.

Create ERC20 token payment split smart contract

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

Create ERC20 token payment split smart contract

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

Create ERC20 token payment split smart contract

The OpenZeppelin contract library needs to be installed.

Create ERC20 token payment split smart contract

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;

}

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_,
    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"
    );
    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)) + _totalTokenReleased;
    uint256 payment = (tokenTotalReceived * _shares[account]) / _totalShares - _tokenReleased[account];
    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 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.

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.

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 () => {}

}

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.address]
    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.

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.address]
    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 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 token sent to pool', async () => {
    payeeAddressArray = [account1.address, account2.address, account3.address, account4.address]
    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)
    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.

Create ERC20 token payment split smart contract

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.

 

Posted by:CoinYuppie,Reprinted with attribution to:https://coinyuppie.com/create-erc20-token-payment-split-smart-contract/
Coinyuppie is an open information publishing platform, all information provided is not related to the views and positions of coinyuppie, and does not constitute any investment and financial advice. Users are expected to carefully screen and prevent risks.

Like (2)
Donate Buy me a coffee Buy me a coffee
Previous 2021-09-17 22:29
Next 2021-09-17 22:31

Related articles