OpenZeppelin / openzeppelin-contracts

OpenZeppelin Contracts is a library for secure smart contract development.
https://openzeppelin.com/contracts
MIT License
25.01k stars 11.82k forks source link

Deprecate ERC777 implementation #2620

Closed k06a closed 1 year ago

k06a commented 3 years ago

🧐 Motivation

There is an opinion that ERC777 is over-engineered and is a bad practice to follow. Moreover it introduces bad abstractions to rely on and requires very important checks to be implemented by every integrator.

Let's switch to EIP2612 (Permit) to deprecate dangerous infinite approve behavior and make it mainstream ASAP: https://github.com/OpenZeppelin/openzeppelin-contracts/contracts/token/ERC20/extensions/draft-ERC20Permit.sol

📝 Details

Do we need to collect list of issues? Starting here:

  1. Whole idea of avoiding spam tokens by having hook function is wrong. It is not possible to protect from spam tokens because developers of these tokens will always modify their tokens to allow spamming – just ignore them.
  2. Token fallback concept is wrong because the callback is being called from token smart contract and there is no way to verify who was the original msg.sender. There are so many possible ways to abuse ERC777Receiver. The only way to solve it I see – work with whitelisted tokens only – DeFi deserves better approach. Imagine you have DEX and wanna deposit token and do custom action (swap):

    function tokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    ) external override {
        address token = msg.sender;
        balances[token][operator] += amount;
        _performSwap(operator, operatorData); // <- `operator` is not trustworthy, due `msg.sender` can be malicious smart contract
    }
    
    function _performSwap(address user, bytes memory data) internal {
        // ...
    }
Amxx commented 3 years ago

Hello @k06a, As a developper working with ERC777 during hackathon, I can only agree that this standard creates a lot of frustration ... and can result in very gas-extensive transactions.

However, deprecating contract breaks backward compatibility. This is not something we would consider doing during a "minor" release. The latest major release was v4.0, less then a month ago. Thus, we wouldn't move with anything like that before a while.

Still, I'd love to build a case for/against ERC777 (and other contract that might not be relevant). This will help us taking decision whenever we fell like moving to the next major version.

rstormsf commented 3 years ago

I definitely support this, so new solidity developers won't inherit this broken token design pattern. We have seen enough exploits/hacks with this approach.

k06a commented 3 years ago

@Amxx @rstormsf thanks for your support, added 2 cases to the OP.

frangio commented 3 years ago

Just to clarify, we can't remove the contract until the next major release, but if there is consensus we can deprecate it, in the sense of announcing its deprecation, not recommending or promoting it anymore, possibly hiding it from the documentation site, etc.

Thank you all for raising this.

phbdias commented 3 years ago

I agree that there are some issues with ERC777, however I don't see any alternative to avoid the ERC20 behavior of 'approve' then 'transferFrom' (opposing to the one-step transfer that the ERC777 proposes). There are any safer alternative to ERC777 (already implemented) that I am missing out?

Thanks in advance!

k06a commented 3 years ago

@phbdias sure, check out ERC20Permit.sol

Amxx commented 3 years ago

@phbdias ERC1363, which is not yet part of OZ contracts, but can easily be added is there is a request for it, is also good to know !

frangio commented 3 years ago

We don't think it should be OpenZeppelin Contracts making the decision to deprecate the ERC.

We're happy to include comments in our documentation about the downsides and alternatives.

For actual deprecation, we're happy to host the discussion here, but ultimately we feel that a deprecation requires a more ecosystem-wide consensus.

jeffreyscholz commented 2 years ago

@Amxx as rough as the transition is, I think some token that supports a receiver hook needs to be supported. If it isn't ERC777, then by all means ERC1363. Having to approve tokens first is counterintuitive to newcomers and it introduces attack vectors. I don't have strong opinions about the ERC777, but ERC1363 does seem more intuitive to me. Regardless, I'm 100% behind moving to a new token standard away from ERC20. It needs to happen at some point.

k06a commented 2 years ago

We can have better EIP which would allow receiver contract to do transferFrom(msg.sender, …), during transferAndCall where msg.sender will be token contract. But this flow would make it fully compatible with ERC20. Except msg.sender will not be beneficiary of the call anymore.

k06a commented 2 years ago

Here is the concept of CREATE2-based factory for arbitrary calls, where the receiver of the call still can do transferFrom(msg.sender, ..., ...) to have ultimate compatibility with ERC20: https://gist.github.com/k06a/0ea02ecd824bd3d44c760825b62bc264

Pros:

Cons:

frangio commented 2 years ago

@k06a I was going to mention ERC827 but I see you were involved in that discussion already!

