Skip to content
Open
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/stateless-constructor-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openzeppelin/upgrades-core': patch
---

Allow contracts annotated with `@custom:stateless` to define constructor logic during upgrade safety validation.
10 changes: 10 additions & 0 deletions packages/core/contracts/test/ValidationsNatspec.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,20 @@ contract HasNonEmptyConstructorNatspec4 {
constructor() { msg.sender; }
}

/// @custom:stateless
contract StatelessNonEmptyConstructor {
constructor() {
assembly {
sstore(0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00, 1)
}
}
}

contract ParentHasNonEmptyConstructorNatspec1 is HasNonEmptyConstructorNatspec1 {}
contract ParentHasNonEmptyConstructorNatspec2 is HasNonEmptyConstructorNatspec2 {}
contract AncestorHasNonEmptyConstructorNatspec1 is ParentHasNonEmptyConstructorNatspec1 {}
contract AncestorHasNonEmptyConstructorNatspec2 is ParentHasNonEmptyConstructorNatspec2 {}
contract ChildOfStatelessNonEmptyConstructor is StatelessNonEmptyConstructor {}

/// @custom:oz-upgrades-unsafe-allow state-variable-assignment
contract HasStateVariableAssignmentNatspec1 {
Expand Down
18 changes: 17 additions & 1 deletion packages/core/contracts/test/cli/Validate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ contract HasUpgradeToConstructorUnsafe {
}
}

/**
* @custom:stateless
*/
contract StatelessConstructor {
constructor() {
assembly {
sstore(0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00, 1)
}
}
}

contract HasStatelessConstructorParent is StatelessConstructor {
function upgradeTo(address) public {
}
}

/**
* @custom:oz-upgrades
*/
Expand Down Expand Up @@ -151,4 +167,4 @@ contract StorageV2_Ok_NoAnnotation {
uint256 public x;
uint256 public y;
uint256[48] private __gap;
}
}
9 changes: 9 additions & 0 deletions packages/core/src/cli/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,15 @@ test('validate - single contract - ok', async t => {
t.snapshot(output);
});

test('validate - single contract with stateless constructor parent - ok', async t => {
const temp = await getTempDir(t);
const buildInfo = await artifacts.getBuildInfo(`contracts/test/cli/Validate.sol:HasStatelessConstructorParent`);
await fs.writeFile(path.join(temp, 'validate.json'), JSON.stringify(buildInfo));

const output = (await execAsync(`${CLI} validate ${temp} --contract HasStatelessConstructorParent`)).stdout;
t.regex(output, /SUCCESS/);
});

test('validate - fully qualified version of ambiguous contract name', async t => {
const temp = await getTempDir(t);
const buildInfo = await artifacts.getBuildInfo(`contracts/test/ValidationsSameNameSafe.sol:SameName`);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ test.before(async t => {
const contracts = [
'contracts/test/Validations.sol:HasEmptyConstructor',
'contracts/test/ValidationsNatspec.sol:HasNonEmptyConstructorNatspec1',
'contracts/test/ValidationsNatspec.sol:ChildOfStatelessNonEmptyConstructor',
'contracts/test/Proxiable.sol:ChildOfProxiable',
'contracts/test/ValidationsUDVT.sol:ValidationsUDVT',
'contracts/test/ValidationsFunctionPointers.sol:InternalFunctionPointer',
Expand Down Expand Up @@ -128,6 +129,7 @@ testValid('ParentHasNonEmptyConstructorNatspec1', 'transparent', true);
testValid('ParentHasNonEmptyConstructorNatspec2', 'transparent', true);
testValid('AncestorHasNonEmptyConstructorNatspec1', 'transparent', true);
testValid('AncestorHasNonEmptyConstructorNatspec2', 'transparent', true);
testValid('ChildOfStatelessNonEmptyConstructor', 'transparent', true);
testValid('HasStateVariableAssignmentNatspec1', 'transparent', true);
testValid('HasStateVariableAssignmentNatspec2', 'transparent', true);
testValid('HasStateVariableAssignmentNatspec3', 'transparent', false);
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/validate/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import { extractLinkReferences, LinkReference } from '../link-refs';
import { extractStorageLayout } from '../storage/extract';
import { StorageLayout } from '../storage/layout';
import { getFullyQualifiedName } from '../utils/contract-name';
import { getAnnotationArgs as getSupportedAnnotationArgs, getDocumentation } from '../utils/annotations';
import {
getAnnotationArgs as getSupportedAnnotationArgs,
getDocumentation,
hasAnnotationTag,
} from '../utils/annotations';
import { getStorageLocationAnnotation, isNamespaceSupported } from '../storage/namespace';
import { UpgradesError } from '../error';
import { assertUnreachable } from '../utils/assert';
Expand Down Expand Up @@ -369,6 +373,10 @@ function getNamespacedCompilationContext(
}

function* getConstructorErrors(contractDef: ContractDefinition, decodeSrc: SrcDecoder): Generator<ValidationError> {
if (hasAnnotationTag(getDocumentation(contractDef), 'stateless')) {
return;
}

for (const fnDef of findAll('FunctionDefinition', contractDef, node => skipCheck('constructor', node))) {
if (fnDef.kind === 'constructor' && ((fnDef.body?.statements?.length ?? 0) > 0 || fnDef.modifiers.length > 0)) {
yield {
Expand Down