Skip to content

ERC: transferAndCall Token Standard #677

Closed
@se3000

Description

@se3000

Preamble

 ERC: 677
 Title: transferAndCall Token Standard
 Type: Informational
 Category: ERC
 Status: Draft
 Created: 2017-07-17
 Requires: ERC20

Simple Summary

Allow tokens to be transferred to contracts and have the contract trigger logic for how to respond to receiving the tokens within a single transaction.

Abstract

This adds a new function to ERC20 token contracts, transferAndCall which can be called to transfer tokens to a contract and then call the contract with the additional data provided. Once the token is transferred, the token contract calls the receiving contract's function onTokenTransfer(address,uint256,bytes) and triggers an event Transfer(address,address,uint,bytes), following the convention set in ERC223.

Motivation

ERC20 requires a multistep process for tokens to be transferred to a contract. First approve must be called on the token contract, enabling the contract to withdraw the tokens. Next, the contract needs to be informed that it has been approved to withdraw tokens. Finally, the contract has to actually withdraw the tokens, and run any code related to receiving tokens. This process typically takes two to three steps, which is inefficient and a poor user experience.

While ERC223 solves the described problem with its transfer(address,uint256,bytes) function, it opens other problems. ERC223 changes the behavior of ERC20's transfer(address,uint256), specifying that it should throw if transferring to a contract that does not implement onTokenTransfer. This is problematic because there are deployed contracts in use that assume they can safely call transfer(address,uint256) to move tokens to their recipient. If one of these deployed contracts were to transfer an ERC223 token to a contract(e.g. a multisig wallet) the tokens would effectively become stuck in the transferring contract.

This ERC aims to provide the helpful functionality of ERC223 without colliding with it. By giving contracts a reason to implement onTokenTransfer before ERC223 becomes widely implemented, a smooth transition is provided until a larger part of the Ethereum ecosystem is informed about and capable of handling ERC223 tokens. transferAndCall behaves similarly to transfer(address,uint256,bytes), but allows implementers to gain the functionality without the risk of inadvertently locking up tokens in non-ERC223 compatible contracts. It is distinct from ERC223's transfer(address,uint256,bytes) only in name, but this distinction allows for easy distinguishability between tokens that are ERC223 and tokens that are simply ERC20 + ERC667.

Specification

Token

transferAndCall

function transferAndCall(address receiver, uint amount, bytes data) returns (bool success)

Transfers tokens to receiver, via ERC20's transfer(address,uint256) function. It then logs an event Transfer(address,address,uint256,bytes). Once the transfer has succeeded and the event is logged, the token calls onTokenTransfer(address,uint256,bytes) on the receiver with the sender, the amount approved, and additional bytes data as parameters.

Receiving Contract

onTokenTransfer

function onTokenTransfer(address from, uint256 amount, bytes data) returns (bool success)

The function is added to contracts enabling them to react to receiving tokens within a single transaction. The from parameter is the account which just trasfered amount from the token contract. data is available to pass additional parameters, i.e. to indicate what the intention of the transfer is if a contract allows transfers for multiple reasons.

Backwards Compatibility

This proposal is backwards compatible for all ERC20 tokens and contracts. New tokens and contracts moving forward can implement the transferAndCall functionality, but also still fallback to the original approve-transferFrom workflow when dealing with legacy contracts. It does not require any changes or additional steps from already deployed contracts, but enables future contracts to gain this functionality.

Implementation

Example implementation.

Activity

Arachnid

Arachnid commented on Jul 20, 2017

@Arachnid
Contributor

What calls receiveApproval, and in what circumstances?

I see the point of structuring approveAndCall like this for backwards-compatibility reasons, but wouldn't a transferAndCall be a better option going forward? With this, you only need a single storage update, instead of two.

se3000

se3000 commented on Jul 21, 2017

@se3000
Author

receiveApproval is called by the token contract, from within the approveAndCall function after the receiving contract has been approved to withdraw.

