Skip to content

Latest commit

 

History

History
273 lines (265 loc) · 12.3 KB

Pull Payment V1.md

File metadata and controls

273 lines (265 loc) · 12.3 KB

PumaPay Pull Payment V1

Solidity Version: v0.5.8

Contract constructor

Sets the token address that the contract facilitates.

constructor (IERC20 _token)

Payable

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

function () external payable

Members

Owner

Our PumaPayPullPayment contract is PayableOwnable.

contract PumaPayPullPayment is PayableOwnable

Note: PayableOwnable is an extension of the Ownable smart contract where the owner is also a payable address allowing the owner to receive ETH from the smart contract. The owner (only one) of the smart contract is responsible for:

  1. Add executors function addExecutor(address _executor)
  2. 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 are 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 curstomer 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. Similarily to the owner, on registration/cancellation function the balance of the executor is checked and if is lower than 0.01 ETH 1 more ETH is sent from the smart contract to the executor to allow for registration/cancellation of 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 a 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 {
    bytes32 paymentID;                      /// ID of the payment
    bytes32 businessID;                     /// ID of the business
    string uniqueReferenceID;               /// unique reference ID the business is adding on the pull 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
    address treasuryAddress;                /// address which pma tokens will be transfer to on execution
}

Constants

uint256 constant private DECIMAL_FIXER = 10 ** 10;              /// 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 OVERFLOW_LIMITER_NUMBER = 10 ** 20;    /// 1e^20 - Prevent numeric overflows

uint256 constant private ONE_ETHER = 1 ether;                               /// PumaPay token has 18 decimals - same as one ETHER
uint256 constant private FUNDING_AMOUNT = 1 ether;                          /// Amount to transfer to owner/executor
uint256 constant private MINIMUM_AMOUNT_OF_ETH_FOR_OPERATORS = 0.15 ether; /// min amount of ETH for owner/executor

Public Functions - Owner

addExecutor()

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

function addExecutor(address payable _executor)
public
onlyOwner
isValidAddress(_executor)
executorDoesNotExists(_executor)
removeExecutor()

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

function removeExecutor(address payable _executor)
public
onlyOwner
isValidAddress(_executor)
executorExists(_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
onlyOwner

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 singed by the customer of the account. On pull payment registration, the first execution of the pull payment happens as well i.e. transfer of PMA from the customer to the business treasury wallet. This will happen based on the pull payment type. 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,
    bytes32[2] memory _ids, // [0] paymentID, [1] businessID
    address[3] memory _addresses, // [0] customer, [1] pull payment executor, [2] treasury
    string memory _currency,
    string memory _uniqueReferenceID,
    uint256 _initialPaymentAmountInCents,
    uint256 _fiatAmountInCents,
    uint256 _frequency,
    uint256 _numberOfPayments,
    uint256 _startTimestamp
)
public
isExecutor()
deletePullPayment()

Deletes a pull payment for a beneficiary. The deletion needs 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 singed by the customer 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,
    bytes32 _paymentID,
    address _customerAddress,
    address _pullPaymentExecutor
)
public
isExecutor()
paymentExists(_customerAddress, _pullPaymentExecutor)
paymentNotCancelled(_customerAddress, _pullPaymentExecutor)
isValidDeletionRequest(_paymentID, _customerAddress, _pullPaymentExecutor)

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 customer 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 customer account the amount in PMA. After execution we set the last payment timestamp to NOW and the initialPaymentAmountInCents to ZERO.
function executePullPayment(address _customer, bytes32 _paymentID)
public
paymentExists(_customer, msg.sender)
isValidPullPaymentExecutionRequest(_customer, msg.sender, _paymentID)

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. The execution of the pull payment will transfer PMA from the customer wallet to the business treasury wallet.

Internal Functions

isValidRegistration()

Checks if a registration request is valid by comparing the v, r, s params and the hashed params with the customer. address.

function isValidRegistration(
    uint8 v,
    bytes32 r,
    bytes32 s,
    address _customer,
    address _pullPaymentExecutor,
    PullPayment memory _pullPayment
)
internal
pure
returns (bool)
{
    return ecrecover(
        keccak256(
            abi.encodePacked(
                _pullPaymentExecutor,
                _pullPayment.paymentID,
                _pullPayment.businessID,
                _pullPayment.uniqueReferenceID,
                _pullPayment.treasuryAddress,
                _pullPayment.currency,
                _pullPayment.initialPaymentAmountInCents,
                _pullPayment.fiatAmountInCents,
                _pullPayment.frequency,
                _pullPayment.numberOfPayments,
                _pullPayment.startTimestamp
            )
        ),
        v, r, s) == _customer;
}

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 customer. address and the paymentID itself as well. The hashed parameters is the paymentID and the beneficiary (merchant) address.

function isValidDeletion(
    uint8 v,
    bytes32 r,
    bytes32 s,
    bytes32 _paymentID,
    address _customerAddress,
    address _pullPaymentExecutor
)
internal
view
returns (bool)
{
    return ecrecover(
        keccak256(
            abi.encodePacked(
                _paymentID,
                _pullPaymentExecutor
            )
        ), v, r, s) == _customerAddress
    && keccak256(
        abi.encodePacked(pullPayments[_customerAddress][_pullPaymentExecutor].paymentIds[0])
    ) == 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 - One ETH in wei
  2. DECIMAL_FIXER - Transforms the Rate from decimals to uint256 and is the same value that is being used for setting the rate
  3. 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:
function calculatePMAFromFiat(uint256 _fiatAmountInCents, string memory _currency)
internal
view
validConversionRate(_currency)
validAmount(_fiatAmountInCents)
returns (uint256) {
    return ONE_ETHER.mul(DECIMAL_FIXER).mul(_fiatAmountInCents).div(conversionRates[_currency]).div(FIAT_TO_CENT_FIXER);
}
doesPaymentExist()

Checks if a payment for a pull payment executor of a customer 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);
event LogExecutorRemoved(address executor);
event LogSetConversionRate(string currency, uint256 conversionRate);

event LogPaymentRegistered(
    address customerAddress,
    bytes32 paymentID,
    bytes32 businessID,
    string uniqueReferenceID
);
event LogPaymentCancelled(
    address customerAddress,
    bytes32 paymentID,
    bytes32 businessID,
    string uniqueReferenceID
);
event LogPullPaymentExecuted(
    address customerAddress,
    bytes32 paymentID,
    bytes32 businessID,
    string uniqueReferenceID
);