Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC: transferAndCall Token Standard #677

Closed
se3000 opened this issue Jul 19, 2017 · 79 comments
Closed

ERC: transferAndCall Token Standard #677

se3000 opened this issue Jul 19, 2017 · 79 comments

Comments

@se3000
Copy link

se3000 commented Jul 19, 2017

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.

@Arachnid
Copy link
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
Copy link
Author

se3000 commented Jul 21, 2017

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
Copy link
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
Copy link
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
Copy link
Author

se3000 commented Jul 25, 2017

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
Copy link
Contributor

Dexaran commented Jul 29, 2017

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
Copy link
Author

se3000 commented Aug 1, 2017

@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
Copy link
Author

se3000 commented Aug 1, 2017

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

@se3000 se3000 changed the title ERC: approveAndCall Token Standard ERC: approveAndCall/transferAndCall Token Standard Aug 1, 2017
@Dexaran
Copy link
Contributor

Dexaran commented Aug 2, 2017

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
Copy link
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
Copy link
Contributor

vbuterin commented Aug 3, 2017

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

@se3000 se3000 changed the title ERC: approveAndCall/transferAndCall Token Standard ERC: transferAndCall Token Standard Sep 11, 2017
@se3000
Copy link
Author

se3000 commented Sep 11, 2017

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
Copy link
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.

@Arachnid
Copy link
Contributor

Why specify a fixed function at all? It seems to me it would make a lot more sense to pass in a bytes argument and call the target contract with that as a raw payload.

@MicahZoltu
Copy link
Contributor

@Arachnid Similar argument to that over in the URI EIP. Human readability, which leads to increased security by way of allowing users to make an informed decision. When prompted to sign, if I see transfer(0xdeadbeef, 100, 0xabcdef0123456789) all I know is that I'm transferring 100 tokens to the 0xdeadbeef contract but I don't know anything about how those tokens will be used. For something like Augur (a large project), there are many ways in which a token transfer can be used. On the other hand, if I see: transfer(0xdeadbeef, 100, "buyShare", 0xabcdef0123456789) I at least know that the tokens will be used to buy a share. I haven't figured out an easy way to have the function parameters decoded (which would be even better) but this is at least a step in the right direction. Ideally the call would look something like transfer(0xdeadbeef, 100, "buyShare", "LONG", 200) to indicate that I want to buy 200 long shares for the price of 100 tokens.

@Arachnid
Copy link
Contributor

Arachnid commented Sep 11, 2017

Similar argument to that over in the URI EIP. Human readability, which leads to increased security by way of allowing users to make an informed decision.

This is a standard for an ABI, though - human readability shouldn't come into it, and certainly not as an overriding consideration. Further, with onTokenTransfer being the only fallback function, you won't get any useful information for the user anyway.

Insofar as you can decode the outer call's ABI encoding to show something to the user, too, you could also decode the inner encoding in the same fashion.

@se3000
Copy link
Author

se3000 commented Sep 11, 2017

Why specify a fixed function at all? It seems to me it would make a lot more sense to pass in a bytes argument and call the target contract with that as a raw payload.

The receiving contract should know who sent the tokens and the amount sent. If that is specified in the bytes then the transferrer could lie and say they sent more than they did. Making the token contract report ensures the correct parameters are passed. Or, is there a way I'm missing to make sure the bytes accurately represent the transfer?

@Arachnid
Copy link
Contributor

The receiving contract should know who sent the tokens and the amount sent. If that is specified in the bytes then the transferrer could lie and say they sent more than they did. Making the token contract report ensures the correct parameters are passed. Or, is there a way I'm missing to make sure the bytes accurately represent the transfer?

Information about the transfer does not need to be contained in the message payload; the contract can identify the caller and check its balance there.

@MicahZoltu
Copy link
Contributor

MicahZoltu commented Sep 11, 2017

Information about the transfer does not need to be contained in the message payload; the contract can identify the caller and check its balance there.

Current balance is very different from amount received. In order for current balance to work/be useful, the contract would need to track "last known balance" for every token it receives and update it on every token receipt which means at least 5k additional gas for the state update. This also assumes that the contract isn't receiving tokens via some other means (e.g., traditional ERC20 token transfers).

@MicahZoltu
Copy link
Contributor

MicahZoltu commented Sep 11, 2017

Insofar as you can decode the outer call's ABI encoding to show something to the user, too, you could also decode the inner encoding in the same fashion.

This would require tooling to directly implement support for tokens implementing this. Using the scheme I described above, the tooling only needs to be able to decode/display top level contract calls (which some tools like Parity already do).

