solidity icon indicating copy to clipboard operation
solidity copied to clipboard

Allow nonpayable functions to override payable functions

Open frangio opened this issue 4 years ago • 23 comments

Abstract

If an interface defines a view function, it is possible for a derived contract to provide a pure implementation of it.

Could it be possible to extend this so that payable functions can be implemented with non payable functions? It is the same kind of state mutability narrowing.

Motivation

https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2610

ERC721's transferFrom is defined as payable in the spec. Our standard implementation is not payable, because if it were payable we would have to define what to do with the message value, which isn't standardized so we don't want to define a specific way to handle it.

Because our implementation isn't payable, we've also defined the interface as non-payable, but people have pointed out that 1) this is more restrictive than the spec, and 2) there is no way to extend our implementation (through inheritance) to make it payable and add logic to handle msg.value.

Specification

A nonPayable function should be a valid implementation of a payable function.

A child contract should be able to extend our nonPayable implementation with a payable function to add a way to handle msg.value before forwarding to super.transferFrom.

frangio avatar Apr 13 '21 19:04 frangio

This is a duplicate of https://github.com/ethereum/solidity/issues/3412. But that issue was apparently closed prematurely, so I'm glad to see this back from the dead.

fulldecent avatar May 25 '21 02:05 fulldecent

I remember that we discussed this, but thinking about it for a minute, I cannot see a reason why this should not be allowed. @ekpyron what do you think?

chriseth avatar May 25 '21 14:05 chriseth

I remember that we discussed this, but thinking about it for a minute, I cannot see a reason why this should not be allowed. @ekpyron what do you think?

Apparently, I was for allowing it at some point in https://github.com/ethereum/solidity/issues/3412#issuecomment-652419856 and @axic half-agreed https://github.com/ethereum/solidity/issues/3412#issuecomment-654928436, but was still sceptical :-). Without thinking about it too long, I also still don't see a reason why not.

ekpyron avatar May 25 '21 15:05 ekpyron

Tentatively agreed on today's meeting to allow narrowing from payable to non-payable, but keep widening from non-payable to payable disallowed.

Narrowing is similar to the case of view vs pure, with the difference that the compiler inserts code (the check for msg.value == 0), however that code can be already added manually by the user into payable functions.

Widening on the other hand would mean that the expected behaviour (fail on value) is not enforced.

axic avatar Jun 02 '21 13:06 axic

As a non-exhaustive list of auditors, pinging @GNSPS @montyly @austin-williams @stbeyer for feedback.

axic avatar Jun 02 '21 13:06 axic

We also discussed marking it as "good first issue". I'm adding the label with "difficulty: medium". The implementation is simple so it should be doable for someone new but will probably require some guidance.

cameel avatar Jun 02 '21 15:06 cameel

How would it work if I defines a payable function, A is I implements it as non-payable, and C is I, A implements it as payable? Is that considered widening?

My first impression is that this should be allowed. C is declaring that it wants to allow receiving value in that function.

I would agree that C is A should not be able to widen it from non-payable to payable.

frangio avatar Jun 03 '21 14:06 frangio

I would say it's the same as in the case of non-view/view/pure: If a contract strengthens the requirement in the inheritance graph, all derived contracts must also implement the stronger version. In your example, C must implement it as non-payable.

If you declare the function in A as non-payable, then you say that if you are implementing a derived contract that claims to conform to the requirements of A's interface, the function also has to reject all ether transfers.

C is I, A essentially says that C conforms to both I and A.

Why would you say that C is A is different from C is I, A?

chriseth avatar Jun 03 '21 14:06 chriseth

I see... I think this is due to a different interpretation of what payable and non-payable mean. The situation is not exactly the same as non-view/view/pure, because in that case the default is the least strict (non-view), and with non-payable/payable the default is the most strict (non-payable).

In my mind, payable is the meaningful one and is a way to declare that you want to accept value. But if the compiler sees non-payable as a way to declare that you want to reject value, these are two perspectives that will arrive at different expectations for how overrides should behave.

C is I, A says C conforms to both I and A, but then you could say: a) at least one of I,A wants to accept value so C will accept it, or b) at least one of I,A wants to reject value so C will reject it.

I don't really have a strong opinion here... Although the motivation to open this issue was so that we could have IERC721.transferFrom as payable, then implement the core logic in ERC721.transferFrom as non-payable, while still allowing users to use the core logic and add value-handling logic on top. If this is considered widening and will not be allowed, it won't fix this use case. It may be complicated to allow that scenario in a solid way, so I would understand it.

frangio avatar Jun 04 '21 20:06 frangio

What are your opinions if we add a nonPayable attribute, that would be required a compilation to narrow a function shadowed from payable to non-payable?

Because I can see the case of a developer using a library, shadowing a payable function, and forgetting to keep it payable. So having it explicit helps to understand that its the intent of the developper.

So the rules would be something like:

  • default: non-payable
  • payable: only possible if it does not shadow a non-payable function
  • nonPayable: only way to shadow a payable function without being payable (otherwise same behavior as the default)

However, as the current proposal does not solve OZ problem, and I am not aware of any large project that needs this feature, I am not sure it's worth adding complexity here, and we can just keep the existing rules ;)

montyly avatar Jun 10 '21 08:06 montyly

Adding a keyword for nonpayable is not necessary.

Nonpayable is more restrictive than payable and does not introduce dangers. Therefore overriding from payable to nonpayable will not result in people accidentally getting something more dangerous than they expected.


The original issue here is good and should be implemented. Even if "only" OpenZeppelin Contracts and the ERC-721 reference implementation are using this then that should be reason enough to consider this change as impactful.

During the ERC-721 standardization process we identified about 30 language design issues (including this) and most were fixed.

fulldecent avatar Nov 13 '21 16:11 fulldecent

This issue has been marked as stale due to inactivity for the last 90 days. It will be automatically closed in 7 days.

github-actions[bot] avatar Apr 05 '23 12:04 github-actions[bot]

unstale

fulldecent avatar Apr 05 '23 22:04 fulldecent

This issue has been marked as stale due to inactivity for the last 90 days. It will be automatically closed in 7 days.

github-actions[bot] avatar Jul 05 '23 12:07 github-actions[bot]

Bump

fulldecent avatar Jul 06 '23 01:07 fulldecent

This issue has been marked as stale due to inactivity for the last 90 days. It will be automatically closed in 7 days.

github-actions[bot] avatar Oct 05 '23 12:10 github-actions[bot]

I nominate this issue to stay open.

It is last of the topics written in the text of ERC-721 that has not yet been changed in Solidity.

fulldecent avatar Oct 05 '23 12:10 fulldecent

This issue has been marked as stale due to inactivity for the last 90 days. It will be automatically closed in 7 days.

github-actions[bot] avatar Jan 05 '24 12:01 github-actions[bot]

unstale

fulldecent avatar Jan 05 '24 17:01 fulldecent

Also just ran into this. Would be great to see support for it.

robdoesstuff avatar Jan 13 '24 23:01 robdoesstuff

This issue has been marked as stale due to inactivity for the last 90 days. It will be automatically closed in 7 days.

github-actions[bot] avatar Apr 13 '24 12:04 github-actions[bot]

unstale - let's seriously think about this. Just saw another person run into this issue.

robdoesstuff avatar Apr 16 '24 15:04 robdoesstuff

+1 should be unstale, I have also come across such a use case

zjesko avatar Jul 15 '24 10:07 zjesko