PumaPay PullPayment Protocol and Smart Contracts: A Closer Look

We have just released v2.0 of our solution which includes Ethereum-based single and recurring pull payments. If you are intrigued and you want to see our smart contracts in detail, then this article is for you. PumaPay’s solution is an open-source protocol which can be easily integrated with any kind of merchant platform. This article is a technical deep-dive to the smart contract dictating the PumaPay pull payment protocol. 

Development 

The smart contracts have been written in Solidity and are tested using Truffle and Ganache-cli. They are all based on modules developed by Open Zepellin. 

PumaPay Token 

The PumaPay token is based on the ERC-20 Token standard developed in Solidity and deployed on the Ethereum network on our TGE which occurred on May 7th, 2018. 

Usage 

The address from which the contract is deployed will be set as the owner address. Only the owner can call the methods mint() and finishMinting().
Minting is cumulative. Calling this method twice for the same address (with minting value greater than zero) will result in an increase of that address balance.
The tokens are not transferable until the owner invokes finishMinting().
Once finishMinting() was invoked, it can’t be reversed, i.e. no new tokens can be minted.
ETH Address: 0x846c66cf71c43f80403b51fe3906b3599d63336f
Total Amount of Tokens: 78,042,956,829 PMA – Circulating supply: 15,942,078,847 PMA 

PullPayment Contract 

One of the innovative features of the protocol is that it allows users to pull funds from other wallets instead of one user pushing funds to another user’s wallet. In this respect, PumaPay reverses crypto-payment mechanisms making it easier for the merchant to initiate the “pull transaction”, by first getting authorization from the customer and then pulling the necessary funds from the customer’s wallet, without them bearing the weight of any transaction fees.  

Contract constructor 

Sets the token address that the contract facilitates. 

constructor (PumaPayToken _token)

Payable 

Allows the PumaPayPullPayment contract to receive ETH to facilitate the funding of owner/executors. 

function () external payable 

Members 

Owner 

Our PumaPayPullPayment contract is ownable. 

contract PumaPayPullPayment is Ownable 

The owner (only one) of the smart contract is responsible for: 

  1. Setting the PMA/Fiat rates function setRate(string _currency, uint256 _rate) 
  1. Add executors function addExecutor(address _executor) 
  1. Remove executor function removeExecutor(address _executor)
    On each function related with setting the rate or adding/removing executors the balance of the owner is checked and if the balance is lower than 0.01 ETH then 1 more ETH is sent to the owner address in order to pay for the gas fees related with those transactions.
    The owner is an address owned by the association governing the smart contract. 
if (isFundingNeeded(owner)) {    owner.transfer(1 ether);} 

Executors 

The PumaPayPullPayment contract can have multiple executors. Each executor is allowed to register or cancel a pull payment on behalf of a customer. The customer should sign the pull payment details using keccak256 through the wallet and on registration/cancellation the signature parameters (v, r, s) of the signed pull payment are used to verify that the customer address was indeed the one which requested the registration/cancellation. On registration/cancellation the balance of the executor is checked and, if it is lower than 0.01 ETH, 1 more ETH is sent from the smart contract to the executor to allow for the registration/cancellation of the pull payments.
The executor(s) is an address owned by the association governing the smart contract. 

mapping (address => bool) public executors; 

PullPayment 

The PumaPayPullPayment contract consists of two addresses for the PullPayments. The first address is the customer address and the second one is the merchant address. 

mapping (address => mapping (address => PullPayment)) public pullPayments; 

The PullPayment struct is the following: 

struct PullPayment { 
     string merchantID; /// ID of the merchant 
     string paymentID; /// ID of the payment 
     string currency; /// 3-letter abbr i.e. 'EUR' / 'USD' etc. 
     uint256 initialPaymentAmountInCents; /// initial payment amount in fiat in cents 
     uint256 fiatAmountInCents; /// payment amount in fiat in cents 
     uint256 frequency; /// how often merchant can pull - in seconds
     uint256 numberOfPayments; /// amount of pull payments merchant can make 
     uint256 startTimestamp; /// when subscription starts - in seconds 
     uint256 nextPaymentTimestamp; /// timestamp of next payment 
     uint256 lastPaymentTimestamp; /// timestamp of last payment 
     uint256 cancelTimestamp; /// timestamp the payment was cancelled 
} 

Constants 

uint256 constant private DECIMAL_FIXER = 10000000000; // 1e^10 - This transforms the Rate from decimals to uint256
uint256 constant private FIAT_TO_CENT_FIXER = 100;    // Fiat currencies have 100 cents in 1 basic monetary unit.
uint256 constant private ONE_ETHER = 1 ether;         // PumaPay token has 18 decimals - same as one ETHER
uint256 constant private MINIMUM_AMOUN_OF_ETH_FOR_OPARATORS = 0.01 ether; // minimum amount of ETHER the owner/executor should have

addExecutor() 

Adds an existing executor. It can be executed only by the onwer.
The balance of the owner is checked and if funding is needed 1 ETH is transferred. 

function addExecutor(address _executor) 

removeExecutor() 

Removes an existing executor. It can be executed only by the onwer.
The balance of the owner is checked and if funding is needed 1 ETH is transferred. 

function removeExecutor(address _executor) 

setRate() 