@Arachnid Is your assumption that all tools will directly support this spec and understand what the bytes parameter represents, and decode it to display to users in the same way they decode top-level method parameters? I'm not sure where the appropriate place to have this discussion is, but you and I seem to be approaching several EIPs from different angles. I believe we both recognize the value in signing tools being able to provide users with enough details so the user can make an informed signing decision, but in each of the EIPs you seem to be favoring low-level bytearrays while I am favoring higher level APIs. I'm curious what your vision is for the ecosystem that allows for low-level byte array APIs while still providing users the information at signing time to make informed signing decisions.

At a high level, byte arrays are a lossy interface (method name and types cannot be recovered from method hash) which is what I'm arguing against. I think the data that is lost is very important for informed signing decisions. How do you propose allowing users to re-acquire this lost information so they can make an informed signing decision?

@se3000
Copy link
Author

se3000 commented Sep 11, 2017

The current balance approach is interesting. As Micah pointed out it's expensive and not guaranteed to work if tokens are received in other ways. That is probably solvable, but would increase the complexity of writing any contract that receives tokens.

Similarly, how would you determine who sent the tokens? Assuming not tx.origin, that would also have to be specified in the bytes data and could then be faked. Attributing a transfer to someone else doesn't sound malicious, but it could be problematic. Also, attributing a transfer to someone else is still possible for the receiving contract to implement with transferAndCall.

Stepping back a bit, I'm currently of the mind that token transfers would ideally behave as much like Ether transfers as possible. For that to remain, you should always be able to tell who sent you a token, like an equivalent to msg.sender.

@iam-peekay
Copy link

@se3000 do you have an example implementation yet? I assume the receiving contract will be the same as this implementation of the ERC223 token: https://github.com/Dexaran/ERC223-token-standard/blob/Recommended/Receiver_Interface.sol

As a token contract author with an upcoming token sale in the coming weeks, I'm torn on whether to stick to the ERC20 standard, use this standard with transferAndCall, or use the ERC223 standard.

@MicahZoltu
Copy link
Contributor

@iam-peekay I recommend going with ERC20 for now. You can always release a new token and have users migrate once ERC223 or this is finalized.

@Arachnid
Copy link
Contributor

Arachnid commented Sep 12, 2017

@MicahZoltu @se3000 Good points; being able to determine the ultimate sender and the amount they sent are critical pieces of information that have to be supplied in a trusted fashion. I withdraw my suggestion.

I'm not sure where the appropriate place to have this discussion is, but you and I seem to be approaching several EIPs from different angles. I believe we both recognize the value in signing tools being able to provide users with enough details so the user can make an informed signing decision, but in each of the EIPs you seem to be favoring low-level bytearrays while I am favoring higher level APIs. I'm curious what your vision is for the ecosystem that allows for low-level byte array APIs while still providing users the information at signing time to make informed signing decisions.

Ultimately, I'm a fan of representing things in the form most suited to the layer in which they reside, and of minimising unnecessary overhead inside the EVM. That means user-friendly standards for URIs, but machine-level specifications for ABIs.

User insight can be provided by using a higher level encoding to determine what to display to the user, then deriving the ABI level encoding from that, rather than trying to reverse-engineer ABI encoding or embed extra metadata in it.

Showing function calls to a user is a horrible user experience anyway, and for anyone other than a programmer, only marginally more meaningful than just showing them the raw ABI encoded hex data.

Edit: Another alternative to consider is approveAndCall; this has the significant advantage that it Just Works with existing contracts that consume tokens, and doesn't require passing any trusted data to the callee.

@sgitt-vassky
Copy link

sgitt-vassky commented Sep 12, 2017

@iam-peekay @MicahZoltu

Advising people "i recommend going with ERC20 for now" is exactly the same as saying "your users will guaranteed lose money because of vulnerability of this standard but I will still recommend to use it. It's just someone else's money, you don't need to care about it."
Here is a short demonstration about how people will suffer loss of funds if you will go with ERC20:
I will recommend to carefully read this comment:
#223 (comment)

You guys cast a bad shadow on the whole cryptocurrency industry, developing tokens so that people lose money and advising other developers to do the same just because you don't think that it is serious that someone will suffer because of your inattentiveness.

https://medium.com/@dexaran820/erc20-token-standard-critical-problems-3c10fd48657b

https://www.reddit.com/r/ethereum/comments/6h17og/critical_problem_of_erc20_tokens_effect_appeal_to/?st=j7hhqkxe&sh=cfc5ae37

@Arachnid
Copy link
Contributor

Advising people "i recommend going with ERC20 for now" is exactly the same as saying "your users will guaranteed lose money because of vulnerability of this standard but I will still recommend to use it. It's just someone else's money, you don't need to care about it."

No, it's not. ERC223 is in a state of flux, and it's far from clear that it won't lead to its own causes of lost or locked funds. Recommending people implement a standard that isn't even close to being locked down is a recipe for chaos.

