Create an ERC20 token payment splitting smart contract

0
33

Payments are a reoccurring topic across pretty much every area of crypto, specifically providing payments to multiple stakeholders. For instance, a DAO wants to provide funding to multiple initiatives, a DEX want to consolidate and distribute trading fees to certain participants, or a team wants to distribute tokens to team members as a monthly paycheck.

Smart contracts allow us to automate these type of payment functions, which limits potential mistakes caused by manually managing payments and allows us to spend our valuable time on other productive tasks.

I recently needed to create an ERC20 token payment splitter for AirSwap, a DeFi swapping protocol, as protocol fees are distributed to multiple destinations (e.g., dev fund, community rewards, etc.). If you want to see a version of the token payment splitter contract in the wild then check out AirSwap’s github.

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

Prerequisite and set up

The following requires you to be somewhat familiar with Solidity, though anyone can follow along!

Project Architecture

We will create two contracts. The first will be the ERC20 token payment splitter smart contract, and the second will be a mock pool smart contract. The ERC20 token payment splitter smart contract will be abstract and hold the logic and data for managing payees and their respective payout portions. The mock pool will inherit the ERC20 token payment splitter so that we can distribute payments automatically to multiple stakeholders. The reason for this separation of payment functionality among two contracts, is two fold:

  • Show the use of a token payment splitter contract in a real world use case
  • Ensure the token payment splitter contract is flexible enough for anyone to pick up and integrate into their own project

OpenZeppelin has an existing smart contract called PaymentSplitter.sol, which is used for splitting payments of Ether. We will leverage this existing functionality and customize it to work with ERC20 tokens.

Dev Environment Set Up

Tools in this tutorial:

  • Hardhat — smart contract developer environment
  • OpenZeppelin — audited smart contract templates

If you do not already have Node.js ≥14 version installed then use this link to find the installer.

Now in an empty directory initiate an NPM project with npm init -y

Once the project is set up, install Hardhat with:

npm install --save-dev hardhat

After Hardhat is installed, enter npx hardhat and select the option to Create a basic sample project. This will include a handy file structure to easily create, test, and deploy your own contracts.

Select Create a basic sample project

You can delete the file Greeter.sol found in the contracts folder and remove the file sample-test.js from the test folder.

We are also going to install two more libraries that are Hardhat plugins. They allow us to add utilities for testing and deploying our smart contract.

npm install --save-dev @nomiclabs/hardhat-waffle @nomiclabs/hardhat-ethers

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

require(“@nomiclabs/hardhat-waffle”); //this should already be there
require(“@nomiclabs/hardhat-ethers”);

We will also install a package called chai, which will be used for testing our smart contract.

npm install --save-dev chai

Let’s also install the OpenZeppelin contract library.

npm install --save-dev @openzeppelin/contracts

Creating the token payment splitter

This token payment splitter smart contract will provide logic to set and store data involving a list of payees and shares per each payee. The number of shares per payee is equal to the proportion of funds they should receive (e.g., if there are 4 payees and each has 5 shares, then they would each receive a proportion of 25% of any payout).

To get started on this contract, we will create a new file in our contracts folder and name it TokenPaymentSplitter.sol.

Let’s set up our pragma line and contract shell.

Note that this is an abstract contract, as we will later import it into a mock pool contract. Making it abstract will also allow us to easily import this contract into any other real project in the future.

Now let’s import a useful utility from OpenZeppelin.

SafeERC20.sol provides the ERC20 interface, which will allow us to call standard functions from any ERC20 smart contract, and wraps those calls in additional functionality to provide a more secure way to transfer tokens.

We’ll now create variables to store the contract’s data.

paymentToken is the address of the ERC20 token that we will use for payments.

_totalShares provides the addition of shares from all payees.

_totalTokenReleased is the total amount of payment token that has been paid out to all payees.

_payees provides an array of all current payees’ addresses

_shares is a mapping of a payee’s address to the number of shares assigned to them.

_tokenReleased is a mapping of a payee’s address to the amount of payment token that has been paid out to them.

Now let’s put in place a constructor that will accept three parameters. The first parameter is an array of the payees that we want to initialize with the contract deployment. The second parameter is an array of the shares per payee. The third is the address for the ERC20 token that will be used for payments.

The constructor contains a require statement to make sure the two arrays have the same length so that each payee will have shares assigned to them. There is another require statement to make sure the contract initializes with at least one payee.

There is also a for-loop that assigns each payee and their shares to the variables we created above. This is done via a function called _addPayee, which we will create shortly.

With the constructor in place, let’s add a few more functions to call and get the contract variables.

Now we will create the function for adding payees.

_addPayee is the function we called in the constructor to set the array of payees. This function takes two parameters, the payee’s account and their associated number of shares. It then checks the account is not the zero address, the shares are greater than zero, and that the account is already not registered as a payee. If all checks pass then we add the data to the respective variables.

Now let us add a function to enable the distribution of tokens to a payee.