I think the approach with CREATE2 is interesting but isn't it an important security practice to always invoke transferFrom with msg.sender as the argument? For example, as seen here for Uniswap.

Doesn't this make that approach useless? Because msg.sender would not be the owner of the tokens.

k06a commented 2 years ago

@frangio I agree, check my example, it is fully compatible with tranferFrom(msg.sender, …)

frangio commented 2 years ago

Oh I hadn't noticed that. It's a pretty interesting idea, but I don't think I'd feel comfortable recommending it. transferFrom is not the only thing the receiver may use msg.sender for. For example, the auxiliary Caller contract may be granted an LP NFT in return, and some of these assets or rights may not be transferable from the auxiliary contract back to the "real" msg.sender.

k06a commented 2 years ago

@frangio exactly, that’s why I use create2 based factory, where salt is equal to user address. This makes users to own this address of msg.sender in the call 😁

Pandapip1 commented 2 years ago

Not sure if this issue is still active, but how about https://eips.ethereum.org/EIPS/eip-4524?

lukehutch commented 2 years ago

Beyond all the over-engineering, the biggest issue with ERC777 is that it is insecure as defined, because the sender notification interface is called before state is updated: #3463

@Amxx I agree that ERC1363 is a good standard (simple and secure), as is ERC4524. An implementation of both of these should be in OpenZeppelin, and the two implementations could both share a lot of common code.

Differences: ERC1363 does not allow sending to EOAs or using an EOA as the spender/operator; ERC4524 allows both. ERC1363 allows notification of both spender/operator and recipient; ERC4524 only notifies recipients. Both declare their notification interface(s) via ERC165, which is simpler than ERC1820 used by ERC777.

Amxx commented 2 years ago

ERC1363 does not allow sending to EOAs or using an EOA as the spender/operator

Where do you get that from? AFAIK this is not true.

Amxx commented 2 years ago

ERC1363 allows notification of both spender/operator and recipient;

How is that an issue ?

In both cases, only one address is notified, and that is the address of the entity that could possibly want to react to either receiving tokens, or being approved to transfer tokens.

Amxx commented 2 years ago

the biggest issue with ERC777 is that it is insecure as defined, because the sender notification interface is called before state is updated

This is not necessarily an issue if handled properly. Our implementation of ERC4626 show how being aware of that, we can make sure it doesn't become a liability

lukehutch commented 2 years ago

ERC1363 does not allow sending to EOAs or using an EOA as the spender/operator

Where do you get that from? AFAIK this is not true.

From the spec (search for "MUST implement"), and from the reference implementation: 1 2.

Maybe you're thinking of using the ERC20 approve/transferFrom methods from an ERC1363-capable token, because ERC1363 is a superset of ERC20. I'm referring to the ERC1363-specific methods, approveAndCall/transferFromAndCall.

ERC1363 allows notification of both spender/operator and recipient;

How is that an issue ?

I wasn't saying it was an issue. I was only pointing out differences between the APIs, to show that there are reasons to implement more than one of these standards or proposals.

One of the primary reasons for the existence of ERC777, ERC1363, ERC4524, etc. is to prevent tokens being sent to contracts that do not know what to do with them, especially non-proxied contracts that cannot be modified (because it causes any received tokens to be lost forever). All of these standards prevent sending to contracts that do not define or declare the right interface. ERC777 and ERC4524 also transparently degrade to a standard transfer when sending to an EOA. ERC1363 doesn't allow that, so it's technically the strongest of all three standards in terms of guaranteeing that you're only sending to the desired address. But being able to send to an EOA using the ERC777 or ERC4524 API, without having to switch back to the ERC20 API, has the benefit of convenience.

Another slightly more esoteric issue with ERC777, by the way, is that if the receiver declares the correct ERC1820 interface but somehow incorrectly defines the receiver notification hook, so that the function signature is wrong, and if that contract defines a fallback function that does not revert, then the ERC777 token contract will think it successfully notified the receiver even though the receiver notification hook never got called. Therefore the fallback function feature of Solidity actually breaks the ERC777 standard, for improperly implemented receivers. ERC1363 and ERC4524 don't have this issue, because they require the hooks they call to return their own function selector as a return value.

Amxx commented 2 years ago

ERC1363 does not allow sending to EOAs or using an EOA as the spender/operator

Where do you get that from? AFAIK this is not true.

From the spec (search for "MUST implement"), and from the reference implementation: 1 2.

Maybe you're thinking of using the ERC20 approve/transferFrom methods from an ERC1363-capable token, because ERC1363 is a superset of ERC20. I'm referring to the ERC1363-specific methods, approveAndCall/transferFromAndCall.

It says that

A contract that wants to accept token payments via transferAndCall or transferFromAndCall MUST implement the following interface:

It also says that

This proposal has been inspired by the ERC-721 onERC721Received and ERC721TokenReceiver behaviours.

My understanding is that, just like in the case of 721, you should be able to transferAndCall (or similar) to an address that has no code ... the call will be successful, and we should accept the empty returndata if the target has no code.

lukehutch commented 2 years ago

Yes but look at the reference implementation. It cannot send to EOAs, or allow EOAs to act as spender.

Therefore, it's clear that the total omission of any mention of EOAs in ERC1363 actually originates from the author's intent that "A contract that wants to accept" means "A contract (and only a contract) that wants to accept"...

Amxx commented 2 years ago

One of the primary reasons for the existence of ERC777, ERC1363, ERC4524, etc. is to prevent tokens being sent to contracts that do not know what to do with them

That may be the goal of 4524, but it is not the reason for 777 or 1363. I'm particularly sure of that in the context of 1363. The reason is for the contract to react to a token transfer, which simplifies the "approve + transferFrom" workflow, allow the target to "prepare", to send events, ...

Amxx commented 2 years ago

Yes but look at the reference implementation

Do you have a link? Its not in the ERC. (Also the reference implementation could technically be wrong, it does not replace the specs)

lukehutch commented 2 years ago

One of the primary reasons for the existence of ERC777, ERC1363, ERC4524, etc. is to prevent tokens being sent to contracts that do not know what to do with them

That may be the goal of 4524, but it is not the reason for 777 or 1363. I'm particularly sure of that in the context of 1363. The reason is for the contract to react to a token transfer, which simplifies the "approve + transferFrom" workflow.

I pointed this out in my comparison of these standards. ERC1363 is the only one that is capable of notifying the spender, and that's quite interesting and unique. But the whole purpose of notifying the recipient originated from the fact that hundreds of millions of dollars have been lost already because of ERC20 tokens being sent to contracts that couldn't handle them.

Do you have a link? Its not in the ERC. (Also the reference implementation could technically be wrong, it does not replace the specs)

I provided the link already. Here it is (here they are) again: 1, 2

lukehutch commented 2 years ago

(Also the reference implementation could technically be wrong, it does not replace the specs)

The specs are simply ambiguous. They do not even address the issue of what happens if an EOA is a recipient or spender.

Amxx commented 2 years ago

That is unfortunately true of many ERC. The fact that it explicitly mentions being inspired by 721 makes me think that the behavior should be the same, but that is just an interpretation.

That should indeed be clarified. Without clarification, I guess any implementation is free to do what they consider best ... (which is not great). I don't think however that this ambiguity justifies starting another ERC from scratch. ERC4626 is ambiguous ... even ERC20 is ambiguous about some things (events emitted on mint/burn) ... but we manage to live with it.

Amxx commented 2 years ago

For the record, we have a PR for adding 1363: #3017

lukehutch commented 2 years ago

When I look to implement a spec, I take the combination of the spec plus the reference implementation as the standard (particularly if the reference implementation was written by the same person or people as the spec).

Taking that combination and reimplementing from scratch in a different context has led me to find and report issues (either inconsistencies, ambiguities, or security vulnerabilities) with several EIPs/ERCs. Of course these never get fixed once an ERC is finalized.

In the case of ERC1363, the spec is so short and terse that I don't think it can be taken as a spec without consulting the reference implementation.

Amxx commented 2 years ago

IMO, there is no such thing as the reference implementation unless it's referenced in the ERC in a way that is immutable.

What if the author reads this, and updates the code on its implementation? Would that change the (final) standard?

lukehutch commented 2 years ago

It's a fair question, but there is principle and then practice. Even if we asked the author if their intent was what was in the reference implementation and they said yes, they can't update a standard once it's final. And since the standard is actually ambiguous in this case (or omits explaining how a critical corner case should work), it gives a lot of latitude for the reference implementation to be accepted as the standard way to resolve the ambiguity.

lukehutch commented 2 years ago

I just don't see how digging in on your particular interpretation of ambiguous wording in the spec is better than having OpenZeppelin have the same behavior as the reference implementation written by the same author as the spec. It really is not. If the spec is ambiguous and you choose to resolve the ambiguity in the opposite way from the reference implementation, you're going to create compatibility problems needlessly.

Amxx commented 2 years ago

I pinged the author so that he can give is view on the matter.

vittominacori commented 2 years ago

@Amxx @lukehutch thanks for the overview on that missing part of the spec. Happy to discuss about it.

When I PR the EIP1363, I did not analyzed the EOA address behavior because of my purpose was to standardize the transfer (or the approve) to a contract capable to be notified and make a callback in a single transaction. So in fact, it says that receiver or spender MUST implement the interface.