@sgitt-vassky
Copy link

If you implement ERC20 then your users will lose money.
If you implement ERC223 then it depends on how you will implement it.

I prefer not to do what is guaranteed to be a mistake and result in loss of money for users.

@MicahZoltu
Copy link
Contributor

Implementing ERC223 right now is equivalent of not implementing any standard. You are welcome to create a non-standard token, it has been done before. If you want your token to be interoperable though, you have to pick a standard and right now there is only one (ERC20).

@Pandapip1
Copy link
Member

Again, @MicahZoltu can we consider this superseded by https://eips.ethereum.org/EIPS/eip-4524? It has nearly the exact same functionality.

@MicahZoltu
Copy link
Contributor

Again, @MicahZoltu can we consider this superseded by https://eips.ethereum.org/EIPS/eip-4524? It has nearly the exact same functionality.

Currently, this is just an idea, not an EIP, so it cannot be superceded by anything. Separately, superseding is not something editors do because that would mean we are taking an opinionated position on which EIPs should supersede which shouldn't. If this was an actual final EIP, the authors of 4524 could mention that 4524 is meant to supersede this one but that wouldn't be any sort of official thing, just the opinion of the authors of 4524.


Regarding this issue though, I am going to close it as this is no longer the right place to pursue this idea further. The correct next course of action @se3000 is to either continue discussion on Ethereum Magicians, or create a draft EIP for this idea.

@jtakalai
Copy link

jtakalai commented Jun 6, 2022

I'd like to point out that since this is already in use in many high value token contracts, it's misleading to call it a mere "idea". EIPs, even accepted ones, are full of "ideas" that never (yet) have seen practical implementation. It ought to be considered a shortcoming in the EIP process itself if it cannot stay ahead of or even closely track the state of the art; if it aspires to be normative but fails to even be exhaustively descriptive. This is not an individual failure, of course, but social/organizational one; also it's not a dramatic failure, since it's very easy to fix once there's agreement on all sides to do the standardization work.

Demoting the actual state of the art to the level of "just an idea" probably doesn't help in the way of forming an agreement.

@Amxx
Copy link
Contributor

Amxx commented Jun 6, 2022

@jtakalai The truth is, ERC-1363 went through the EIP process, got finalized, and I would argue is a better design than issue 677.
So yes, many projects implementing this interface instead of 1363 is definitely not an individual failure. If there is anyone to blame, I would argue the blame does NOT go toward the EIP process, but toward all the devs that ignored this process and implemented 667 regardless.

@Amxx
Copy link
Contributor

Amxx commented Jun 6, 2022

And yes, the fact that 667 is just written down in an issue, without link to a proper discussion, and without a PR to make it actually into the repo means that 667 is definitely NOT an ERC. Calling it an idea is possibly the most accurate thing you could say about it.

@MicahZoltu
Copy link
Contributor

I'd like to point out that since this is already in use in many high value token contracts, it's misleading to call it a mere "idea"

By definition, this is not an official Ethereum standard as there is no Final EIP for it. It may be a defacto standard (there are many such things in the wild), but it isn't an EIP, it doesn't have a number, it doesn't exist as an official Ethereum standard, etc.

@MicahZoltu
Copy link
Contributor

If you want to turn this into an actual numbered standard you are welcome to, just create a pull request against this repository adding it following the instructions in EIP-1.

@luziusmeisser
Copy link

The important part is that this issue stays online so anyone googling ERC 677 gets a rough idea about its purpose. I'm one of the developers usually adding ERC 677 support to tokens as it is a relatively light-weight and convenient "standard" in comparison to the more heavy-weight proposals.

For the future, what I would wish for was a convenient and light-weight standards for "permit" that can be signed offline. And by light-weight I mean low in gas fees. This could also cover some of the "transfer and call" use cases if the receiving contract supports the permits.

@Pandapip1
Copy link
Member

Pandapip1 commented Jun 7, 2022

The important part is that this issue stays online so anyone googling ERC 677 gets a rough idea about its purpose. I'm one of the developers usually adding ERC 677 support to tokens as it is a relatively light-weight and convenient "standard" in comparison to the more heavy-weight proposals.

I would like to point in the direction of https://eips.ethereum.org/EIPS/eip-1363, a final EIP that is effectively this proposal.

@MicahZoltu
Copy link
Contributor

The important part is that this issue stays online so anyone googling ERC 677 gets a rough idea about its purpose.
This issue will remain, but it is important that everyone understand that there is no ERC-677. There may be an ERC-677 in the future, but it may have nothing to do with the concepts put forward in this issue. EIP editors assign EIP numbers during the first step of the EIP process, after a pull request is submitted. At some point in the future we may assign 677 to someone else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests