Skip to content

Commit 38da80c

Browse files
typicalHumanericglauernestognw
authored
Use namespaced storage for variables when upgradeable (#704)
Co-authored-by: Eric Lau <[email protected]> Co-authored-by: Ernesto García <[email protected]>
1 parent 0f0509d commit 38da80c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1171
-170
lines changed

.changeset/rude-nails-mate.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@openzeppelin/wizard': minor
3+
---
4+
5+
**Breaking changes**: Use namespaced storage instead of state variables when upgradeability is enabled.
6+
- For ERC-20, use namespaced storage for `tokenBridge` when cross-chain bridging is set to `'custom'` and upgradeability is enabled.
7+
- For ERC-721, use namespaced storage for `_nextTokenId` when mintable, auto increment IDs, and upgradeability are enabled.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
"**/typescript"
2929
]
3030
},
31+
"resolutions": {
32+
"ethereum-cryptography": "^3.2.0"
33+
},
3134
"devDependencies": {
3235
"@eslint/js": "^9.21.0",
3336
"concurrently": "^9.1.2",

packages/common/src/ai/descriptions/solidity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const solidityCommonDescriptions = {
1818
'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.',
1919
upgradeable:
2020
'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.',
21+
namespacePrefix:
22+
'The prefix for ERC-7201 namespace identifiers. 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".',
2123
};
2224

2325
export const solidityERC20Descriptions = {

packages/core/solidity/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
"test:watch": "ava --watch",
2222
"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"
2323
},
24+
"dependencies": {
25+
"ethereum-cryptography": "^3.2.0"
26+
},
2427
"devDependencies": {
2528
"@openzeppelin/community-contracts": "git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#b0ddd27",
2629
"@openzeppelin/contracts": "^5.4.0",

packages/core/solidity/src/contract.test.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,38 @@ test('contract with overridden function with code', t => {
135135

136136
test('contract with one variable', t => {
137137
const Foo = new ContractBuilder('Foo');
138-
Foo.addVariable('uint value = 42;');
138+
Foo.addStateVariable('uint value = 42;', false);
139139
t.snapshot(printContract(Foo));
140140
});
141141

142142
test('contract with two variables', t => {
143143
const Foo = new ContractBuilder('Foo');
144-
Foo.addVariable('uint value = 42;');
145-
Foo.addVariable('string name = "john";');
144+
Foo.addStateVariable('uint value = 42;', false);
145+
Foo.addStateVariable('string name = "john";', false);
146146
t.snapshot(printContract(Foo));
147147
});
148148

149+
test('contract with immutable', t => {
150+
const Foo = new ContractBuilder('Foo');
151+
Foo.addConstantOrImmutableOrErrorDefinition('uint immutable value = 42;');
152+
t.snapshot(printContract(Foo));
153+
});
154+
155+
test('contract with constant and comment', t => {
156+
const Foo = new ContractBuilder('Foo');
157+
Foo.addConstantOrImmutableOrErrorDefinition('uint constant value = 42;', ['// This is a comment']);
158+
t.snapshot(printContract(Foo));
159+
});
160+
161+
test('contract with variable and upgradeable - error', t => {
162+
const Foo = new ContractBuilder('Foo');
163+
const error = t.throws(() => Foo.addStateVariable('uint value = 42;', true));
164+
t.is(
165+
(error as Error).message,
166+
'State variables should not be used when upgradeable is true. Set namespaced storage instead.',
167+
);
168+
});
169+
149170
test('name with special characters', t => {
150171
const Foo = new ContractBuilder('foo bar baz');
151172
t.snapshot(printContract(Foo));

packages/core/solidity/src/contract.test.ts.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,33 @@ Generated by [AVA](https://avajs.dev).
262262
}␊
263263
`
264264

265+
## contract with immutable
266+
267+
> Snapshot 1
268+
269+
`// SPDX-License-Identifier: MIT␊
270+
// Compatible with OpenZeppelin Contracts ^5.4.0␊
271+
pragma solidity ^0.8.27;␊
272+
273+
contract Foo {␊
274+
uint immutable value = 42;␊
275+
}␊
276+
`
277+
278+
## contract with constant and comment
279+
280+
> Snapshot 1
281+
282+
`// SPDX-License-Identifier: MIT␊
283+
// Compatible with OpenZeppelin Contracts ^5.4.0␊
284+
pragma solidity ^0.8.27;␊
285+
286+
contract Foo {␊
287+
// This is a comment␊
288+
uint constant value = 42;␊
289+
}␊
290+
`
291+
265292
## name with special characters
266293

267294
> Snapshot 1
63 Bytes
Binary file not shown.

packages/core/solidity/src/contract.ts

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ export interface Contract {
77
natspecTags: NatspecTag[];
88
imports: ImportContract[];
99
functions: ContractFunction[];
10+
structs: ContractStruct[];
1011
constructorCode: string[];
1112
constructorArgs: FunctionArgument[];
12-
variables: string[];
13+
variableOrErrorDefinitions: VariableOrErrorDefinition[];
1314
upgradeable: boolean;
1415
}
1516

@@ -52,7 +53,13 @@ export interface ContractFunction extends BaseFunction {
5253
comments: string[];
5354
}
5455

55-
export type FunctionKind = 'internal' | 'public';
56+
export interface ContractStruct {
57+
name: string;
58+
comments: string[];
59+
variables: string[];
60+
}
61+
62+
export type FunctionKind = 'internal' | 'public' | 'private';
5663
export type FunctionMutability = (typeof mutabilityRank)[number];
5764

5865
// Order is important
@@ -72,6 +79,11 @@ export interface NatspecTag {
7279
value: string;
7380
}
7481

82+
export interface VariableOrErrorDefinition {
83+
code: string;
84+
comments?: string[];
85+
}
86+
7587
export class ContractBuilder implements Contract {
7688
readonly name: string;
7789
license: string = 'MIT';
@@ -82,10 +94,11 @@ export class ContractBuilder implements Contract {
8294

8395
readonly constructorArgs: FunctionArgument[] = [];
8496
readonly constructorCode: string[] = [];
85-
readonly variableSet: Set<string> = new Set();
8697

98+
readonly variableOrErrorMap: Map<string, VariableOrErrorDefinition> = new Map<string, VariableOrErrorDefinition>();
8799
private parentMap: Map<string, Parent> = new Map<string, Parent>();
88100
private functionMap: Map<string, ContractFunction> = new Map();
101+
private structMap: Map<string, ContractStruct> = new Map();
89102

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

116-
get variables(): string[] {
117-
return [...this.variableSet];
129+
get structs(): ContractStruct[] {
130+
return [...this.structMap.values()];
131+
}
132+
133+
get variableOrErrorDefinitions(): VariableOrErrorDefinition[] {
134+
return [...this.variableOrErrorMap.values()];
118135
}
119136

120137
addParent(contract: ImportContract, params: Value[] = []): boolean {
@@ -172,6 +189,19 @@ export class ContractBuilder implements Contract {
172189
}
173190
}
174191

192+
private addStruct(_struct: ContractStruct): ContractStruct {
193+
const got = this.structMap.get(_struct.name);
194+
if (got !== undefined) {
195+
return got;
196+
} else {
197+
const struct: ContractStruct = {
198+
..._struct,
199+
};
200+
this.structMap.set(_struct.name, struct);
201+
return struct;
202+
}
203+
}
204+
175205
addConstructorArgument(arg: FunctionArgument) {
176206
this.constructorArgs.push(arg);
177207
}
@@ -212,11 +242,36 @@ export class ContractBuilder implements Contract {
212242
}
213243

214244
/**
215-
* Note: The type in the variable is not currently transpiled, even if it refers to a contract
245+
* Note: The type in the code is not currently transpiled, even if it refers to a contract
246+
*/
247+
addStateVariable(code: string, upgradeable: boolean): boolean {
248+
if (upgradeable) {
249+
throw new Error('State variables should not be used when upgradeable is true. Set namespaced storage instead.');
250+
} else {
251+
return this._addVariableOrErrorDefinition({ code });
252+
}
253+
}
254+
255+
/**
256+
* Note: The type in the code is not currently transpiled, even if it refers to a contract
216257
*/
217-
addVariable(code: string): boolean {
218-
const present = this.variableSet.has(code);
219-
this.variableSet.add(code);
258+
addConstantOrImmutableOrErrorDefinition(code: string, comments?: string[]): boolean {
259+
return this._addVariableOrErrorDefinition({ code, comments });
260+
}
261+
262+
private _addVariableOrErrorDefinition(variableOrErrorDefinition: VariableOrErrorDefinition): boolean {
263+
const present = this.variableOrErrorMap.has(variableOrErrorDefinition.code);
264+
this.variableOrErrorMap.set(variableOrErrorDefinition.code, variableOrErrorDefinition);
265+
return !present;
266+
}
267+
268+
addStructVariable(baseStruct: ContractStruct, code: string): boolean {
269+
let struct = this.structMap.get(baseStruct.name);
270+
if (!struct) {
271+
struct = this.addStruct(baseStruct);
272+
}
273+
const present = struct.variables.includes(code);
274+
if (!present) struct.variables.push(code);
220275
return !present;
221276
}
222277
}

packages/core/solidity/src/erc1155.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,14 @@ export interface ERC1155Options extends CommonOptions {
2222
}
2323

2424
export const defaults: Required<ERC1155Options> = {
25+
...commonDefaults,
2526
name: 'MyToken',
2627
uri: '',
2728
burnable: false,
2829
pausable: false,
2930
mintable: false,
3031
supply: false,
3132
updatableUri: true,
32-
access: commonDefaults.access,
33-
upgradeable: commonDefaults.upgradeable,
34-
info: commonDefaults.info,
3533
} as const;
3634

3735
function withDefaults(opts: ERC1155Options): Required<ERC1155Options> {

packages/core/solidity/src/erc20.test.ts.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,14 @@ Generated by [AVA](https://avajs.dev).
863863
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";␊
864864
865865
contract MyToken is Initializable, ERC20Upgradeable, ERC20BridgeableUpgradeable, ERC20PermitUpgradeable {␊
866-
address public tokenBridge;␊
866+
/// @custom:storage-location erc7201:myProject.MyToken␊
867+
struct MyTokenStorage {␊
868+
address tokenBridge;␊
869+
}␊
870+
871+
// keccak256(abi.encode(uint256(keccak256("myProject.MyToken")) - 1)) & ~bytes32(uint256(0xff))␊
872+
bytes32 private constant MYTOKEN_STORAGE_LOCATION = 0xfbb7c9e4123fcf4b1aad53c70358f7b1c1d7cf28092f5178b53e55db565e9200;␊
873+
867874
error Unauthorized();␊
868875
869876
/// @custom:oz-upgrades-unsafe-allow constructor␊
@@ -877,11 +884,17 @@ Generated by [AVA](https://avajs.dev).
877884
__ERC20Permit_init("MyToken");␊
878885
879886
require(tokenBridge_ != address(0), "Invalid tokenBridge_ address");␊
880-
tokenBridge = tokenBridge_;␊
887+
MyTokenStorage storage $ = _getMyTokenStorage();␊
888+
$.tokenBridge = tokenBridge_;␊
881889
}␊
882890
883891
function _checkTokenBridge(address caller) internal view override {␊
884-
if (caller != tokenBridge) revert Unauthorized();␊
892+
MyTokenStorage storage $ = _getMyTokenStorage();␊
893+
if (caller != $.tokenBridge) revert Unauthorized();␊
894+
}␊
895+
896+
function _getMyTokenStorage() private pure returns (MyTokenStorage storage $) {␊
897+
assembly { $.slot := MYTOKEN_STORAGE_LOCATION }␊
885898
}␊
886899
}␊
887900
`

0 commit comments

Comments
 (0)