Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strong-guests-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openzeppelin/upgrades-core': patch
---

Fix `Missing initializer calls` error when initializer name ends with `_unchained`
74 changes: 74 additions & 0 deletions packages/core/contracts/test/ValidationsInitializer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,80 @@ contract Recursive_Ok is Parent {
}
}

// ==== Omitting validations of *_unchained for non-parent contracts ====

contract Parent_With_Unchained is Initializable {
uint64 x;
function __Parent_init() onlyInitializing internal {
__Parent_init_unchained();
}

function __Parent_init_unchained() onlyInitializing internal {
x = 1;
}
}

contract Child_With_Unchained_Ok is Initializable, Parent_With_Unchained {
uint64 y;
function __Child_init() onlyInitializing internal {
__Parent_init_unchained();
__Child_init_unchained();
}

function __Child_init_unchained() onlyInitializing internal {
y = 1;
}
}

contract Child_Missing_Parent_Unchained_Call_Bad is Initializable, Parent_With_Unchained {
uint64 y;
function __Child_init() onlyInitializing internal {
// missing call to __Parent_init_unchained
__Child_init_unchained();
}

function __Child_init_unchained() onlyInitializing internal {
y = 1;
}
}

contract Child_Duplicate_Parent_Unchained_Call_Bad is Initializable, Parent_With_Unchained {
uint64 y;
function __Child_init() onlyInitializing internal {
__Parent_init_unchained();
__Parent_init_unchained();
__Child_init_unchained();
}

function __Child_init_unchained() onlyInitializing internal {
y = 1;
}
}

contract Parent2_With_Unchained is Initializable {
uint64 x2;
function __Parent2_init() onlyInitializing internal {
__Parent2_init_unchained();
}

function __Parent2_init_unchained() onlyInitializing internal {
x2 = 1;
}
}

contract Child_Wrong_Order_Parent_Unchained_Call_Warning is Initializable, Parent_With_Unchained, Parent2_With_Unchained {
uint64 y;
function __Child_init() onlyInitializing internal {
__Child_init_unchained();
__Parent2_init_unchained();
__Parent_init_unchained();
}

function __Child_init_unchained() onlyInitializing internal {
y = 1;
}
}

// ==== Complex ERC20 examples ====

contract ERC20_Ok is Initializable, ERC20Upgradeable, ERC20BurnableUpgradeable, ERC20PausableUpgradeable, OwnableUpgradeable, ERC20PermitUpgradeable, ERC20FlashMintUpgradeable, UUPSUpgradeable {
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/validate-initializers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,17 @@ testRejects('Recursive_Bad', 'transparent', {
});
testAccepts('Recursive_Ok', 'transparent');

testAccepts('Child_With_Unchained_Ok', 'transparent');
testRejects('Child_Missing_Parent_Unchained_Call_Bad', 'transparent', {
contains: ['Missing initializer calls for one or more parent contracts: `Parent_With_Unchained`'],
count: 1,
});
testRejects('Child_Duplicate_Parent_Unchained_Call_Bad', 'transparent', {
contains: ['Duplicate calls found to initializer `__Parent_init_unchained` for contract `Parent_With_Unchained`'],
count: 1,
});
testAccepts('Child_Wrong_Order_Parent_Unchained_Call_Warning', 'transparent'); // warn 'Expected: Parent_With_Unchained, Parent2_With_Unchained'

testAccepts('ERC20_Ok', 'uups');
testRejects('ERC20_Bad', 'uups', {
contains: [
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/validate/run/initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,11 @@ function* getInitializerCallExceptions(
}
}

// Report any remaining parents that were not directly initialized
// Report any remaining parents that were not directly initialized,
// unless this initializer is named `*_unchained` since by design it doesn't need to call parent initializers
const unchained = contractInitializer.name.endsWith('_unchained');
if (
!unchained &&
remainingDirectCalls.length > 0 &&
!skipCheck('missing-initializer-call', contractDef) &&
!skipCheck('missing-initializer-call', contractInitializer)
Expand Down