0

Background

RETN Deals offers discount coupons from global brands and in addition, offers generous cashback in Cryptocurrency (RETN). The web application automatically tracks consumer purchases at these stores and rewards them in crypto. Once the user reaches the min withdraw-able limit that is set within the web-app, they become eligible to redeem RETN.

Looking for the best approach to handle the redemption process.

Ways of approach

Approach 1.

Whenever the user clicks Redeem, the web app calls token.transfer method and transfers the token from RETN Deal’s address to the user’s ERC20 address.

Benefits

  • Validation happens on the server side, the user cannot manipulate more value than what they eligible for.

Problems

  • Gas Price: The web app owner has to bear the gas price for every single redeem. Cost consumed in transferring the tokens to the end users are very high
  • Queue: Every user’s transactions get clogged until the previous transactions get cleared. This is because of the NONCE will get incremented for every transaction as they originate from the same address.

Approach 2

Create a smart contract that can be called (via metamask) by the user

Benefits

  • Cost-effective : The consumer is the bearer for the gas price

  • Queue : Transaction origination will be different for each customer’s and thus the transaction will be fast

  • Customer’s choice: The customer can choose how much to withdraw, and when to withdraw it. He/she can control the gas price too.

Problems

  • The withdraw-able limit has to be set every time in the contract so that the user cannot withdraw more than they deserve. This consumes gas. Even if an array of items is set, the number of items in an array cannot be more than 180(approx.) due to the block/transaction gas limit, thus more number of transactions will need to set the limit itself which will again consume the gas from the sender and the transactions keep pending in the queue until its ancestor transactions get mined.

  • If the user’s withdraw-able limit is not set, they will not able to redeem

  • If we are to allow the users to redeem without setting the limit, the user can withdraw more value than what he/she is eligible for.

What is a better approach to allowing consumers to redeem RETN, which can create value for the consumer, maintain the security of the contract, reduce gas consumption by RETN, and not clog the blockchain?

Should oraclize be of any help to this kind of problems? Guess that will still consume gas.

0

Found the best way is to make the receiver pay for the transaction. Followed Steve Marx in this article to arrive the following code. The flow of the code will be like below.

  • Users update their preferred wallet address on the app
  • Users ensure they have ETH for gas and the wallet address loaded in metamask
  • On the server side, calculating how many tokens they are supposed to receive, and then signs the transaction

node server code

const recipient = receipientaddress;
const amount =  web3.toWei(tokensToSend);
const nonce = req.body.nonce; //calucated nonce of the wallet address (of metamask)
const tmpContractAddress = "0x...";//contract address that will send the token (address of the deployed contract below) 
var hash = "0x" + ethereumjs.soliditySHA3(
    ["address", "uint256", "uint256", "address"],
    [recipient,amount, nonce, tmpContractAddress]
  ).toString("hex");
var signed = web3.personal.sign(hash, senderAddr, senderAddrPass);

smart contract code

pragma solidity ^0.4.20;

/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {

  address public owner;
  event OwnershipTransferred (address indexed _from, address indexed _to);

  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  function Ownable() public{
    owner = msg.sender;
    emit OwnershipTransferred(address(0), owner);
  }

  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }

  /**
   * @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 newOwner) public onlyOwner {
    require(newOwner != address(0));
    owner = newOwner;
    emit OwnershipTransferred(owner,newOwner);
  }
}

/**
 * @title Token
 * @dev API interface for interacting with the Token contract 
 */
interface Token {
  function transfer(address _to, uint256 _value) external returns (bool);
  function balanceOf(address _owner) constant external returns (uint256 balance);
}

contract RetnWithdraw10 is Ownable{
    address owner;
    Token token;
    mapping(address => mapping(uint256 => bool)) usedNonces;
    event Redeemed(address indexed beneficiary, uint256 value);

    function RetnWithdraw10() public {
        address _tokenAddr = 0x815CfC2701C1d072F2fb7E8bDBe692dEEefFfe41;//0x2ADd07C4d319a1211Ed6362D8D0fBE5EF56b65F6; 
      // test token 0x815CfC2701C1d072F2fb7E8bDBe692dEEefFfe41;
      token = Token(_tokenAddr);
        owner = msg.sender;
    }

    function claimPayment(uint256 amount, uint256 nonce, bytes sig) public {


        require (token.balanceOf(this) >= amount);
        require (!usedNonces[msg.sender][nonce]);

        // This recreates the message that was signed on the client.
        bytes32 message = prefixed(keccak256(msg.sender, amount, nonce, this));

        require (recoverSigner(message, sig) == owner);


        usedNonces[msg.sender][nonce] = true;

        //msg.sender.transfer(amount);
        if(token.transfer(msg.sender,amount)){
           emit Redeemed(msg.sender,amount);
      }
      else
      usedNonces[msg.sender][nonce] = false;
    }

    // Destroy contract and reclaim leftover funds.
    function kill() public onlyOwner{
        uint256 remaining = token.balanceOf(this);
        if(remaining>0)
            token.transfer(owner,remaining);
        selfdestruct(msg.sender);
    }


    // Signature methods

    function splitSignature(bytes sig)
        internal
        pure
        returns (uint8, bytes32, bytes32)
    {
        require(sig.length == 65);

        bytes32 r;
        bytes32 s;
        uint8 v;

        assembly {
            // first 32 bytes, after the length prefix
            r := mload(add(sig, 32))
            // second 32 bytes
            s := mload(add(sig, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(sig, 96)))
        }

        return (v, r, s);
    }

    function recoverSigner(bytes32 message, bytes sig)
        internal
        pure
        returns (address)
    {
        uint8 v;
        bytes32 r;
        bytes32 s;

        (v, r, s) = splitSignature(sig);

        return ecrecover(message, v, r, s);
    }

    // Builds a prefixed hash to mimic the behavior of eth_sign.
    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return keccak256("\x19Ethereum Signed Message:\n32", hash);
    }
}

and finall at html (front-end) side

tokensToRedeem = 2; //should be same as what is passed to the server
        var abi = [ { "constant": false, "inputs": [ { "name": "amount", "type": "uint256" }, { "name": "nonce", "type": "uint256" }, { "name":  "sig", "type": "bytes" } ], "name": "claimPayment", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" } ];
        var MyContract = web3.eth.contract(abi);
        var contrctAddress = "0x...";//deployed contract address
        var myContractInstance = MyContract.at(contrctAddress);
        web3.eth.getAccounts(function(error, result) { 
            if(!error)
            {
                if(result.length == 0)
                {
                    toastr.error('Please ensure you logged in metamask.');
                    return false; 
                }
                myContractInstance.claimPayment(web3.toWei(tokensToRedeem),2,hash,{ from:result[0],
                            to:contrctAddress,
                            value:  0, 
                        }, function(err, transactionHash) {
                            if (!err)
                            {
                            $('#btnMetaMask').css('display','none');
                                $('#notesMeta').html("Transaction Submitted. Track <a target='_blank' href='<?php echo $etherscanurl;?>/tx/"+transactionHash +"'>here</a>");
                                console.log(transactionHash); 
                            }
                            else
                            {
                                console.log('Error While Redeem');
                                console.log(error);
                            }; 
                    }) 
                } 
                else
                { console.log('error');
                    console.log(err);
                }
            });

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.