Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9842397
Support for ERC721 with Mintable → Auto Increment IDs and Upgradeabil…
typicalHuman Oct 24, 2025
478eb87
Formatting fixes
typicalHuman Oct 24, 2025
7069b48
Use ethereum-cryptography, refactor
ericglau Oct 24, 2025
5ed815d
Suggested fixes
typicalHuman Oct 25, 2025
a282e2c
Refactor, cleanup
ericglau Oct 27, 2025
9e1c586
lint
ericglau Oct 27, 2025
f8086dc
Add namespace prefix option, refactor
ericglau Oct 27, 2025
8addbc7
Add namespace prefix to UI
ericglau Oct 27, 2025
1d4fc33
Fix lint
ericglau Oct 27, 2025
d353f08
Use namespace for token bridge
ericglau Oct 27, 2025
c809aa4
Use spaces between vars that have comments
ericglau Oct 27, 2025
2e1258b
Update snapshots
ericglau Oct 27, 2025
bfeef29
Update snapshots
ericglau Oct 28, 2025
db46fd1
Update snapshots
ericglau Oct 28, 2025
18b671d
Fix deno check
ericglau Oct 28, 2025
c56c34c
Added test for ERC721 with namespacePrefix
typicalHuman Oct 28, 2025
17ca029
Updated namespacePrefix description
typicalHuman Oct 28, 2025
ddf8d21
Removed "ethereum-cryptography" dependency & changed "computeNamespac…
typicalHuman Oct 28, 2025
a14b9cc
Update packages/core/solidity/package.json
typicalHuman Oct 28, 2025
417c581
Revert "Update packages/core/solidity/package.json"
ericglau Oct 28, 2025
2695618
Revert "Removed "ethereum-cryptography" dependency & changed "compute…
ericglau Oct 28, 2025
be72639
Merge branch 'master' into erc721-uups-namespaced-integration
typicalHuman Oct 29, 2025
7621522
Update prefix tooltip and prompt
ericglau Oct 29, 2025
77eb539
Check for whitespace
ericglau Oct 29, 2025
9dd2f37
Add unit tests
ericglau Oct 29, 2025
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
7 changes: 7 additions & 0 deletions .changeset/rude-nails-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@openzeppelin/wizard': minor
---

