Description
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.