/** * Source Code first verified at https://etherscan.io on Friday, May 3, 2019 (UTC) */ // File: openzeppelin-solidity/contracts/token/ERC20/IERC20.sol pragma solidity 0.5.0; /** * @title ERC20 interface * @dev see https://github.com/ethereum/EIPs/issues/20 */ interface IERC20 { function transfer(address to, uint256 value) external returns (bool); function approve(address spender, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); function totalSupply() external view returns (uint256); function balanceOf(address who) external view returns (uint256); function allowance(address owner, address spender) external view returns (uint256); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } // File: openzeppelin-solidity/contracts/math/SafeMath.sol pragma solidity 0.5.0; /** * @title SafeMath * @dev Unsigned math operations with safety checks that revert on error */ library SafeMath { /** * @dev Multiplies two unsigned integers, reverts on overflow. */ function mul(uint256 a, uint256 b) internal pure returns (uint256) { // Gas optimization: this is cheaper than requiring 'a' not being zero, but the // benefit is lost if 'b' is also tested. // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 if (a == 0) { return 0; } uint256 c = a * b; require(c / a == b); return c; } /** * @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero. */ function div(uint256 a, uint256 b) internal pure returns (uint256) { // Solidity only automatically asserts when dividing by 0 require(b > 0); uint256 c = a / b; // assert(a == b * c + a % b); // There is no case in which this doesn't hold return c; } /** * @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend). */ function sub(uint256 a, uint256 b) internal pure returns (uint256) { require(b <= a); uint256 c = a - b; return c; } /** * @dev Adds two unsigned integers, reverts on overflow. */ function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a); return c; } /** * @dev Divides two unsigned integers and returns the remainder (unsigned integer modulo), * reverts when dividing by zero. */ function mod(uint256 a, uint256 b) internal pure returns (uint256) { require(b != 0); return a % b; } } // File: openzeppelin-solidity/contracts/token/ERC20/ERC20.sol pragma solidity 0.5.0; /** * @title Standard ERC20 token * * @dev Implementation of the basic standard token. * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md * Originally based on code by FirstBlood: * https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol * * This implementation emits additional Approval events, allowing applications to reconstruct the allowance status for * all accounts just by listening to said events. Note that this isn't required by the specification, and other * compliant implementations may not do it. */ contract ERC20 is IERC20 { using SafeMath for uint256; mapping (address => uint256) private _balances; mapping (address => mapping (address => uint256)) private _allowed; uint256 private _totalSupply; /** * @dev Total number of tokens in existence */ function totalSupply() public view returns (uint256) { return _totalSupply; } /** * @dev Gets the balance of the specified address. * @param owner The address to query the balance of. * @return An uint256 representing the amount owned by the passed address. */ function balanceOf(address owner) public view returns (uint256) { return _balances[owner]; } /** * @dev Function to check the amount of tokens that an owner allowed to a spender. * @param owner address The address which owns the funds. * @param spender address The address which will spend the funds. * @return A uint256 specifying the amount of tokens still available for the spender. */ function allowance(address owner, address spender) public view returns (uint256) { return _allowed[owner][spender]; } /** * @dev Transfer token for a specified address * @param to The address to transfer to. * @param value The amount to be transferred. */ function transfer(address to, uint256 value) public returns (bool) { _transfer(msg.sender, to, value); return true; } /** * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. * Beware that changing an allowance with this method brings the risk that someone may use both the old * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 * @param spender The address which will spend the funds. * @param value The amount of tokens to be spent. */ function approve(address spender, uint256 value) public returns (bool) { require(spender != address(0)); _allowed[msg.sender][spender] = value; emit Approval(msg.sender, spender, value); return true; } /** * @dev Transfer tokens from one address to another. * Note that while this function emits an Approval event, this is not required as per the specification, * and other compliant implementations may not emit the event. * @param from address The address which you want to send tokens from * @param to address The address which you want to transfer to * @param value uint256 the amount of tokens to be transferred */ function transferFrom(address from, address to, uint256 value) public returns (bool) { _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value); _transfer(from, to, value); emit Approval(from, msg.sender, _allowed[from][msg.sender]); return true; } /** * @dev Increase the amount of tokens that an owner allowed to a spender. * approve should be called when allowed_[_spender] == 0. To increment * allowed value is better to use this function to avoid 2 calls (and wait until * the first transaction is mined) * From MonolithDAO Token.sol * Emits an Approval event. * @param spender The address which will spend the funds. * @param addedValue The amount of tokens to increase the allowance by. */ function increaseAllowance(address spender, uint256 addedValue) public returns (bool) { require(spender != address(0)); _allowed[msg.sender][spender] = _allowed[msg.sender][spender].add(addedValue); emit Approval(msg.sender, spender, _allowed[msg.sender][spender]); return true; } /** * @dev Decrease the amount of tokens that an owner allowed to a spender. * approve should be called when allowed_[_spender] == 0. To decrement * allowed value is better to use this function to avoid 2 calls (and wait until * the first transaction is mined) * From MonolithDAO Token.sol * Emits an Approval event. * @param spender The address which will spend the funds. * @param subtractedValue The amount of tokens to decrease the allowance by. */ function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) { require(spender != address(0)); _allowed[msg.sender][spender] = _allowed[msg.sender][spender].sub(subtractedValue); emit Approval(msg.sender, spender, _allowed[msg.sender][spender]); return true; } /** * @dev Transfer token for a specified addresses * @param from The address to transfer from. * @param to The address to transfer to. * @param value The amount to be transferred. */ function _transfer(address from, address to, uint256 value) internal { require(to != address(0)); _balances[from] = _balances[from].sub(value); _balances[to] = _balances[to].add(value); emit Transfer(from, to, value); } /** * @dev Internal function that mints an amount of the token and assigns it to * an account. This encapsulates the modification of balances such that the * proper events are emitted. * @param account The account that will receive the created tokens. * @param value The amount that will be created. */ function _mint(address account, uint256 value) internal { require(account != address(0)); _totalSupply = _totalSupply.add(value); _balances[account] = _balances[account].add(value); emit Transfer(address(0), account, value); } /** * @dev Internal function that burns an amount of the token of a given * account. * @param account The account whose tokens will be burnt. * @param value The amount that will be burnt. */ function _burn(address account, uint256 value) internal { require(account != address(0)); _totalSupply = _totalSupply.sub(value); _balances[account] = _balances[account].sub(value); emit Transfer(account, address(0), value); } /** * @dev Internal function that burns an amount of the token of a given * account, deducting from the sender's allowance for said account. Uses the * internal burn function. * Emits an Approval event (reflecting the reduced allowance). * @param account The account whose tokens will be burnt. * @param value The amount that will be burnt. */ function _burnFrom(address account, uint256 value) internal { _allowed[account][msg.sender] = _allowed[account][msg.sender].sub(value); _burn(account, value); emit Approval(account, msg.sender, _allowed[account][msg.sender]); } } // File: openzeppelin-solidity/contracts/access/Roles.sol pragma solidity 0.5.0; /** * @title Roles * @dev Library for managing addresses assigned to a Role. */ library Roles { struct Role { mapping (address => bool) bearer; } /** * @dev give an account access to this role */ function add(Role storage role, address account) internal { require(account != address(0)); require(!has(role, account)); role.bearer[account] = true; } /** * @dev remove an account's access to this role */ function remove(Role storage role, address account) internal { require(account != address(0)); require(has(role, account)); role.bearer[account] = false; } /** * @dev check if an account has this role * @return bool */ function has(Role storage role, address account) internal view returns (bool) { require(account != address(0)); return role.bearer[account]; } } // File: openzeppelin-solidity/contracts/access/roles/MinterRole.sol pragma solidity 0.5.0; contract MinterRole { using Roles for Roles.Role; event MinterAdded(address indexed account); event MinterRemoved(address indexed account); Roles.Role private _minters; constructor () internal { _addMinter(msg.sender); } modifier onlyMinter() { require(isMinter(msg.sender)); _; } function isMinter(address account) public view returns (bool) { return _minters.has(account); } function addMinter(address account) public onlyMinter { _addMinter(account); } function renounceMinter() public { _removeMinter(msg.sender); } function _addMinter(address account) internal { _minters.add(account); emit MinterAdded(account); } function _removeMinter(address account) internal { _minters.remove(account); emit MinterRemoved(account); } } // File: openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol pragma solidity 0.5.0; /** * @title ERC20Mintable * @dev ERC20 minting logic */ contract ERC20Mintable is ERC20, MinterRole { /** * @dev Function to mint tokens * @param to The address that will receive the minted tokens. * @param value The amount of tokens to mint. * @return A boolean that indicates if the operation was successful. */ function mint(address to, uint256 value) public onlyMinter returns (bool) { _mint(to, value); return true; } } // File: contracts/PumaPayToken.sol pragma solidity 0.5.0; /// PumaPayToken inherits from MintableToken, which in turn inherits from StandardToken. /// Super is used to bypass the original function signature and include the whenNotMinting modifier. contract PumaPayToken is ERC20Mintable { string public name = "PumaPay"; string public symbol = "PMA"; uint8 public decimals = 18; constructor() public { } /// @dev transfer token for a specified address /// @param _to address The address to transfer to. /// @param _value uint256 The amount to be transferred. /// @return success bool Calling super.transfer and returns true if successful. function transfer(address _to, uint256 _value) public returns (bool) { return super.transfer(_to, _value); } /// @dev Transfer tokens from one address to another. /// @param _from address The address which you want to send tokens from. /// @param _to address The address which you want to transfer to. /// @param _value uint256 the amount of tokens to be transferred. /// @return success bool Calling super.transferFrom and returns true if successful. function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { return super.transferFrom(_from, _to, _value); } } // File: contracts/ownership/PayableOwnable.sol pragma solidity 0.5.0; /** * @title PayableOwnable * @dev The PayableOwnable contract has an owner address, and provides basic authorization control * functions, this simplifies the implementation of "user permissions". * PayableOwnable is extended from open-zeppelin Ownable smart contract, with the difference of making the owner * a payable address. */ contract PayableOwnable { address payable private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev The Ownable constructor sets the original `owner` of the contract to the sender * account. */ constructor () internal { _owner = msg.sender; emit OwnershipTransferred(address(0), _owner); } /** * @return the address of the owner. */ function owner() public view returns (address payable) { return _owner; } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(isOwner()); _; } /** * @return true if `msg.sender` is the owner of the contract. */ function isOwner() public view returns (bool) { return msg.sender == _owner; } /** * @dev Allows the current owner to relinquish control of the contract. * @notice Renouncing to ownership will leave the contract without an owner. * It will not be possible to call the functions with the `onlyOwner` * modifier anymore. */ function renounceOwnership() public onlyOwner { emit OwnershipTransferred(_owner, address(0)); _owner = address(0); } /** * @dev Allows the current owner to transfer control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function transferOwnership(address payable newOwner) public onlyOwner { _transferOwnership(newOwner); } /** * @dev Transfers control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function _transferOwnership(address payable newOwner) internal { require(newOwner != address(0)); emit OwnershipTransferred(_owner, newOwner); _owner = newOwner; } } // File: contracts/PumaPayPullPayment.sol pragma solidity 0.5.0; /// @title PumaPay Pull Payment - Contract that facilitates our pull payment protocol /// @author PumaPay Dev Team - <[email protected]> contract PumaPayPullPayment is PayableOwnable { using SafeMath for uint256; /// =============================================================================================================== /// Events /// =============================================================================================================== event LogExecutorAdded(address executor); event LogExecutorRemoved(address executor); event LogSetConversionRate(string currency, uint256 conversionRate); event LogPaymentRegistered( address customerAddress, bytes32 paymentID, bytes32 businessID, bytes32 uniqueReferenceID ); event LogPaymentCancelled( address customerAddress, bytes32 paymentID, bytes32 businessID, bytes32 uniqueReferenceID ); event LogPullPaymentExecuted( address customerAddress, bytes32 paymentID, bytes32 businessID, bytes32 uniqueReferenceID ); /// =============================================================================================================== /// 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 /// =============================================================================================================== /// Members /// =============================================================================================================== PumaPayToken public token; mapping(string => uint256) private conversionRates; mapping(address => bool) public executors; mapping(address => mapping(address => PullPayment)) public pullPayments; struct PullPayment { bytes32 paymentID; /// ID of the payment bytes32 businessID; /// ID of the business bytes32 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 } /// =============================================================================================================== /// Modifiers /// =============================================================================================================== modifier isExecutor() { require(executors[msg.sender], "msg.sender not an executor"); _; } modifier executorExists(address _executor) { require(executors[_executor], "Executor does not exists."); _; } modifier executorDoesNotExists(address _executor) { require(!executors[_executor], "Executor already exists."); _; } modifier paymentExists(address _client, address _pullPaymentExecutor) { require(doesPaymentExist(_client, _pullPaymentExecutor), "Pull Payment does not exists"); _; } modifier paymentNotCancelled(address _client, address _pullPaymentExecutor) { require(pullPayments[_client][_pullPaymentExecutor].cancelTimestamp == 0, "Pull Payment is cancelled."); _; } modifier isValidPullPaymentExecutionRequest(address _client, address _pullPaymentExecutor, bytes32 _paymentID) { require( (pullPayments[_client][_pullPaymentExecutor].initialPaymentAmountInCents > 0 || (now >= pullPayments[_client][_pullPaymentExecutor].startTimestamp && now >= pullPayments[_client][_pullPaymentExecutor].nextPaymentTimestamp) ), "Invalid pull payment execution request - Time of execution is invalid." ); require(pullPayments[_client][_pullPaymentExecutor].numberOfPayments > 0, "Invalid pull payment execution request - Number of payments is zero."); require((pullPayments[_client][_pullPaymentExecutor].cancelTimestamp == 0 || pullPayments[_client][_pullPaymentExecutor].cancelTimestamp > pullPayments[_client][_pullPaymentExecutor].nextPaymentTimestamp), "Invalid pull payment execution request - Pull payment is cancelled"); require(keccak256( abi.encodePacked(pullPayments[_client][_pullPaymentExecutor].paymentID) ) == keccak256(abi.encodePacked(_paymentID)), "Invalid pull payment execution request - Payment ID not matching."); _; } modifier isValidDeletionRequest(bytes32 _paymentID, address _client, address _pullPaymentExecutor) { require(_client != address(0), "Invalid deletion request - Client address is ZERO_ADDRESS."); require(_pullPaymentExecutor != address(0), "Invalid deletion request - Beneficiary address is ZERO_ADDRESS."); require(_paymentID.length != 0, "Invalid deletion request - Payment ID is empty."); _; } modifier isValidAddress(address _address) { require(_address != address(0), "Invalid address - ZERO_ADDRESS provided"); _; } modifier validConversionRate(string memory _currency) { require(bytes(_currency).length != 0, "Invalid conversion rate - Currency is empty."); require(conversionRates[_currency] > 0, "Invalid conversion rate - Must be higher than zero."); _; } modifier validAmount(uint256 _fiatAmountInCents) { require(_fiatAmountInCents > 0, "Invalid amount - Must be higher than zero"); _; } /// =============================================================================================================== /// Constructor /// =============================================================================================================== /// @dev Contract constructor - sets the token address that the contract facilitates. /// @param _token Token Address. constructor (address _token) public { require(_token != address(0), "Invalid address for token - ZERO_ADDRESS provided"); token = PumaPayToken(_token); } // @notice Will receive any eth sent to the contract function() external payable { } /// =============================================================================================================== /// Public Functions - Owner Only /// =============================================================================================================== /// @dev Adds a new executor. - can be executed only by the onwer. /// When adding a new executor 1 ETH is tranferred to allow the executor to pay for gas. /// The balance of the owner is also checked and if funding is needed 1 ETH is transferred. /// @param _executor - address of the executor which cannot be zero address. function addExecutor(address payable _executor) public onlyOwner isValidAddress(_executor) executorDoesNotExists(_executor) { _executor.transfer(FUNDING_AMOUNT); executors[_executor] = true; if (isFundingNeeded(owner())) { owner().transfer(FUNDING_AMOUNT); } emit LogExecutorAdded(_executor); } /// @dev Removes a new executor. - can be executed only by the onwer. /// The balance of the owner is checked and if funding is needed 1 ETH is transferred. /// @param _executor - address of the executor which cannot be zero address. function removeExecutor(address payable _executor) public onlyOwner isValidAddress(_executor) executorExists(_executor) { executors[_executor] = false; if (isFundingNeeded(owner())) { owner().transfer(FUNDING_AMOUNT); } emit LogExecutorRemoved(_executor); } /// @dev Sets the exchange rate for a currency. - can be executed only by the onwer. /// Emits 'LogSetConversionRate' with the currency and the updated rate. /// The balance of the owner is checked and if funding is needed 1 ETH is transferred. /// @param _currency - address of the executor which cannot be zero address /// @param _rate - address of the executor which cannot be zero address function setRate(string memory _currency, uint256 _rate) public onlyOwner returns (bool) { conversionRates[_currency] = _rate; emit LogSetConversionRate(_currency, _rate); if (isFundingNeeded(owner())) { owner().transfer(FUNDING_AMOUNT); } return true; } /// =============================================================================================================== /// Public Functions - Executors Only /// =============================================================================================================== /// @dev 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 client of the account. /// The balance of the executor (msg.sender) is checked and if funding is needed 1 ETH is transferred. /// Emits 'LogPaymentRegistered' with client address, beneficiary address and paymentID. /// @param v - recovery ID of the ETH signature. - https://github.com/ethereum/EIPs/issues/155 /// @param r - R output of ECDSA signature. /// @param s - S output of ECDSA signature. /// @param _ids - all the relevant IDs for the payment. /// @param _addresses - all the relevant addresses for the payment. /// @param _currency - currency of the payment / 3-letter abbr i.e. 'EUR'. /// @param _fiatAmountInCents - payment amount in fiat in cents. /// @param _frequency - how often merchant can pull - in seconds. /// @param _numberOfPayments - amount of pull payments merchant can make /// @param _startTimestamp - when subscription starts - in seconds. function registerPullPayment( uint8 v, bytes32 r, bytes32 s, bytes32[3] memory _ids, // 0 paymentID, 1 businessID, 2 uniqueReferenceID address[3] memory _addresses, // 0 customer, 1 pull payment executor, 2 treasury string memory _currency, uint256 _initialPaymentAmountInCents, uint256 _fiatAmountInCents, uint256 _frequency, uint256 _numberOfPayments, uint256 _startTimestamp ) public isExecutor() { require(_ids[0].length > 0, "Payment ID is empty."); require(_ids[1].length > 0, "Business ID is empty."); require(_ids[2].length > 0, "Unique Reference ID is empty."); require(bytes(_currency).length > 0, "Currency is empty"); require(_addresses[0] != address(0), "Customer Address is ZERO_ADDRESS."); require(_addresses[1] != address(0), "Beneficiary Address is ZERO_ADDRESS."); require(_addresses[2] != address(0), "Treasury Address is ZERO_ADDRESS."); require(_fiatAmountInCents > 0, "Payment amount in fiat is zero."); require(_frequency > 0, "Payment frequency is zero."); require(_frequency < OVERFLOW_LIMITER_NUMBER, "Payment frequency is higher thant the overflow limit."); require(_numberOfPayments > 0, "Payment number of payments is zero."); require(_numberOfPayments < OVERFLOW_LIMITER_NUMBER, "Payment number of payments is higher thant the overflow limit."); require(_startTimestamp > 0, "Payment start time is zero."); require(_startTimestamp < OVERFLOW_LIMITER_NUMBER, "Payment start time is higher thant the overflow limit."); pullPayments[_addresses[0]][_addresses[1]].currency = _currency; pullPayments[_addresses[0]][_addresses[1]].initialPaymentAmountInCents = _initialPaymentAmountInCents; pullPayments[_addresses[0]][_addresses[1]].fiatAmountInCents = _fiatAmountInCents; pullPayments[_addresses[0]][_addresses[1]].frequency = _frequency; pullPayments[_addresses[0]][_addresses[1]].startTimestamp = _startTimestamp; pullPayments[_addresses[0]][_addresses[1]].numberOfPayments = _numberOfPayments; pullPayments[_addresses[0]][_addresses[1]].paymentID = _ids[0]; pullPayments[_addresses[0]][_addresses[1]].businessID = _ids[1]; pullPayments[_addresses[0]][_addresses[1]].uniqueReferenceID = _ids[2]; pullPayments[_addresses[0]][_addresses[1]].treasuryAddress = _addresses[2]; require(isValidRegistration( v, r, s, _addresses[0], _addresses[1], pullPayments[_addresses[0]][_addresses[1]]), "Invalid pull payment registration - ECRECOVER_FAILED" ); pullPayments[_addresses[0]][_addresses[1]].nextPaymentTimestamp = _startTimestamp; pullPayments[_addresses[0]][_addresses[1]].lastPaymentTimestamp = 0; pullPayments[_addresses[0]][_addresses[1]].cancelTimestamp = 0; if (isFundingNeeded(msg.sender)) { msg.sender.transfer(FUNDING_AMOUNT); } emit LogPaymentRegistered(_addresses[0], _ids[0], _ids[1], _ids[2]); } /// @dev 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 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. /// Emits 'LogPaymentCancelled' with beneficiary address and paymentID. /// @param v - recovery ID of the ETH signature. - https://github.com/ethereum/EIPs/issues/155 /// @param r - R output of ECDSA signature. /// @param s - S output of ECDSA signature. /// @param _paymentID - ID of the payment. /// @param _client - client address that is linked to this pull payment. /// @param _pullPaymentExecutor - address that is allowed to execute this pull payment. function deletePullPayment( uint8 v, bytes32 r, bytes32 s, bytes32 _paymentID, address _client, address _pullPaymentExecutor ) public isExecutor() paymentExists(_client, _pullPaymentExecutor) paymentNotCancelled(_client, _pullPaymentExecutor) isValidDeletionRequest(_paymentID, _client, _pullPaymentExecutor) { require(isValidDeletion(v, r, s, _paymentID, _client, _pullPaymentExecutor), "Invalid deletion - ECRECOVER_FAILED."); pullPayments[_client][_pullPaymentExecutor].cancelTimestamp = now; if (isFundingNeeded(msg.sender)) { msg.sender.transfer(FUNDING_AMOUNT); } emit LogPaymentCancelled( _client, _paymentID, pullPayments[_client][_pullPaymentExecutor].businessID, pullPayments[_client][_pullPaymentExecutor].uniqueReferenceID ); } /// =============================================================================================================== /// Public Functions /// =============================================================================================================== /// @dev Executes a pull payment for the msg.sender - The pull payment should exist and the payment request /// should be valid in terms of when it can be executed. /// Emits 'LogPullPaymentExecuted' with client address, msg.sender as the beneficiary address and the paymentID. /// 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 decreased 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. /// @param _client - address of the client from which the msg.sender requires to pull funds. /// @param _paymentID - ID of the payment. function executePullPayment(address _client, bytes32 _paymentID) public paymentExists(_client, msg.sender) isValidPullPaymentExecutionRequest(_client, msg.sender, _paymentID) { uint256 amountInPMA; if (pullPayments[_client][msg.sender].initialPaymentAmountInCents > 0) { amountInPMA = calculatePMAFromFiat( pullPayments[_client][msg.sender].initialPaymentAmountInCents, pullPayments[_client][msg.sender].currency ); pullPayments[_client][msg.sender].initialPaymentAmountInCents = 0; } else { amountInPMA = calculatePMAFromFiat( pullPayments[_client][msg.sender].fiatAmountInCents, pullPayments[_client][msg.sender].currency ); pullPayments[_client][msg.sender].nextPaymentTimestamp = pullPayments[_client][msg.sender].nextPaymentTimestamp + pullPayments[_client][msg.sender].frequency; pullPayments[_client][msg.sender].numberOfPayments = pullPayments[_client][msg.sender].numberOfPayments - 1; } pullPayments[_client][msg.sender].lastPaymentTimestamp = now; token.transferFrom( _client, pullPayments[_client][msg.sender].treasuryAddress, amountInPMA ); emit LogPullPaymentExecuted( _client, pullPayments[_client][msg.sender].paymentID, pullPayments[_client][msg.sender].businessID, pullPayments[_client][msg.sender].uniqueReferenceID ); } function getRate(string memory _currency) public view returns (uint256) { return conversionRates[_currency]; } /// =============================================================================================================== /// Internal Functions /// =============================================================================================================== /// @dev 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. /// @param _fiatAmountInCents - payment amount in fiat CENTS so that is always integer /// @param _currency - currency in which the payment needs to take place /// RATE CALCULATION EXAMPLE /// ------------------------ /// RATE ==> 1 PMA = 0.01 USD$ /// 1 USD$ = 1/0.01 PMA = 100 PMA /// Start the calculation from one ether - PMA Token has 18 decimals /// Multiply by the DECIMAL_FIXER (1e+10) to fix the multiplication of the rate /// Multiply with the fiat amount in cents /// Divide by the Rate of PMA to Fiat in cents /// Divide by the FIAT_TO_CENT_FIXER to fix the _fiatAmountInCents 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); } /// @dev Checks if a registration request is valid by comparing the v, r, s params /// and the hashed params with the client address. /// @param v - recovery ID of the ETH signature. - https://github.com/ethereum/EIPs/issues/155 /// @param r - R output of ECDSA signature. /// @param s - S output of ECDSA signature. /// @param _client - client address that is linked to this pull payment. /// @param _pullPaymentExecutor - address that is allowed to execute this pull payment. /// @param _pullPayment - pull payment to be validated. /// @return bool - if the v, r, s params with the hashed params match the client address function isValidRegistration( uint8 v, bytes32 r, bytes32 s, address _client, 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) == _client; } /// @dev Checks if a deletion request is valid by comparing the v, r, s params /// and the hashed params with the client address. /// @param v - recovery ID of the ETH signature. - https://github.com/ethereum/EIPs/issues/155 /// @param r - R output of ECDSA signature. /// @param s - S output of ECDSA signature. /// @param _paymentID - ID of the payment. /// @param _client - client address that is linked to this pull payment. /// @param _pullPaymentExecutor - address that is allowed to execute this pull payment. /// @return bool - if the v, r, s params with the hashed params match the client address function isValidDeletion( uint8 v, bytes32 r, bytes32 s, bytes32 _paymentID, address _client, address _pullPaymentExecutor ) internal view returns (bool) { return ecrecover( keccak256( abi.encodePacked( _paymentID, _pullPaymentExecutor ) ), v, r, s) == _client && keccak256( abi.encodePacked(pullPayments[_client][_pullPaymentExecutor].paymentID) ) == keccak256(abi.encodePacked(_paymentID) ); } /// @dev Checks if a payment for a beneficiary of a client exists. /// @param _client - client address that is linked to this pull payment. /// @param _pullPaymentExecutor - address to execute a pull payment. /// @return bool - whether the beneficiary for this client has a pull payment to execute. function doesPaymentExist(address _client, address _pullPaymentExecutor) internal view returns (bool) { return ( bytes(pullPayments[_client][_pullPaymentExecutor].currency).length > 0 && pullPayments[_client][_pullPaymentExecutor].fiatAmountInCents > 0 && pullPayments[_client][_pullPaymentExecutor].frequency > 0 && pullPayments[_client][_pullPaymentExecutor].startTimestamp > 0 && pullPayments[_client][_pullPaymentExecutor].numberOfPayments > 0 && pullPayments[_client][_pullPaymentExecutor].nextPaymentTimestamp > 0 ); } /// @dev Checks if the address of an owner/executor needs to be funded. /// The minimum amount the owner/executors should always have is 0.001 ETH /// @param _address - address of owner/executors that the balance is checked against. /// @return bool - whether the address needs more ETH. function isFundingNeeded(address _address) private view returns (bool) { return address(_address).balance <= MINIMUM_AMOUNT_OF_ETH_FOR_OPERATORS; } }