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

Detect issues in parent initializer calls #1095

Draft
wants to merge 24 commits into
base: master
Choose a base branch
from

Conversation

ericglau
Copy link
Member

@ericglau ericglau commented Oct 25, 2024

Detects issues in parent initializer calls.

This infers functions as initializers if they have a modifier that is initializer, reinitializer, or onlyInitializing, or the function itself is named as initialize, initializer, reinitialize, or reinitializer.
For parent contracts, we only consider internal functions as initializers that must be called from the child.
For child contracts, we only consider non-private functions as initializers.

This reports an error if any parent contract has an inferred initializer and any of the following are true for the (child) contract being validated:

  1. Missing initializer: This contract does not appear to have an initializer.
  2. Missing initializer call: This contract's initializer is missing a call to a parent initializer.
  3. Duplicate initializer call: This contract has duplicate calls to the same parent initializer function.
  4. Incorrect initializer order: This contract does not call parent initializers in the correct order.

Limitations (could be future improvements):

  • This does not recursively check the initializer function, where its called functions (e.g. helper functions) may have calls to the parent initializer. This could result in a false positive, but users could workaround this by using unsafe-allow missing-initializer-call
  • If a parent contract has a public initializer (which is ignored in this PR), the child contract does not strictly need its own initializer if the child contract has nothing else to initialize. But if the child contract DOES have an initializer, should we enforce that it also calls the parent's public initializer?
  • Allow users to annotate that a function is an initializer - see Detect issues in parent initializer calls #1095 (comment)

Fixes #160

@ericglau ericglau changed the title Check for initializer errors Detect issues in parent initializer calls Dec 10, 2024
@ericglau ericglau marked this pull request as ready for review December 10, 2024 21:01
@ericglau ericglau requested a review from a team December 10, 2024 21:01
@ericglau ericglau requested a review from Amxx December 17, 2024 22:32
(fnDef.modifiers.some(modifier =>
['initializer', 'reinitializer', 'onlyInitializing'].includes(modifier.modifierName.name),
) ||
['initialize', 'initializer', 'reinitialize', 'reinitializer'].includes(fnDef.name)) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For another PR, I think it may be useful if we annotate initializers with @custom:initializer or similar.
The idea is that there are contracts with initializers that don't follow the initialize or initializer convention in its name.

A concrete example is any of the Signers we created for ERC4337 accounts:

abstract contract SignerRSA is AbstractSigner {
    error SignerRSAUninitializedSigner(bytes e, bytes n);

    bytes private _e;
    bytes private _n;

    /// @custom:initializer true
    function _initializeSigner(bytes memory e, bytes memory n) internal {
        if (_e.length != 0 || _n.length != 0) revert SignerRSAUninitializedSigner(e, n);
        _e = e;
        _n = n;
    }

...
}

packages/core/src/validate/run.ts Outdated Show resolved Hide resolved
@ericglau ericglau marked this pull request as draft December 20, 2024 22:36
@ericglau
Copy link
Member Author

The previous approach did not work for one of the main use cases that we want to detect: missing initializer calls to transitive parent initializers.

Here is an illustrating example:

contract Ownable2Step_Bad is Initializable, ERC20Upgradeable, Ownable2StepUpgradeable {
, which represents the issue in OpenZeppelin/openzeppelin-contracts#4690. Since Ownable2StepUpgradeable inherits OwnableUpgradeable but does not call its initializer, the user's contract must call it.
The previous approach would not detect this issue because it only checks the direct parent initializers.

Instead, I changed this to get the entire linearized base contracts list, remove the ones that are already initialized by some other parent in the list, and then check if the user's contract has an initializer that calls the remaining ones.


Remaining considerations:

  • Duplicate initializer calls are currently not detected if they occur from different contracts (e.g. a grandparent initializer is called by both a parent and a child). This is not necessarily unsafe. Initializer usage is not a strict as constructors, and this cannot be reliably detected because users could have multiple initializers and/or reinitializers. Perhaps we should avoid checking for duplicates altogether, or just leave the duplicate checks for be the same contract.
  • Incorrect order checks might also be too strict. Incorrect order of linearized contracts is not necessarily unsafe. For example, in this testcase where the parent is initialized before the grandparent, that is incorrect linearization but does not pose an actual issue. Perhaps we should check linearization order only for initializer calls to direct parents.
  • Simplify/cleanup code.

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

Successfully merging this pull request may close these issues.

Detect issues in parent initializer calls
3 participants