The reason for not using transferAndCall is that it would make it difficult, and often impossible, for the receiving contract to actually verify that tokens had been transferred to it(verifying would either require someone to check logs off chain, or a new API to be introduced). Since this function has to be public in order for tokens to call it, it would be very easy to maliciously call it and tell it that tokens were transferred without actually ever transferring tokens. By making the contract withdraw the tokens itself, the contract ensures that the transfer happens/succeeds.

Another reason which is less critical, but a nice feature, is that approveAndCall allows the contract some flexibility to withdraw tokens. If a price paid in tokens is dynamically calculated, you can list how much you're willing to pay, call the contract which then calculates how much is needed and withdraws only the required amount. Slightly more flexible.

MicahZoltu

MicahZoltu commented on Jul 21, 2017

@MicahZoltu
Contributor

I would rather see an addition to tokens that allows anyone to call transfer with an additional parameter that is a signature of the token holder. This way, dApps can simply ask the token holder (off-chain) for a signed allowance and then pass that to the transfer call when they need it. A bit more thought needs to be put into exactly how to protect against replay attacks, but I think this would result in a much simpler and more intuitive interface than the user having to interact with the dApp via the token (which is an awkward API).

vbuterin

vbuterin commented on Jul 25, 2017

@vbuterin
Contributor

The reason for not using transferAndCall is that it would make it difficult, and often impossible, for the receiving contract to actually verify that tokens had been transferred to it(verifying would either require someone to check logs off chain, or a new API to be introduced).

How so? User A calls transferAndCall of contract C, contract C then calls receiveTransfer of contract B. Contract B sees that msg.sender = C, and so it's a legit transfer of tokens.

The reason why transferAndCall is superior is as @Arachnid says, it allows you to get rid of any expensive superfluous storage updates whatsoever - all that happens is tokens get transferred and the recipient gets notified.

If a price paid in tokens is dynamically calculated, you can list how much you're willing to pay, call the contract which then calculates how much is needed and withdraws only the required amount. Slightly more flexible.

This is also doable with transferAndCall: you would first call some constant function getHowMuchINeedToPay of the recipient, then send a transferAndCall with that precise amount.

se3000

se3000 commented on Jul 25, 2017

@se3000
Author

The worst case I was imagining is something like a decentralized exchange dealing with lots of tokens they can't evaluate, but just enable people to use them. In this case people could introduce malicious tokens, but as I think more about it, a malicious token contract could also sabotage the approveAndCall functionality.

The getHowMuchINeedToPay pattern would work, but the dynamic gas pattern wouldn't be available with as a standardized behavior.

I'm open to transferAndCall, but leaning towards including both. Is the address token parameter necessary? It seems like it wouldn't be for transferAndCall but maybe still allows some flexibility with approveAndCall. What is enabled by taking the parameter and comparing to msg.sender, as opposed to just using msg.sender as the token parameter? The main benefit I see is fitting the existing pattern that some tokens have used.

Dexaran

Dexaran commented on Jul 29, 2017

@Dexaran
Contributor

Can someone explain me what is the reason of having approves in standard tokens?
They are needed for nothing since the common pattern is just to handle an incoming transaction.
ETH transfers works as above and are handled by payable functions (fallback in most cases).

As I've supposed earlier that the only purpose of approval mechanism was to prevent stack depth attack.
#223 (comment)

So the only reason of approves is backwards compatibility with ERC20?

se3000

se3000 commented on Aug 1, 2017

@se3000
Author

@Dexaran backwards compatibility seems like the most important reason to me. Beyond that, approved withdrawal caps are a pretty standard pattern that have unique workflows that would be more difficult without the approve method.

se3000

se3000 commented on Aug 1, 2017

@se3000
Author

@MicahZoltu Interesting proposal, it looks as if it would fit the pattern proposed by EIP662. It seems like a broad pattern which may be orthogonal and complimentary to token transfer standards.

changed the title [-]ERC: approveAndCall Token Standard[/-] [+]ERC: approveAndCall/transferAndCall Token Standard[/+] on Aug 1, 2017
Dexaran

Dexaran commented on Aug 2, 2017

@Dexaran
Contributor

approved withdrawal caps are a pretty standard pattern that have unique workflows that would be more difficult without the approve method.