release is a function that can be called by anyone and takes a parameter of one of the existing payee accounts. Lets break down what goes on in this function. First it checks that the account actually has shares allocated to it. Then it creates a variable called tokenTotalReceived, which adds the current token balance of the contract with the total amount of tokens previously released. Another variable called payment is created which determines how much in total tokens received is owed to the account and then subtracts however much has already been released to the account. A require statement then checks if the current payment amount is greater than zero (i.e., is the account owed any more tokens currently). If that check passes then _tokenReleased for the account is updated and the _totalTokenReleased is updated. Finally the token amount owed to the account is transferred.

Ok, now we have our functions in place! But there is still one more thing to do for this contract…. Events!

We are going to add two events to our contract, and it is good practice to add events to the top of your contract.

With these events in our contract, we will then emit them in the appropriate functions.

Now we have our token payment splitter contract is set up! To understand how this would work in a real world scenario, let’s create a mock pool contract that will import our token payment splitter.

Creating the Mock Pool contract

This contract will not be very complex, as we just want to demonstrate how to integrate the token payment splitter. You can image that this contract periodically receives a specific ERC20 token that we want to distribute to our list of payees. This ERC20 token could arrive via different scenarios, such as user deposits or redirected fees from another smart contract. In real life, depending on your project, you would probably have a much more intricate contract with more functionality to meet your use case.

In your contracts folder, create a new file called MockPool.sol, then add the following code.

In this contract, we import three things. First is the Ownable utility from OpenZeppelin to use an onlyOwner modifier on certain functions. Second is SafeERC20 to allow secure ERC20 token transfers as you’ll see in the contract. Third is our TokenPaymentSplitter contract.

In the MockPool constructor, we require the same three parameters for the TokenPaymentSplitter, and we just pass them through to our inherited contract.

I added another function to this contract, drainTo. It actually doesn’t have anything to do with the TokenPaymentSplitter contract. It is just a safety mechanism in the event that another ERC20 token, which is not set as the payment token, is sent to the pool, then there is a way for the contract owner to release that token.

Now that we have the our contacts in place, let’s test them out!

Testing the contracts

Testing smart contracts is just as important as creating them. These contracts deal with assets that often belong to others, so it is our responsibility as the developer to ensure they work as they should and that our tests can cover pretty much any edge case.

The tests that we will make here are examples to show that the TokenPaymentSplitter smart contract works as we expect. When working on your own project, you would want to create tests that fit specifically to your use case.

To support our tests, we want to include an ERC20 token, so for this we will create a new Solidity file that imports an OpenZepplin ERC20 template to be available for our tests. In your contracts folder, create a new file called Imports.sol, and include the following code:

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

Now for setting up our tests we will first create the necessary variables, create a beforeEach function, which is called before each test, and create an empty describe function that will soon contain our tests.

With those pieces in place, lets get into the meat of these tests!

Payment token is distributed evenly to multiple payees

In our first test, we want to see what happens when we deploy a contract that contains a list of payees with evenly distributed shares. The code for the test is below.

In this test, we deploy the contract with four payees, each with equal shares of ten. We then send 100000 units of a testPaymentToken to the contract and release payments to each of the payees. You’ll note in the test that each payee is calling the function to release tokens to themselves.

Payment token is distributed unevenly to multiple payees

In our second test, we want to make sure that the math still works even when the shares for each payee is unevenly distributed.

Looks like the payees still get paid out, but did you notice something??? There is still one unit of the payment token left in the contract! Since Solidity doesn’t have decimals, when it gets to the lowest unit it will typically round out and this can end up with contracts having dust, as we see here. No worries though, as we would expect there to be future inflows of payment tokens to the contract so it would continue to be distributed.

Payment token is distributed unevenly to multiple payees with additional payment token sent to pool

This is similar to the previous test, though with the addition of more payment tokens sent to the pool in between funds being released to payees. This is showing that with continual inflows of payment tokens to the Mock Pool contract, the math still works to ensure payees receive the right amounts.

Now with all the tests in place, it is time to run them and see if they work! In the project root folder, launch the tests with npx hardhat test. If everything is correct then you should see all green checks like the image below.

Congrats for getting this far!!!

As mentioned above, you’d want to do several more tests to ensure your overall project/protocol works as intended, with this Payment Splitter an integrated piece of it all. This would mean more unit tests to cover all available functions, and more sophisticated integration tests depending on your use case.

Final words

Payments are a common aspect to many crypto protocols, and there are several ways to tackle them. Today we learned about one way to manage payments, though you could even build upon this contract to fit your specific needs, such as enabling payments across multiple tokens, adding additional payees or removing payees, or distributing all payments simultaneously in one function call.

The project that we created today is also available on github. If you found this tutorial helpful then be sure to give this article a few claps and a star on the github repo.

Have questions or feedback? Leave a comment below! You can also see what else I’m up to on https://martinsoffice.com/

Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing

Also, Read


Create an ERC20 token payment splitting smart contract was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.