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

Do not require override for functions from interfaces #8281

Closed
frangio opened this issue Feb 10, 2020 · 31 comments · Fixed by #11628
Closed

Do not require override for functions from interfaces #8281

frangio opened this issue Feb 10, 2020 · 31 comments · Fixed by #11628
Projects

Comments

@frangio
Copy link
Contributor

frangio commented Feb 10, 2020

The override keyword is required when implementing a function from a parent interface.

interface I {
    function foo() external;
}

contract C is I {
    function foo() external override { // Does not compile without override
    }
}

This doesn't sound right to me. The function is not overriding anything, it's simply filling in a missing abstract function.

@frangio frangio changed the title Required override for functions in interfaces Required override for functions from interfaces Feb 10, 2020
@axic
Copy link
Member

axic commented Feb 10, 2020

I think this is a delicate question. It is true that override may sound misleading, as it suggests an existing implementation is changed. This makes sense for inherited functions. It is confusing for implementing functions from abstract contracts, too.

However, if we would remove the keyword, then it would not be possible to easily tell apart functions specific to C or functions which are part of an inherited interface I.

@frangio
Copy link
Contributor Author

frangio commented Feb 10, 2020

It is confusing for implementing functions from abstract contracts, too.

Yes, I agree.

The way I see it, override is required when overriding an existing function because it hints to the developer or reviewer that something weird can happen at that point. They should consider the consequences, and the syntax also forces them to understand exactly what they're overriding (through override(A, B, C)).

Implementing an abstract function is more of a normal situation. If override is also used in this normal situation, it loses significance. The developer may mistakenly think they're implementing something that was abstract.

@nventuro
Copy link
Contributor

As @axic pointed out, one of the benefits of override is that it lets the reader easily know if a function is 'new' or if it's just a definition for an already declared one. It also acts as an assertion that a function is indeed already declared, and prevents errors if the declaration changes on the interface but not on the implementation (making the derived contract abstract).

However, given that we also have abstract, I'm not sure this is actually needed. Failing to define a function, including the regression bug I mentioned above, will cause the contract to require the abstract keyword, making it obvious something is wrong.

I'm with @frangio on this one: if we require override in these cases, then it becomes very hard for a reader to tell whether a function definition is actually an override, or if it's just the base implementation.

@chriseth
Copy link
Contributor

@frangio @nventuro so you wouldn't want to know if the function actually refers to the declaration in the interface or not? What if you have:

interface I
contract C is I
contract D is C

and a function f is defined in I and D. You wanted to define it also in C but you accidentally made a typo. The only protection against that typo in your proposed model is that you would get an error that you do not need the override in D. I'm almost certain that there are cases with multiple inheritance where this would even pass unnoticed.

@nventuro
Copy link
Contributor

If C didn't fully implement I due to lacking f, it'd then have to be abstract, which would hint that something may be wrong.

More importantly though, if D initially implemented f using override and the compiler did not raise any errors when it was also added to C (as happens in 0.6.2), D's behavior would change from 'implementing an undefined function' to 'overriding a base implementation' with no changes to its source code.

Overrides should be used carefully, with knowledge of the expected behavior of the overridden function in the base contract. Allowing contracts to unknowingly override a base function is IMO dangerous.

@frangio
Copy link
Contributor Author

frangio commented Feb 11, 2020

More importantly though, if D initially implemented f using override and the compiler did not raise any errors when it was also added to C (as happens in 0.6.2), D's behavior would change from 'implementing an undefined function' to 'overriding a base implementation' with no changes to its source code.

Note that this is only true if f is implemented as virtual override in C.

@ekpyron
Copy link
Member

ekpyron commented Feb 11, 2020

I'm wondering: Would anyone have a good suggestion for a second keyword for indicating that a function implements a previously unimplemented function? implements would be a natural choice, but then you kind of expect it saying what it implements... and requiring function f() external implements(I.f) might be a tad too much and I don't really like the look of it, but maybe there's a better choice...
EDIT: implements function f() { .. } could work, I'm not sure... but yeah, just thought it might be worth exploring that option.