**Breaking changes**: Use namespaced storage instead of state variables when upgradeability is enabled.
- For ERC-20, use namespaced storage for `tokenBridge` when cross-chain bridging is set to `'custom'` and upgradeability is enabled.
- For ERC-721, use namespaced storage for `_nextTokenId` when mintable, auto increment IDs, and upgradeability are enabled.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"**/typescript"
]
},
"resolutions": {
"ethereum-cryptography": "^3.2.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"concurrently": "^9.1.2",
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/ai/descriptions/solidity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const solidityCommonDescriptions = {
'The type of access control to provision. Ownable is a simple mechanism with a single account authorized for all privileged actions. Roles is a flexible mechanism with a separate role for each privileged action. A role can have many authorized accounts. Managed enables a central contract to define a policy that allows certain callers to access certain functions.',
upgradeable:
'Whether the smart contract is upgradeable. Transparent uses more complex proxy with higher overhead, requires less changes in your contract. Can also be used with beacons. UUPS uses simpler proxy with less overhead, requires including extra code in your contract. Allows flexibility for authorizing upgrades.',
namespacePrefix:
'The prefix for an ERC-7201 namespace identifier. It should be derived from the project name or a unique naming convention specific to the project. Used only if the contract includes storage variables and upgradeability is enabled. Default is "myProject".',
};

export const solidityERC20Descriptions = {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/solidity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"test:watch": "ava --watch",
"update-env": "rm ./src/environments/hardhat/package-lock.json && npm install --package-lock-only --prefix ./src/environments/hardhat && rm ./src/environments/hardhat/upgradeable/package-lock.json && npm install --package-lock-only --prefix ./src/environments/hardhat/upgradeable"
},
"dependencies": {
"ethereum-cryptography": "^3.2.0"
},
"devDependencies": {
"@openzeppelin/community-contracts": "git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#b0ddd27",
"@openzeppelin/contracts": "^5.4.0",
Expand Down
27 changes: 24 additions & 3 deletions packages/core/solidity/src/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,38 @@ test('contract with overridden function with code', t => {

test('contract with one variable', t => {
const Foo = new ContractBuilder('Foo');
Foo.addVariable('uint value = 42;');
Foo.addStateVariable('uint value = 42;', false);
t.snapshot(printContract(Foo));
});

test('contract with two variables', t => {
const Foo = new ContractBuilder('Foo');
Foo.addVariable('uint value = 42;');
Foo.addVariable('string name = "john";');
Foo.addStateVariable('uint value = 42;', false);
Foo.addStateVariable('string name = "john";', false);
t.snapshot(printContract(Foo));
});

test('contract with immutable', t => {
const Foo = new ContractBuilder('Foo');
Foo.addConstantOrImmutableOrErrorDefinition('uint immutable value = 42;');
t.snapshot(printContract(Foo));
});

test('contract with constant and comment', t => {
const Foo = new ContractBuilder('Foo');
Foo.addConstantOrImmutableOrErrorDefinition('uint constant value = 42;', ['// This is a comment']);
t.snapshot(printContract(Foo));
});

test('contract with variable and upgradeable - error', t => {
const Foo = new ContractBuilder('Foo');
const error = t.throws(() => Foo.addStateVariable('uint value = 42;', true));
t.is(
(error as Error).message,
'State variables should not be used when upgradeable is true. Set namespaced storage instead.',
);
});

test('name with special characters', t => {
const Foo = new ContractBuilder('foo bar baz');
t.snapshot(printContract(Foo));
Expand Down
27 changes: 27 additions & 0 deletions packages/core/solidity/src/contract.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,33 @@ Generated by [AVA](https://avajs.dev).
}␊
`

## contract with immutable

> Snapshot 1

`// SPDX-License-Identifier: MIT␊
// Compatible with OpenZeppelin Contracts ^5.4.0␊
pragma solidity ^0.8.27;␊
contract Foo {␊
uint immutable value = 42;␊
}␊
`

## contract with constant and comment

> Snapshot 1

`// SPDX-License-Identifier: MIT␊
// Compatible with OpenZeppelin Contracts ^5.4.0␊
pragma solidity ^0.8.27;␊
contract Foo {␊
// This is a comment␊
uint constant value = 42;␊
}␊
`

## name with special characters

> Snapshot 1
Expand Down
Binary file modified packages/core/solidity/src/contract.test.ts.snap
Binary file not shown.
73 changes: 64 additions & 9 deletions packages/core/solidity/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ export interface Contract {
natspecTags: NatspecTag[];
imports: ImportContract[];
functions: ContractFunction[];
structs: ContractStruct[];
constructorCode: string[];
constructorArgs: FunctionArgument[];
variables: string[];
variableOrErrorDefinitions: VariableOrErrorDefinition[];
upgradeable: boolean;
}

Expand Down Expand Up @@ -52,7 +53,13 @@ export interface ContractFunction extends BaseFunction {
comments: string[];
}

export type FunctionKind = 'internal' | 'public';
export interface ContractStruct {
name: string;
comments: string[];
variables: string[];
}

export type FunctionKind = 'internal' | 'public' | 'private';
export type FunctionMutability = (typeof mutabilityRank)[number];

// Order is important
Expand All @@ -72,6 +79,11 @@ export interface NatspecTag {
value: string;
}

export interface VariableOrErrorDefinition {
code: string;
comments?: string[];
}

export class ContractBuilder implements Contract {
readonly name: string;
license: string = 'MIT';
Expand All @@ -82,10 +94,11 @@ export class ContractBuilder implements Contract {

readonly constructorArgs: FunctionArgument[] = [];
readonly constructorCode: string[] = [];
readonly variableSet: Set<string> = new Set();

readonly variableOrErrorMap: Map<string, VariableOrErrorDefinition> = new Map<string, VariableOrErrorDefinition>();
private parentMap: Map<string, Parent> = new Map<string, Parent>();
private functionMap: Map<string, ContractFunction> = new Map();
private structMap: Map<string, ContractStruct> = new Map();

constructor(name: string) {
this.name = toIdentifier(name, true);
Expand Down Expand Up @@ -113,8 +126,12 @@ export class ContractBuilder implements Contract {
return [...this.functionMap.values()];
}

get variables(): string[] {
return [...this.variableSet];
get structs(): ContractStruct[] {
return [...this.structMap.values()];
}

get variableOrErrorDefinitions(): VariableOrErrorDefinition[] {
return [...this.variableOrErrorMap.values()];
}

addParent(contract: ImportContract, params: Value[] = []): boolean {
Expand Down Expand Up @@ -172,6 +189,19 @@ export class ContractBuilder implements Contract {
}
}

private addStruct(_struct: ContractStruct): ContractStruct {
const got = this.structMap.get(_struct.name);
if (got !== undefined) {
return got;
} else {
const struct: ContractStruct = {
..._struct,
};
this.structMap.set(_struct.name, struct);
return struct;
}
}

addConstructorArgument(arg: FunctionArgument) {
this.constructorArgs.push(arg);
}
Expand Down Expand Up @@ -212,11 +242,36 @@ export class ContractBuilder implements Contract {
}

/**
* Note: The type in the variable is not currently transpiled, even if it refers to a contract
* Note: The type in the code is not currently transpiled, even if it refers to a contract
*/
addStateVariable(code: string, upgradeable: boolean): boolean {
if (upgradeable) {
throw new Error('State variables should not be used when upgradeable is true. Set namespaced storage instead.');
} else {
return this._addVariableOrErrorDefinition({ code });
}
}

/**
* Note: The type in the code is not currently transpiled, even if it refers to a contract
*/
addVariable(code: string): boolean {
const present = this.variableSet.has(code);
this.variableSet.add(code);
addConstantOrImmutableOrErrorDefinition(code: string, comments?: string[]): boolean {
return this._addVariableOrErrorDefinition({ code, comments });
}

private _addVariableOrErrorDefinition(variableOrErrorDefinition: VariableOrErrorDefinition): boolean {
const present = this.variableOrErrorMap.has(variableOrErrorDefinition.code);
this.variableOrErrorMap.set(variableOrErrorDefinition.code, variableOrErrorDefinition);
return !present;
}

addStructVariable(baseStruct: ContractStruct, code: string): boolean {
let struct = this.structMap.get(baseStruct.name);
if (!struct) {
struct = this.addStruct(baseStruct);
}
const present = struct.variables.includes(code);
if (!present) struct.variables.push(code);
return !present;
}
}
4 changes: 1 addition & 3 deletions packages/core/solidity/src/erc1155.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,14 @@ export interface ERC1155Options extends CommonOptions {
}

export const defaults: Required<ERC1155Options> = {
...commonDefaults,
name: 'MyToken',
uri: '',
burnable: false,
pausable: false,
mintable: false,
supply: false,
updatableUri: true,
access: commonDefaults.access,
upgradeable: commonDefaults.upgradeable,
info: commonDefaults.info,
} as const;

function withDefaults(opts: ERC1155Options): Required<ERC1155Options> {
Expand Down
19 changes: 16 additions & 3 deletions packages/core/solidity/src/erc20.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,14 @@ Generated by [AVA](https://avajs.dev).
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";␊
contract MyToken is Initializable, ERC20Upgradeable, ERC20BridgeableUpgradeable, ERC20PermitUpgradeable {␊
address public tokenBridge;␊
/// @custom:storage-location erc7201:myProject.MyToken␊
struct MyTokenStorage {␊
address tokenBridge;␊
}␊
// keccak256(abi.encode(uint256(keccak256("myProject.MyToken")) - 1)) & ~bytes32(uint256(0xff))␊
bytes32 private constant MYTOKEN_STORAGE_LOCATION = 0xfbb7c9e4123fcf4b1aad53c70358f7b1c1d7cf28092f5178b53e55db565e9200;␊
error Unauthorized();␊
/// @custom:oz-upgrades-unsafe-allow constructor␊
Expand All @@ -877,11 +884,17 @@ Generated by [AVA](https://avajs.dev).
__ERC20Permit_init("MyToken");␊
require(tokenBridge_ != address(0), "Invalid tokenBridge_ address");␊
tokenBridge = tokenBridge_;␊
MyTokenStorage storage $ = _getMyTokenStorage();␊
$.tokenBridge = tokenBridge_;␊
}␊
function _checkTokenBridge(address caller) internal view override {␊
if (caller != tokenBridge) revert Unauthorized();␊
MyTokenStorage storage $ = _getMyTokenStorage();␊
if (caller != $.tokenBridge) revert Unauthorized();␊
}␊
function _getMyTokenStorage() private pure returns (MyTokenStorage storage $) {␊
assembly { $.slot := MYTOKEN_STORAGE_LOCATION }␊
}␊
}␊
`
Expand Down
Binary file modified packages/core/solidity/src/erc20.test.ts.snap
Binary file not shown.
Loading
Loading