@se3000
What unique workflows are you talking about? I ask for an example over the past three months, but I have never received any real answer. Only answers such as "a useful functional that you must believe exists, but no one has seen it."

By the way Ether dosn't have any approves and everything is OK with it.

MicahZoltu

MicahZoltu commented on Aug 2, 2017

@MicahZoltu
Contributor

@Dexaran I'm with you on wanting to remove approvals entirely. However, the workflow that is meaningful to me is the one where you have some complicated system (such as Augur) that has need to move user tokens around internally. Having the user start the interaction with a token transfer works, but it complicates the interface because it means the entrypoint for Augur is always through a token, rather than through a contract call. Since the details of the interaction are coming through via the data parameter, this means that the receiving contract needs to decode those bytes (in-contract) and then make decisions based on the data. Not only are these bytes opaque to any user looking at the transaction (which includes tools designed to show transaction details using the ABI), decoding them into something useful on-chain is hard.

For the most naive case, the data could just be a number (effectively part of an enum), and this isn't too bad. However, this won't work for all cases such as an exchange where you need to send quite a few details along with the token transfer like counterparty address, token being traded for, price, etc. All of this needs to be encoded into a byte array that is totally opaque to any reader.

Note: The above aren't insurmountable problems, and I'm not sure approve is really any better of a solution because it requires multiple signatures from the user (something a UI can't hide), but I do think it is something worth considering.

vbuterin

vbuterin commented on Aug 3, 2017

@vbuterin
Contributor

Approvals have one other problem: using approvals creates a workflow where the contract that uses the approval needs to do something like: (i) call transferFrom, THEN (ii) do something else. This is an external call followed by a state change, which is generally considered a no-no because of re-entrancy issues.

I now increasingly support transferAndCall as defined in #223, as the execution path is very clean - first go into the token contract, edit balances there, then go into the destination account.

I previously had the objection that it complicates implementation since you would need to be able to do "ABI inside ABI", but I have since softened on this since realizing that multisig contracts and other forwarding contracts require this already.

Also, stack depth attacks are irrelevant since the Tangerine Whistle hard fork (that's last October).

changed the title [-]ERC: approveAndCall/transferAndCall Token Standard[/-] [+]ERC: transferAndCall Token Standard[/+] on Sep 11, 2017
se3000

se3000 commented on Sep 11, 2017

@se3000
Author

Ok, thanks for the feedback so far. Based on the helpful feedback, I've switched this to transferAndCall and updated the spec at the top.

At first I thought that ERC223 was sufficient to offer the transfer and call functionality. Based on further consideration it seems like not every token will want to be ERC223 compatible, as some contracts using ERC20 today do not allow for an approve - transferFrom withdrawal process. Such contracts would end up unintentionally holding ERC223 tokens and not allowing them to be withdrawn by contracts, because transfer(address,uint256) throws an error when sending to a contract; a common use case being a multisig wallet receiving tokens from another contract.

transferAndCall(address,uint256,bytes) as currently proposed is effectively the same as ERC223's transfer(address,uint256,bytes), calling the same tokenFallback(address,uint256,bytes) function on the receiver, and triggering the same Transfer(address,address,uint256,bytes) event. This allows contracts set up to receive tokens via ERC667 to be compatible with ERC223 transfers in the future and vice versa. By providing a step that allows for ERC20 tokens to call tokenFallback, it provides a transition path, until more contracts are prepared to handle receiving ERC223 tokens.

MicahZoltu

MicahZoltu commented on Sep 11, 2017

@MicahZoltu
Contributor

Feedback is basically the same as I provided to ERC223. onTokenTransfer rather than tokenFallback and I'm still a fan of transferAndCall taking in a string name of the function to call (with onTokenTransfer being default). I also recommend using the method name transfer rather than transferAndCall since Solidity supports function overloads and IMO having several functions named transfer with different parameters is more pithy than having several functions all with different names and different parameters.

120 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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

        Participants

        @Arachnid@niran@alexvandesande@3esmit@chfast

        Issue actions

          ERC: transferAndCall Token Standard · Issue #677 · ethereum/EIPs