Sets the exchange rate for a currency. It can be executed only by the onwer.
The balance of the owner is checked and if funding is needed 1 ETH is transferred. 

function setRate(string _currency, uint256 _rate) 

Public Functions – Executor 

registerPullPayment() 

Registers a new pull payment to the PumaPay Pull Payment Contract. The registration can be executed only by one of the executors of the PumaPay Pull Payment Contract and the PumaPay Pull Payment Contract checks that the pull payment has been signed by the client of the account. The balance of the executor (msg.sender) is checked and if funding is needed 1 ETH is transferred. 

function registerPullPayment (
     uint8 v,        
     bytes32 r,        
     bytes32 s,        
     string _merchantID,        
     string _paymentID,        
     address _client,        
     address _beneficiary,        
     string _currency,        
     uint256 _initialPaymentAmountInCents,        
     uint256 _fiatAmountInCents,        
     uint256 _frequency,        
     uint256 _numberOfPayments,        
     uint256 _startTimestamp    
) 

deletePullPayment() 

Deletes a pull payment for a beneficiary. The deletion can be executed only by one of the executors of the PumaPay Pull Payment Contract and the PumaPay Pull Payment Contract checks that the beneficiary and the paymentID have been signed by the client of the account. This method sets the cancellation of the pull payment in the pull payments array for this beneficiary specified. The balance of the executor (msg.sender) is checked and if funding is needed, 1 ETH is transferred. 

function deletePullPayment (
     uint8 v,        
     bytes32 r,        
     bytes32 s,        
     string _paymentID,        
     address _client,        
     address _beneficiary    
) 

Public Functions 

executePullPayment() 

Executes a pull payment for the address that is calling the function msg.sender. The pull payment should exist and the payment request should be valid in terms of when it can be executed. 

  • Use Case 1: Single/Recurring Fixed Pull Payment (initialPaymentAmountInCents == 0) We calculate the amount in PMA using the rate for the currency specified in the pull payment and the fiatAmountInCents and we transfer from the client account the amount in PMA. After execution we set the last payment timestamp to NOW, the next payment timestamp is incremented by the frequency and the number of payments is decresed by 1. 
  • Use Case 2: Recurring Fixed Pull Payment with initial fee (initialPaymentAmountInCents > 0) We calculate the amount in PMA using the rate for the currency specified in the pull payment and the ‘initialPaymentAmountInCents’ and we transfer from the client account the amount in PMA. After execution we set the last payment timestamp to NOW and the initialPaymentAmountInCents to ZERO. 

Internal Functions 

isValidRegistration() 

Checks if a registration request is valid by comparing the v, r, s params and the hashed params with the client. address. The hashed parameters is the beneficiary (merchant) address, the currency, the initialPaymentAmountInCents, fiatAmountInCents, frequency, numberOfPayments and startTimestamp. 

ecrecover(    
     keccak256(        
          abi.encodePacked(            
          _beneficiary,            
          _pullPayment.currency,            
          _pullPayment.initialPaymentAmountInCents,            
          _pullPayment.fiatAmountInCents,            
          _pullPayment.frequency,            
          _pullPayment.numberOfPayments,            
          _pullPayment.startTimestamp        
)), v, r, s) == _client; 

More about recovery ID of an ETH signature and ECDSA signatures can be found here. 


isValidDeletion() 

Checks if a deletion request is valid by comparing the v, r, s params and the hashed params with the client. address and the paymentID itself as well. The hashed parameters is the paymentID and the beneficiary (merchant) address. 

ecrecover(    
     keccak256(        
           abi.encodePacked(            
           _paymentID,            
           _beneficiary        
)), v, r, s) == _client && 
keccak256(abi.encodePacked(pullPayments[_client][_beneficiary].paymentID)) == keccak256(abi.encodePacked(_paymentID)); 

calculatePMAFromFiat() 

Calculates the PMA Rate for the fiat currency specified. The rate is set every 10 minutes by our PMA server for the currencies specified in the smart contract. Two helpers/fixers are used for this calculation: 

  1. ONE_ETHER – 1e18 to facilitate for the value of one ETH in WEI 
  1. DECIMAL_FIXER – Transforms the Rate from decimals to uint256 and is the same value that is being used for setting the rate 
  1. FIAT_TO_CENT_FIXER – The payment amounts that are being used in the smart contract are noted in CENTS since decimals are not supported in solidity yet. Calculation: 

doesPaymentExist() 

Checks if a payment for a beneficiary (merchant) of a client exists. 


isFundingNeeded() 

Checks if the address of the owner or an executor needs to be funded. The minimum amount the owner/executors should always have is 0.01 ETH 


Events 

event LogExecutorAdded(address executor);       // When adding a new executor
event LogExecutorRemoved(address executor);     // When removing an existing executor
event LogPaymentRegistered(address clientAddress, address beneficiaryAddress, string paymentID);    // When registering a new pull payment
event LogPaymentCancelled(address clientAddress, address beneficiaryAddress, string paymentID);     // When removing a new pull payment
event LogPullPaymentExecuted(address clientAddress, address beneficiaryAddress, string paymentID);  // When executing a pull payment
event LogSetExchangeRate(string currency, uint256 exchangeRate);        // When updating the PMA/FIAT rates 
0.00 avg. rating (0% score) - 0 votes

    No Comment.