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

Ownable2StepUpgradeable requires initializing OwnableUpgradeable #4690

Open
ericglau opened this issue Oct 17, 2023 · 9 comments
Open

Ownable2StepUpgradeable requires initializing OwnableUpgradeable #4690

ericglau opened this issue Oct 17, 2023 · 9 comments
Milestone

Comments

@ericglau
Copy link
Member

ericglau commented Oct 17, 2023

🧐 Motivation
Ownable2StepUpgradeable inherits OwnableUpgradeable.

Ownable2StepUpgradeable has initializer __Ownable2Step_init()
OwnableUpgradeable has initializer __Ownable_init(address initialOwner)

Currently contracts that inherit Ownable2StepUpgradeable need to call __Ownable_init(address initialOwner) to set the initial owner. This is not obvious because users who are just inheriting Ownable2StepUpgradeable directly may not know about this transitive requirement.

This differs from the non-upgradeable scenario which was discussed in #4368 (comment), because contracts that extend the non-upgradeable Ownable2Step would fail compilation if they did not call the Ownable constructor.

Next steps
Consider whether the initializer of Ownable2StepUpgradeable should also initialize the owner. This would occur if Ownable2Step calls its parent constructor, however there are some drawbacks to multiple inheritance mentioned in comment #4368 (comment)

@ernestognw
Copy link
Member

Note we have other examples of contract initializers only calling their parent's initializable.

@veljkoTNFT
Copy link

All good if that is how it should, it just needs to be mentioned somewhere, that that is how it generaly works, and for people to pay attention to it.

@ernestognw
Copy link
Member

In my opinion, it should call Ownable's constructor because just writing it in the docs creates friction and is error-prone because it's not enforced by any checks.

Although mentioned, I'd prefer having a dev experience that's secure by default. It's also true that is not likely that someone deploys an uninitialized Ownable seriously to production, but it was the default behavior in 4.9 so it isn't crazy to think someone will ignore it.

@veljkoTNFT
Copy link

It happened to me, but I had tests that cover this case, not sure if everyone does. Maybe something like breaking changes will help, because this is really easy to miss, as it was for me.

@ericglau
Copy link
Member Author

@veljkoTNFT
Copy link

Yup, that was exactly where I was coming from initially. I used 4.7.3 and then switched to 5.

@aviggiano
Copy link
Contributor

Hello

I was about to submit this.

While developing a new project with v5, I was expecting Ownable2StepUpgradeable to automatically initialize OwnableUpgradeable, but then my onlyOwner-guarded functions would not work, which led me to find this issue.

I agree with @ernestognw's suggestion, this is not only a matter of having better docs.

@ernestognw
Copy link
Member

Thanks for the feedback to both.

Something worth noting is that we've previously identified that OZ upgrade plugins should check lack of initialization as well (among other things).

For now, adding Ownable to Ownable2Step's constructor shouldn't be a breaking change for 5.1.
I'll tag this as a potential 5.x milestone but may not be implemented if we find any backwards compatibility issue.

@ernestognw
Copy link
Member

ernestognw commented Nov 7, 2023

We chatted about this internally and @Amxx reached the following conclusion:

Consider the following example:

import "@openzeppelin/[email protected]/access/Ownable.sol";

contract OwnableExtension1 is Ownable {
    bool public a;
    constructor(address initialOwner) Ownable(initialOwner) {
        a = true;
    }
}

contract OwnableExtension2 is Ownable {
    bool public b;
    constructor(address initialOwner) Ownable(initialOwner) {
        b = true;
    }
}

contract MyContract is OwnableExtension1, OwnableExtension2 {
    constructor(address initialOwner
        OwnableExtension1(initialOwner)
        OwnableExtension2(initialOwner)
    {}
}

If either OwnableExtension1's or OwnableExtension2's constructor is not called in MyContract ... then we get errors saying:
TypeError: No arguments passed to the base constructor. Specify the arguments or mark "MyContract" as abstract.
But if we declare both constructors, we get:
DeclarationError: Base constructor arguments given twice.

This is a problem that not only applies to Ownable but to any contract following the pattern of calling the parent's constructor. It just happened to Ownable because it's the only case in the library where we also extend the base contract (Ownable).

Generally, this will be a problem for any inheritance tree where the linearization graph is either incomplete or initializes something twice. So I personally don't see a point in fixing just the case of Ownable if it's a broader problem.

We've decided that this is something we should definitely provide along with upgrade-plugins. In a way, is like the replacement for the Solidity inheritance checks (eg. double constructor or missing constructors) but for upgrades. Quoting our current documentation:

The function __{ContractName}_init_unchained found in every contract is the initializer function minus the calls to parent initializers, and can be used to avoid the double initialization problem, but doing this manually is not recommended. We hope to be able to implement safety checks for this in future versions of the Upgrades Plugins.

We'll keep track of this update through this issue in upgrades plugins.

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

4 participants