I referred the ERC721 behavior (I meant the onERC721Received approach), but ERC721 has a single method both for EOA or contracts. As ERC1363 was built on top of ERC20 I couldn't change the standard ERC20 transfer to add bytes data and I also didn't want to overload method signatures. So I added the *AndCall methods just to be used with contracts. If we want to transfer to EOA, or to a contract that does nothing, we can use the standard ERC20 transfer.

I didn't included implementation in the EIP because the reviewer said to remove any external link.

lukehutch commented 2 years ago

Thanks @vittominacori -- I agree it makes logical sense to just use ERC20 methods for transfer to EOA, therefore ERC1363 implementations should succeed only with compliant recipient or spender implementations. (Standards that break ERC20 backwards compatibility (eg. ERC223) are problematic.)

frangio commented 2 years ago

The conversation about EIP 1363 is off topic here. Please continue the discussion in https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3017.

frangio commented 2 years ago

Going back to the discussion about ERC777...

I really wouldn't mind deprecating this contract. It seems pretty clear we've learned that it's not good practice. In particular, the apparent interface-compatibility with ERC20 with subtle differences in behavior, which have extremely important consequences as to reentrancy, was a really bad idea. Even though the ERC20 interface is not required, our implementation includes it by default.

So, I would recommend to deprecate this contract in our next release. If we hear complaints from users for whom ERC777 is important, we can consider adding a version of ERC777 that does not have the ERC20 compatibility layer. However, as far as I know, its advanced features are not in wide use.

cc @k06a @Amxx

lukehutch commented 2 years ago

Just removing the ERC20 API from the ERC777 implementation will not fix the reentrancy issue, because the reentrancy issue is introduced by the EIP itself, by specifying that the sender notification hook (if defined) should run before any state is updated.

lukehutch commented 2 years ago

The solution is to either break with the standard (calling the sender hook after state is updated) -- which it sounds like nobody is comfortable with doing -- or remove ERC777 entirely. Ideally there would be a warning added to EIP777, but I don't think there's a provision for that.

k06a commented 2 years ago

I have no idea why would anyone wish to use ERC777, except newcomers who do not understand what they really need - they think it is “swiss army knife”.

lukehutch commented 2 years ago

Is there any way to determine how many contracts have been deployed to Ethereum mainnet that implement the ERC777 API, or the ERC777 sender or receiver APIs?

k06a commented 2 years ago

@lukehutch I’ll try to count both using https://dune.com

k06a commented 2 years ago

Ethereum Mainnet has in total (https://dune.com/queries/1004627):

Number of unique receivers per month (https://dune.com/queries/1004633):

image

Number of receiver calls per month (https://dune.com/queries/1004633):

image
lukehutch commented 2 years ago

@k06a Super helpful, thanks for running this.

Can you please also run for the sender callback interface, because that's the one that violates Checks-Effects-Interactions (and sender notification is probably much more rare than receiver notification).

k06a commented 2 years ago

Ethereum Mainnet has in total (https://dune.com/queries/1004891):

Number of unique fallbacks per month (https://dune.com/queries/1004894):

image

Number of fallbacks calls per month (https://dune.com/queries/1004894):

image
k06a commented 2 years ago

ERC-777 is officially "Not Recommended" (3 weeks ago). It says "ERC-777 is difficult to implement properly, due to its susceptibility to different forms of attack"

https://twitter.com/subinium/status/1577194524341739520 https://ethereum.org/en/developers/docs/standards/tokens/erc-777/

lukehutch commented 2 years ago

@k06a thanks for the update, this is the appropriate outcome.

One other point I wanted to mention about ERC1363 and ERC4524: both of them require their notification hook functions to return a magic value (the function selector for the notification hook function), which fixes an issue that ERC777, ERC223, and ERC667 had. The notification hook functions of ERC777, ERC223, and ERC667 did not return a value. Therefore, if the notification hook function was not implemented but a fallback function was implemented, it was possible for the notification hook call to appear to succeed when in fact the call should have ended up failing, because that function was not defined.

Therefore, if anyone is looking for an alternative to ERC777 specifically to prevent token loss by sending to contracts that do not support receiving tokens, please consider adding ERC1363 and ERC4524 to your ERC20 implementation. (They are both similar enough that both can be implemented with very little code.)

frangio commented 2 years ago

The link provided is to the ethereum.org site, and it links back to this issue :sweat_smile:. This shouldn't be considered "official" in any way.

We should probably take a stronger stance anyway and begin to recommend against use of ERC777.

Pandapip1 commented 2 years ago

As an EIP editor, I can confirm that EIP-777 is not officially deprecated. There is no process for deprecating EIPs - especially final ones.

As a developer, I can confirm that EIP-777 should no longer be used, and it would be good if OZ hid it from the documentation :)