@robinbryce
Copy link

robinbryce commented Mar 3, 2020

Interfaces require that inheriting contracts implement all the methods declared in the interface. There is, by definition, nothing to override. This was a clean intuitive means to guard against incomplete implementations in related contracts.

Now, with 0.6, having made that declaration, we aditionally need to tag all the implementations as 'override' - and yet they are not overriding anything.

Interfaces were great as they were and now they are, if not broken, certainly diminished.

Is there no way to restore interfaces without compromising the goals for this keyword ?

@nventuro
Copy link
Contributor

nventuro commented Mar 4, 2020

It feels like removing the requirement for override (or switching to a new keyword) when implementing an interface function (along with a deprecation message on the compiler output) would be non-contentious in terms of backwards compatibility.

From this discussion, it seems like moving in this direction would be desirable. Does anyone feel strongly about keeping override for some reason not discussed here?

@ekpyron
Copy link
Member

ekpyron commented Mar 4, 2020

Just to strengthen the case against removing the requirement without any replacement:
Suppose you have two interfaces:

interface I {
  function doImportantStuff(address x) external;
}
interface J {
  function getSomeData(address x) returns (uint256) external;
}

Now consider two parties - one providing a contract library, and the other using it.
Initially the first party writes:

abstract contract A is I, J {
  function doImportantStuff(address x, address y) external
  {
    // ...
  }
}

Now you have an auditor confirm that this always does the right stuff. It's a perfectly conformant implementation of I for an implementation of J the user provides. Every conformant implementation of J will get a conformant implementation of I, nothing can go wrong.

Based on that the second party writes:

contract B is A {
  function validate(address x, address y) public {
    if (/* some involved condition */) revert();
  }
  function getSomeData(address x) returns(uint256) external {
    validate(msg.sender, x);
    return data[x];
  }
}

And an auditor confirms: this is fine - getSomeData conforms perfectly to the requirements of J, this is foolproof.

But now there is a fancy third party, providing a cheaper, safer and more feature-rich implementation of I. They write:

abstract contract A2 is I, J {
  function validate(address x, address y) public virtual;
  function doImportantStuff(address x, address y) external
  {
    validate(x,y);
    // ...
  }
}

And their contracts are audited. It gives you a perfectly conformant implementation of I, if you provide a conformant implementation of J. It is even rumored to be especially safe, since it enforces additional validation.

Now B switches from A to A2. They know their implementation of J is conformant and you know that for every conformant J both A and A2 provide the required conformant I. A2 is cheaper and safer, so why not use that. And you end up with:

contract B is A2 {
  function validate(address x, address y) public {
    if (/* some involved condition */) revert();
  }
  function getSomeData(address x) returns(uint256) external {
    validate(msg.sender, x);
    return data[x];
  }
}

Everything compiles, no reason to doubt this... but you potentially have an utterly broken implementation of the important stuff.

That's of course highly artificial and a bit stupid, but I still maintain that allowing to silently implement unimplemented functions is dangerous. And there would be no justification to allow it for interfaces, but not for abstract contracts, which increases the problem.

@nventuro
Copy link
Contributor

nventuro commented Mar 4, 2020

Yes, the scenario you've described isn't too different from Solidity v0.5, where you unexpectedly may end up overriding an already defined function.

Given the focus on readability and explicitness, somehow signalling that a definition corresponds to a previous declaration makes sense. I like the implements function syntax you proposed, but don't mind the alternatives.

@chriseth
Copy link
Contributor

I see the point that implementing an interface function and overriding a previously implemented functions are two different things, but I'm not sure we should add yet another keyword for that. Furthermore, note that you can have a function that, at the same time, implements an interface and changes the functionality of a parallel base class.

@frangio
Copy link
Contributor Author

frangio commented Mar 12, 2020

I also don't think we should add another keyword for this.

@robinbryce
Copy link

robinbryce commented Mar 13, 2020

