Skip to content

ERC865: Pay transfers in tokens instead of gas, in one transaction #865

Closed
@lgalabru

Description

@lgalabru

Preamble

EIP: <to be assigned>
Title: Token standard
Author: Ludovic Galabru
Type: Informational
Category: ERC
Status: Draft
Created: 2018-01-30
Requires: EIP20

Simple Summary

Ability for token holders to pay transfer transactions in tokens instead of gas, in one transaction.

Abstract

The following describes one standard function a token contract can implement to allow a user to delegate transfer of tokens to a third party. The third party pays for the gas, and takes a fee in tokens.

Motivation

When it comes to using tokens as utility tokens, we need to strive for a good UX. Introducing wallets and transactions to end users is a challenge, and having to explain that token holders needs ETH to send tokens is adding some friction to the process. The goal of this EIP is to abstract the gas for the end user, by introducing a fee paid in tokens. A third party can then bring the transaction on-chain, pay for the gas of that given transaction and get tokens from that user.

Specification

  • A: Sender of the payment
  • B: Recipient of the payment
  • D: Delegate, doing the transaction for A, and paying for the gas.
  • X: Amount of Token T sent from A to B
  • Y: Fee paid in Token T, from A to D for the transaction
  • T: Token to send
  • N: Nonce

Process

The user A gets a quote from the delegate D for the value of the fee Y for 1 transaction (depending on gas price + value of token in ETH).

With their private key, the user generates {V,R,S} for the sha3 of the payload P {N,A,B,D,X,Y,T}.

The user sends {V,R,S} and P (unhashed, unsigned) to the delegate.

The delegate verifies that Y and D have not been altered.

The delegate proceeds to submit the transaction from his account D:

T.delegatedTransfer(N,A,B,X,Y,V,R,S)

The delegatedTransfer method reconstructs the sha3 H of the payload P (where T is the address of the current contract and D is the msg.sender).

We can then call ecrecover(H, V, R, S), make sure that the result matches A, and if that’s the case, safely move X tokens from A to B and Y tokens from A to D.

The challenge mainly resides in imitating the Non-standard Packed Mode on the client side, and obtaining the exact same sha3 as the one generated on-chain.

Methods

delegatedTransfer

function transferPreSigned(
    bytes _signature,
    address _to,
    uint256 _value,
    uint256 _fee,
    uint256 _nonce
)
    public
    returns (bool);

Is called by the delegate, and performs the transfer.

Events

TransferPreSigned

event TransferPreSigned(address indexed from, address indexed to, address indexed delegate, uint256 amount, uint256 fee);

Is triggered whenever delegatedTransfer is successfully called.

Implementation proposal

Assuming a StandardToken and SafeMath available, one could come up with the following implementation.

On-chain operation (solidity)

    /**
     * @notice Submit a presigned transfer
     * @param _signature bytes The signature, issued by the owner.
     * @param _to address The address which you want to transfer to.
     * @param _value uint256 The amount of tokens to be transferred.
     * @param _fee uint256 The amount of tokens paid to msg.sender, by the owner.
     * @param _nonce uint256 Presigned transaction number. Should be unique, per user.
     */
    function transferPreSigned(
        bytes _signature,
        address _to,
        uint256 _value,
        uint256 _fee,
        uint256 _nonce
    )
        public
        returns (bool)
    {
        require(_to != address(0));

        bytes32 hashedParams = transferPreSignedHashing(address(this), _to, _value, _fee, _nonce);
        address from = recover(hashedParams, _signature);
        require(from != address(0));

        bytes32 hashedTx = keccak256(from, hashedParams);
        require(hashedTxs[hashedTx] == false);

        balances[from] = balances[from].sub(_value).sub(_fee);
        balances[_to] = balances[_to].add(_value);
        balances[msg.sender] = balances[msg.sender].add(_fee);
        hashedTxs[hashedTx] = true;

        Transfer(from, _to, _value);
        Transfer(from, msg.sender, _fee);
        TransferPreSigned(from, _to, msg.sender, _value, _fee);
        return true;
    }

    /**
     * @notice Hash (keccak256) of the payload used by transferPreSigned
     * @param _token address The address of the token.
     * @param _to address The address which you want to transfer to.
     * @param _value uint256 The amount of tokens to be transferred.
     * @param _fee uint256 The amount of tokens paid to msg.sender, by the owner.
     * @param _nonce uint256 Presigned transaction number.
     */
    function transferPreSignedHashing(
        address _token,
        address _to,
        uint256 _value,
        uint256 _fee,
        uint256 _nonce
    )
        public
        pure
        returns (bytes32)
    {
        /* "48664c16": transferPreSignedHashing(address,address,address,uint256,uint256,uint256) */
        return keccak256(bytes4(0x48664c16), _token, _to, _value, _fee, _nonce);
    }

Off-chain usage (js)

    describe(`if Charlie performs a transaction T, transfering 100 tokens from Alice to Bob (fee=10)`, () => {
      beforeEach(async () => {
        const nonce = 32;
        const from = alice;
        const to = bob;
        const delegate = charlie;
        const fee = 10;
        const amount = 100;
        const alicePrivateKey = Buffer.from('c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3', 'hex');

        const components = [
          Buffer.from('48664c16', 'hex'),
          formattedAddress(this.token.address),
          formattedAddress(to),
          formattedInt(amount),
          formattedInt(fee),
          formattedInt(nonce)
        ];
        const vrs = ethUtil.ecsign(hashedTightPacked(components), alicePrivateKey);
        const sig = ethUtil.toRpcSig(vrs.v, vrs.r, vrs.s);
        await this.token.transferPreSigned(
          sig,
          to,
          amount,
          fee,
          nonce
          , {from: charlie}).should.be.fulfilled;
      });

Full implementation available

OpenZeppelin/openzeppelin-contracts#741

Additional documentation

Transfer Ethereum tokens without Ether — An ERC20 Improvement to Seriously Consider

Copyright

Copyright and related rights waived via CC0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions