How to Build a Tipping Smart Contract Using Solidity

How to Build a Tipping Smart Contract Using Solidity

As a creative, it goes without saying that you’d like to be rewarded for your work. While there exists a plethora of platforms that you can use to receive monetary rewards from your fans, it goes without saying such platforms still collect a “middleman’s fee” from you! Well, what if there exists a solution where you can get all tips/payments/donations made to you 100%? Therein lies the power of Smart Contract Technology.

In this article, we will build a Tipping Contract that you can deploy on any EVM compatible chain. The contract will ensure the following:

  • You can receive tips/payments/donations in any ERC20 Tokens.

  • You can withdraw the Tokens from the contract to your Externally Owned Address (EOA) also known as your Wallet Address.

  • As the owner of the contract, you are the only one who can carry out the withdrawal function.

  • You can set the minimum tips/payment/donations amount.

The breakdown above has given an insight into the full functionality of the Smart Contract we are going to build. You can immediately get a sense of the various functions we will write, the modifiers, events that we will also want to be emitted, and the original constructor arguments we will deploy the contract with. To be certain that we are aligned in that regard, here are the functions:

  • ReceiveTips(): This is the function that users will call and send tips to the Smart Contract.

  • WithdrawTips(): This is the function that you will call to withdraw tips sent to the Smart Contract.

  • getContractBalance(): This is the function you can call to ascertain the amount of an ERC20 token balance in your Smart Contract.

  • UpdateWithdrawAddress(): This is the function where you can set a new address to withdraw tokens and send to.

With each of the functions, we can immediately understand the various events that we want to emit.

  • Receive

  • WithDraw

OK. It is important to note that your approach to building Dapps might be different. Some developers will rather jump straight into an IDE and start hacking away on their keyboards, but, as the popular saying goes: “Measure Twice, and cut once.”

The next step will be to open our Remix IDE and start to write the Smart Contract code:

As with every Smart Contract, we begin by declaring the License type used, and the solidity version using a pragma statement.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

This contract is set up so that creatives can receive any ERC20 Tokens, for this to be possible in the Smart Contract that we want to deploy, we have to create an interface that will enable us to interact with an already deploy an instance of an ERC20 token. To do this, we will be using the OpenZeppelin library. Import the library:

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

The IERC20 is the interface all ERC20 implementations should conform to. That is typically made in the name, decimal and token symbols convention. You can read the EIP20 to learn more. The SafeERC20 is a wrapper around the IERC20 interface that eliminates the need to handle boolean return values.

In this contract, we are going to ensure that only the owner of the contract can carry out certain functions such as withdrawal and changing the EOA of the owner. To do this, we will also import an OpenZeppelin library for access control.

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

Having completed the steps above, we can start writing the Smart Contract

contract TipMe {
...
}

The first step in writing the main contract is to declare an instance of the interface using the SafeERC20 wrapper, as we will use this for a tranferFrom() method call, and also the Interface for token we will deploy as an example in this lesson.

using SafeERC20 for IERC20;

Instantiate the ERC20 protocol using the interface from OpenZepellin:

IERC20 public token;

The token will be set in the constructor argument, with an already deployed token address parsed as an argument to the interface.

constructor (IERC20 _token) {
  token = _token;
}

In the constructor, we parse the token by data type. It is an ERC20 token and not an address. Its functions can be accessed via the interface IERC20, hence why its data type.

Now the contract looks like this:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

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

contract TipMe {

using SafeERC20 for IERC20;
IERC20 public token;

  constructor (IERC20 _token) {
      token = _token;
  }

}

We will write our functions, with the first function being the receiveTips function

function receiveTips(uint256 _amount) public payable returns(uint256) {
  require(_amount > 0, “Amount MUST be more than 0”);
  token.safeTransferFrom(address(msg.sender), address(this), _amount);
  return _amount;
}

In the receiveTips(), any EOA that holds an ERC20 can call the function and deposit any amount greater than zero. The deposited amount is held in the contract. It is important to illustrate that before any ERC20 token can be deposited by an address into a Smart Contract, it is crucial for the address to call approve() function on the token contract, with the Smart Contract as receiver before the Smart Contract can call safeTransferFrom().

Next step, we can then update the function to emit an event on a successful deposit.

First, we will create an emit event method. Add the following line after the constructor arguments

event TipReceived(address sender, uint256 amount);

Add the following line to the end of receiveTips() function just before the return statement:

emit TipReceived(msg.sender, _amount);

Whenever a tip is sent to the contract, it will emit the Receive event detailing the EOA that sent the tip, and the tip amount.

The next function is the withdrawTips function. The withdrawTips function is to enable the contract owner to withdraw all tips that they have received. This function has a unique property wherein only the contract owner should be able to make a withdrawal.

function withdrawTips(uint256 _amount) public onlyOwner returns(uint256) {
        require(_amount > 0, "Amount MUST > 0");
        token.approve(msg.sender, _amount);
        token.safeTransfer( msg.sender,  _amount);
        return _amount;
}

This function is different from the receiveTips function in the following ways:

  • Only the owner of the Smart Contract can call this function.

  • We can call the approve method for the token to carry out the transfer method. We do not have to go to the token contract to do this.

Other than that, it is not so much different as, you can set the amount to withdraw, use a require statement to ensure that said amount is greater than zero.

We will also add an event to log successful withdrawals. Add the following event:

event WithDrawalSuccessfull(uint256 amount);

Add the following line to the end of wthdrawTips() function just before the return statement:

emit WithDrawalSuccessfull(_amount);

The next function is the getContractBalance(), which you can call to ascertain the amount of an ERC20 token balance in your Smart Contract.

function getContractBalance() public view returns(uint){
       return IERC20(token).balanceOf(address(this));
 }

The last function required is where you can change the owner address to a different EOA. The OpenZeppelin ownable library already offers the opportunity for a user address to be set by default and offers a method called transferOwnership. We can write a new function called changeOwner which you can wrap a function around and make the call.

function changeOwner(address _owner) public returns(bool) {
    transferOwnership(_owner);
    return true;
}

Note that you can only call this function once, and then the owner address is changed.

Here is the full contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

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


contract Tip is  Ownable {

    using SafeERC20 for IERC20;
    IERC20 public token;

    constructor(IERC20 _token){
        token = _token;
    }


    event TipReceived(address sender, uint256 amount);
    event WithDrawalSuccessfull(uint256 amount);


    function receiveTips(uint256 _amount) public payable returns(uint256){
        require(_amount > 0, "Amount MUST > 0");
        token.safeTransferFrom(address(msg.sender), address(this), _amount);
        emit TipReceived(msg.sender, _amount);
        return _amount;
    }

    function withdrawTips(uint256 _amount) public onlyOwner returns(uint256){
        require(_amount > 0, "Amount MUST > 0");
        token.approve(msg.sender, _amount);
        token.safeTransfer( msg.sender,  _amount);
        emit WithDrawalSuccessfull(_amount);
        return _amount;
    }

    function getTokenBalance() public view returns(uint){
        return IERC20(token).balanceOf(address(this));
    }

    function changeOwner(address _owner) public returns(bool) {
        transferOwnership(_owner);
        return true;
    }

}

In the next article, we will build a front end to interact with the contract.