If doImportantStuff is actually 'important' to the implementor of B they would not have switched to A2. If it is important to consumers of B presumably the author of B would advertise the important change. I think attempting to solve this scenario (a social problem) with a keyword breaks what inheritiance is for.

If as a consumer of B i am very sensitive to (or untrusting of) how doImportantStuff is implemented, what I actually want is

contract C is B[using A for doImportantStuff] {
/// not suggesting the class decl is the right place, its just an easier illustration.
}

I am explicitly opting out of the normal mechanics of inheritance, which permit the provider of B full discretion on how 'doImportantStuff' is implemented. This is more explicit than having to carpet bomb my implementations with a keyword that is, mostly, redundant.

Or, rather than overrides everywhere, instead have 'overrides a in b' only in those places where this kind of thing is a problem.

@ekpyron
Copy link
Member

ekpyron commented Mar 13, 2020

To be honest, I'm not in favor of introducing a new keyword either. I was hoping that we could establish that it makes sense to somehow indicate that a function implements a previously unimplemented function, because otherwise we give way to implicit unexpected changes in behavior (the otherwise rather silly example was mainly meant to demonstrate a case where this in fact happens).

Based on that I'd personally argue that the best means to indicate this is to keep it at requiring override for implementing unimplemented functions.

Or put differently: you said: "Or, rather than overrides everywhere, instead have 'overrides a in b' only in those places where this kind of thing is a problem." - I agree with that, but I would argue that implementing unimplemented functions is a place where this kind of thing is a problem.

@robinbryce
Copy link

Thats a fair response. I quible over 'implementing unimplemented' in the case of interfaces. Do you think this situation is an inevitable consequence of mixing 'abstract' with 'interfaces' ? Ie, what interfaces led me to expect is just simply not the right way to think about smart contracts ?

@ekpyron
Copy link
Member

ekpyron commented Mar 13, 2020

Does it help to read override as "change what a previously existing identifier refers to" rather than "change the implementation of a previously existing function"?
The former happens for interface functions, implemented functions and unimplemented functions alike and I'd argue it's something worth making explicit and I think one can construct "problematic" cases without mixing abstract contracts and interfaces. But we should, of course, also take care not to over-engineer things like this relative to potentially merely artificial cases...

@robinbryce
Copy link

Hah! I think that is quite nice mental judo but in my case I read interfaces as saying "I don't refer to anything yet and you must fill that in". But that is a very special case of what is generally going on in significant inheritance graphs. I'm content to adapt.

@elenadimitrova elenadimitrova added this to New issues in Solidity May 14, 2020
@elenadimitrova
Copy link
Collaborator

I also feel strongly for removing the override keyword in interface implementations. Considering interfaces can now inherit too, we could enforce all interface methods are implemented in a contract. This would eliminate the problematic cases described above and is also what I would logically expect when something claims to be I. Essentially when contract C is I {..} we require that all of I is implemented in C. If I it to be implemented across multiple contracts then it can be split down into child interface contracts.

@chriseth
Copy link
Contributor

@elenadimitrova what about an abstract contract that adds some functionality to an interface but not everything?

@elenadimitrova
Copy link
Collaborator

abstract contracts stay unchanged, without the need to override interface methods they implement. For instance:

interface I {
    function a() external view returns (uint);
    function b() external view returns (uint);
}

// partially implements I
abstract contract A is I { 
    function a() external view returns (uint) {
        return 1;
    }
}
// partially implements I
abstract contract B is I {
    function b() external view returns (uint) {
        return 2;
    }
}
contract C is I, A, B { }

What exactly bothers you there?

@chriseth
Copy link
Contributor

"we could enforce all interface methods are implemented in a contract" - we already do enforce that unless the contract is abstract.

@frangio frangio changed the title Required override for functions from interfaces Do not require override for functions from interfaces Jun 16, 2020
@makoto
Copy link

makoto commented Jun 19, 2020

Am I correct to understand that I have to do virtual override if I am using both Interface and Abstract like below?

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;

/**
 * @title Owner
 * @dev Set & change owner
 */
interface Foo {
    function foo1() external view returns (uint);
    function bar1() external view returns (uint);
}


abstract contract Bar is Foo {
    function bar1() external virtual override view returns (uint) {}

    function foo1() external view override returns (uint){
        return this.bar1();
    }
}

contract Baz is Bar {
    function bar1() external override view  returns (uint){
        return 1;   
    }
}

I was currently trying to upgrade my smartcontract but I end up adding override into all functions in the abstract which personally feels like defeating the benefit of having virtual and override to begin with

@robinbryce
Copy link

All of the arguments in favour of override seem to miss the point of ineheritance all together. They are based on largely synthetic examples. And those examples really only illustrate well understood 'gotchas' with Inheritance in general. Really if there is a compeling case for override then inheritance is too dangerous to have in the language all together (somewhat toungue in cheek).

Developers who prioritize explicitness over the established conventions for inheritance are free not to inherit. The burden is then theirs. With respect, override seems to be little more than an opinionated burden on the rest of us. And it is certainly not helping us in any way I recognise.

Remove override or remove inheritance ;-)

@nventuro
Copy link
Contributor

I end up adding override into all functions

This is IMO the key issue: override becomes meaningless when all functions have it, instead of being an attribute that makes you pay attention and think about the implications of overriding the base definition.

@chriseth
Copy link
Contributor

@makoto is your intention that Bar::bar1 is a complete "default" implementation of the interface that returns zero? if not, why do you not leave it out?

@chriseth chriseth moved this from New issues to Design Backlog in Solidity Jun 22, 2020
@frangio
Copy link
Contributor Author

frangio commented Jun 16, 2021

Another data point on this.... As I was reviewing code, I saw a function definition with an override modifier. I was surprised that I didn't see a super call in its body, and had to think about the implications, until I realized this function was only declared in an interface and there was no overriding happening.

@maurelian
Copy link
Contributor

maurelian commented Jun 16, 2021

Seeing this issue for the first time, wanted to add my support. I assume that at least some of the motivation for the override keyword was meant to address the issue that Phil Daian pointed out here, by providing a flag when an inheritance collision happens. But echoing @nventuro's statement above:

override becomes meaningless when all functions have it, instead of being an attribute that makes you pay attention and think about the implications of overriding the base definition.

@cameel
Copy link
Member

cameel commented Jun 25, 2021

We discussed this on the last call. There seems to be no consensus here so we need more feedback. To make discussion a bit more organized and bring more attention to this issue I have created a forum post laying out the available options and summarizing the most important points from this discussion: Should overriding function declarations require the override keyword?. Please post your opinions there.

@chriseth chriseth moved this from Design Backlog to Icebox in Solidity Jul 7, 2021
@ekpyron
Copy link
Member

ekpyron commented Jul 7, 2021

Since we're discussing this again:

The example case in which not requiring override is actually dangerous and can lead to silent unexpected changes in behaviour, if a base interface and a base contract change in a specific way, is only really relevant for overriding unimplemented public functions. (It relies on some contract calling the unimplemented base function, but it's unlikely to do so externally with an interface function instead of redeclaring the interface function public and calling it internally.)
Given that, the actual danger of not requiring override for interface functions may indeed be minimal (e.g. be the argument that contracts will have to turn abstract on typos, etc.).

So while I personally still think it conceptually makes sense to mark functions overriding interface functions, I'd not object to drop that requirement based on popular opinion :-).

@chriseth chriseth moved this from Icebox to Design Backlog in Solidity Jul 7, 2021
@5hanth
Copy link

5hanth commented Jul 16, 2021

imo override keyword without corresponding virtual is sufficient unless the intention is to restrict all inherited contracts from changing the behaviour of the base contract. if one fails to add the virtual keyword to all functions in a UUPS upgradeable contract, they all become non-overrideable if the base contract is inherited during upgrades.

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

Successfully merging a pull request may close this issue.

12 participants