Skip to content

Commit ce8fbba

Browse files
Replace ERC20Custodian with ERC20Freezable (#719)
Co-authored-by: Eric Lau <ericglau@outlook.com>
1 parent 5c1fa0f commit ce8fbba

File tree

15 files changed

+94
-87
lines changed

15 files changed

+94
-87
lines changed

.changeset/smart-fans-build.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@openzeppelin/wizard': patch
3+
'@openzeppelin/wizard-common': minor
4+
'@openzeppelin/contracts-mcp': patch
5+
---
6+
7+
**Breaking changes**: Solidity Stablecoin and RWA: Change `custodian` option to `freezable`. Replace ERC20Custodian with ERC20Freezable.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const solidityERC1155Descriptions = {
5454
};
5555

5656
export const solidityStablecoinDescriptions = {
57-
custodian:
57+
freezable:
5858
'Whether authorized accounts can freeze and unfreeze accounts for regulatory or security purposes. This feature is experimental, not audited and is subject to change.',
5959
restrictions:
6060
'Whether to restrict certain users from transferring tokens, either via allowing or blocking them. This feature is experimental, not audited and is subject to change.',

packages/core/solidity/src/generate/stablecoin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const erc20Full = {
4444

4545
const stablecoinExtensions = {
4646
restrictions: [false, 'allowlist', 'blocklist'] as const,
47-
custodian: booleans,
47+
freezable: booleans,
4848
upgradeable: [false] as const,
4949
};
5050

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ testStablecoin('stablecoin mintable with roles', {
7878
access: 'roles',
7979
});
8080

81+
testStablecoin('stablecoin mintable with role managed', {
82+
mintable: true,
83+
access: 'managed',
84+
});
85+
8186
testStablecoin('stablecoin callback', {
8287
callback: true,
8388
});
@@ -86,8 +91,8 @@ testStablecoin('stablecoin permit', {
8691
permit: true,
8792
});
8893

89-
testStablecoin('stablecoin custodian', {
90-
custodian: true,
94+
testStablecoin('stablecoin freezable', {
95+
freezable: true,
9196
});
9297

9398
testStablecoin('stablecoin allowlist', {
@@ -129,7 +134,7 @@ testStablecoin('stablecoin full', {
129134
crossChainBridging: 'custom',
130135
premintChainId: '10',
131136
restrictions: 'allowlist',
132-
custodian: true,
137+
freezable: true,
133138
});
134139

135140
testAPIEquivalence('stablecoin API default');
@@ -154,7 +159,7 @@ testAPIEquivalence('stablecoin API full', {
154159
crossChainBridging: 'custom',
155160
premintChainId: '10',
156161
restrictions: 'allowlist',
157-
custodian: true,
162+
freezable: true,
158163
});
159164

160165
test('stablecoin API assert defaults', async t => {

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

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,31 @@ Generated by [AVA](https://avajs.dev).
289289
}␊
290290
`
291291

292+
## stablecoin mintable with role managed
293+
294+
> Snapshot 1
295+
296+
`// SPDX-License-Identifier: MIT␊
297+
// Compatible with OpenZeppelin Contracts ^5.5.0␊
298+
pragma solidity ^0.8.27;␊
299+
300+
import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";␊
301+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
302+
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊
303+
304+
contract MyStablecoin is ERC20, AccessManaged, ERC20Permit {␊
305+
constructor(address initialAuthority)␊
306+
ERC20("MyStablecoin", "MST")␊
307+
AccessManaged(initialAuthority)␊
308+
ERC20Permit("MyStablecoin")␊
309+
{}␊
310+
311+
function mint(address to, uint256 amount) public restricted {␊
312+
_mint(to, amount);␊
313+
}␊
314+
}␊
315+
`
316+
292317
## stablecoin callback
293318

294319
> Snapshot 1
@@ -322,7 +347,7 @@ Generated by [AVA](https://avajs.dev).
322347
}␊
323348
`
324349

325-
## stablecoin custodian
350+
## stablecoin freezable
326351

327352
> Snapshot 1
328353
@@ -331,26 +356,26 @@ Generated by [AVA](https://avajs.dev).
331356
pragma solidity ^0.8.27;␊
332357
333358
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
334-
import {ERC20Custodian} from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Custodian.sol";␊
359+
import {ERC20Freezable} from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Freezable.sol";␊
335360
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊
336361
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊
337362
338-
contract MyStablecoin is ERC20, ERC20Permit, ERC20Custodian, Ownable {␊
363+
contract MyStablecoin is ERC20, ERC20Permit, ERC20Freezable, Ownable {␊
339364
constructor(address initialOwner)␊
340365
ERC20("MyStablecoin", "MST")␊
341366
ERC20Permit("MyStablecoin")␊
342367
Ownable(initialOwner)␊
343368
{}␊
344369
345-
function _isCustodian(address user) internal view override returns (bool) {␊
346-
return user == owner();␊
370+
function freeze(address user, uint256 amount) public onlyOwner {␊
371+
_setFrozen(user, amount);␊
347372
}␊
348373
349374
// The following functions are overrides required by Solidity.␊
350375
351376
function _update(address from, address to, uint256 value)␊
352377
internal␊
353-
override(ERC20, ERC20Custodian)␊
378+
override(ERC20, ERC20Freezable)␊
354379
{␊
355380
super._update(from, to, value);␊
356381
}␊
@@ -586,23 +611,23 @@ Generated by [AVA](https://avajs.dev).
586611
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
587612
import {ERC20Bridgeable} from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Bridgeable.sol";␊
588613
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";␊
589-
import {ERC20Custodian} from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Custodian.sol";␊
590614
import {ERC20FlashMint} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20FlashMint.sol";␊
615+
import {ERC20Freezable} from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Freezable.sol";␊
591616
import {ERC20Pausable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";␊
592617
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊
593618
import {ERC20Restricted} from "@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Restricted.sol";␊
594619
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";␊
595620
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";␊
596621
597-
contract MyStablecoin is ERC20, ERC20Bridgeable, AccessControl, ERC20Burnable, ERC20Pausable, ERC1363, ERC20Permit, ERC20Votes, ERC20FlashMint, ERC20Custodian, ERC20Restricted {␊
622+
contract MyStablecoin is ERC20, ERC20Bridgeable, AccessControl, ERC20Burnable, ERC20Pausable, ERC1363, ERC20Permit, ERC20Votes, ERC20FlashMint, ERC20Freezable, ERC20Restricted {␊
598623
bytes32 public constant TOKEN_BRIDGE_ROLE = keccak256("TOKEN_BRIDGE_ROLE");␊
599624
error Unauthorized();␊
600625
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");␊
601626
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");␊
602-
bytes32 public constant CUSTODIAN_ROLE = keccak256("CUSTODIAN_ROLE");␊
627+
bytes32 public constant FREEZER_ROLE = keccak256("FREEZER_ROLE");␊
603628
bytes32 public constant LIMITER_ROLE = keccak256("LIMITER_ROLE");␊
604629
605-
constructor(address defaultAdmin, address tokenBridge, address recipient, address pauser, address minter, address custodian, address limiter)␊
630+
constructor(address defaultAdmin, address tokenBridge, address recipient, address pauser, address minter, address freezer, address limiter)␊
606631
ERC20("MyStablecoin", "MST")␊
607632
ERC20Permit("MyStablecoin")␊
608633
{␊
@@ -613,7 +638,7 @@ Generated by [AVA](https://avajs.dev).
613638
}␊
614639
_grantRole(PAUSER_ROLE, pauser);␊
615640
_grantRole(MINTER_ROLE, minter);␊
616-
_grantRole(CUSTODIAN_ROLE, custodian);␊
641+
_grantRole(FREEZER_ROLE, freezer);␊
617642
_grantRole(LIMITER_ROLE, limiter);␊
618643
}␊
619644
@@ -633,8 +658,8 @@ Generated by [AVA](https://avajs.dev).
633658
_mint(to, amount);␊
634659
}␊
635660
636-
function _isCustodian(address user) internal view override returns (bool) {␊
637-
return hasRole(CUSTODIAN_ROLE, user);␊
661+
function freeze(address user, uint256 amount) public onlyRole(FREEZER_ROLE) {␊
662+
_setFrozen(user, amount);␊
638663
}␊
639664
640665
function isUserAllowed(address user) public view override returns (bool) {␊
@@ -653,7 +678,7 @@ Generated by [AVA](https://avajs.dev).
653678
654679
function _update(address from, address to, uint256 value)␊
655680
internal␊
656-
override(ERC20, ERC20Pausable, ERC20Votes, ERC20Custodian, ERC20Restricted)␊
681+
override(ERC20, ERC20Pausable, ERC20Votes, ERC20Freezable, ERC20Restricted)␊
657682
{␊
658683
super._update(from, to, value);␊
659684
}␊
36 Bytes
Binary file not shown.

packages/core/solidity/src/stablecoin.ts

Lines changed: 22 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Contract, ContractBuilder } from './contract';
22
import type { Access } from './set-access-control';
3-
import { setAccessControl, requireAccessControl } from './set-access-control';
3+
import { requireAccessControl } from './set-access-control';
44
import { defineFunctions } from './utils/define-functions';
55
import { printContract } from './print';
66
import type { ERC20Options } from './erc20';
@@ -13,15 +13,15 @@ import {
1313

1414
export interface StablecoinOptions extends ERC20Options {
1515
restrictions?: false | 'allowlist' | 'blocklist';
16-
custodian?: boolean;
16+
freezable?: boolean;
1717
}
1818

1919
export const defaults: Required<StablecoinOptions> = {
2020
...erc20defaults,
2121
name: 'MyStablecoin',
2222
symbol: 'MST',
2323
restrictions: false,
24-
custodian: false,
24+
freezable: false,
2525
} as const;
2626

2727
function withDefaults(opts: StablecoinOptions): Required<StablecoinOptions> {
@@ -30,7 +30,7 @@ function withDefaults(opts: StablecoinOptions): Required<StablecoinOptions> {
3030
name: opts.name ?? defaults.name,
3131
symbol: opts.symbol ?? defaults.symbol,
3232
restrictions: opts.restrictions ?? defaults.restrictions,
33-
custodian: opts.custodian ?? defaults.custodian,
33+
freezable: opts.freezable ?? defaults.freezable,
3434
};
3535
}
3636

@@ -39,7 +39,7 @@ export function printStablecoin(opts: StablecoinOptions = defaults): string {
3939
}
4040

4141
export function isAccessControlRequired(opts: Partial<StablecoinOptions>): boolean {
42-
return opts.mintable || opts.restrictions !== false || opts.custodian || opts.pausable || opts.upgradeable === 'uups';
42+
return opts.mintable || opts.restrictions !== false || opts.freezable || opts.pausable || opts.upgradeable === 'uups';
4343
}
4444

4545
export function buildStablecoin(opts: StablecoinOptions): Contract {
@@ -50,8 +50,8 @@ export function buildStablecoin(opts: StablecoinOptions): Contract {
5050

5151
const c = buildERC20(allOpts);
5252

53-
if (allOpts.custodian) {
54-
addCustodian(c, allOpts.access);
53+
if (allOpts.freezable) {
54+
addFreezable(c, allOpts.access);
5555
}
5656

5757
if (allOpts.restrictions) {
@@ -87,63 +87,33 @@ function addRestrictions(c: ContractBuilder, access: Access, mode: 'allowlist' |
8787
c.addFunctionCode(`_resetUser(user);`, removeFn);
8888
}
8989

90-
function addCustodian(c: ContractBuilder, access: Access) {
91-
const ERC20Custodian = {
92-
name: 'ERC20Custodian',
93-
path: '@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Custodian.sol',
90+
function addFreezable(c: ContractBuilder, access: Access) {
91+
const ERC20Freezable = {
92+
name: 'ERC20Freezable',
93+
path: '@openzeppelin/community-contracts/token/ERC20/extensions/ERC20Freezable.sol',
9494
};
9595

96-
c.addParent(ERC20Custodian);
97-
c.addOverride(ERC20Custodian, functions._update);
98-
c.addOverride(ERC20Custodian, functions._isCustodian);
96+
c.addParent(ERC20Freezable);
97+
c.addOverride(ERC20Freezable, functions._update);
9998

10099
if (access === false) {
101100
access = 'ownable';
102101
}
103102

104-
setAccessControl(c, access);
105-
106-
switch (access) {
107-
case 'ownable': {
108-
c.setFunctionBody([`return user == owner();`], functions._isCustodian);
109-
break;
110-
}
111-
case 'roles': {
112-
const roleOwner = 'custodian';
113-
const roleId = 'CUSTODIAN_ROLE';
114-
const addedConstant = c.addConstantOrImmutableOrErrorDefinition(
115-
`bytes32 public constant ${roleId} = keccak256("${roleId}");`,
116-
);
117-
if (roleOwner && addedConstant) {
118-
c.addConstructorArgument({ type: 'address', name: roleOwner });
119-
c.addConstructorCode(`_grantRole(${roleId}, ${roleOwner});`);
120-
}
121-
c.setFunctionBody([`return hasRole(CUSTODIAN_ROLE, user);`], functions._isCustodian);
122-
break;
123-
}
124-
case 'managed': {
125-
c.addImportOnly({
126-
name: 'AuthorityUtils',
127-
path: `@openzeppelin/contracts/access/manager/AuthorityUtils.sol`,
128-
});
129-
const logic = [
130-
`(bool immediate,) = AuthorityUtils.canCallWithDelay(authority(), user, address(this), bytes4(_msgData()[0:4]));`,
131-
`return immediate;`,
132-
];
133-
c.setFunctionBody(logic, functions._isCustodian);
134-
break;
135-
}
136-
}
103+
const freezeFn = functions.freeze;
104+
requireAccessControl(c, freezeFn, access, 'FREEZER', 'freezer');
105+
c.setFunctionBody([`_setFrozen(user, amount);`], freezeFn);
137106
}
138107

139108
const functions = {
140109
...erc20functions,
141110
...defineFunctions({
142-
_isCustodian: {
143-
kind: 'internal' as const,
144-
args: [{ name: 'user', type: 'address' }],
145-
returns: ['bool'],
146-
mutability: 'view' as const,
111+
freeze: {
112+
kind: 'public' as const,
113+
args: [
114+
{ name: 'user', type: 'address' },
115+
{ name: 'amount', type: 'uint256' },
116+
],
147117
},
148118

149119
allowUser: {

packages/mcp/src/solidity/schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const stablecoinSchema = {
108108
.or(z.literal('blocklist'))
109109
.optional()
110110
.describe(solidityStablecoinDescriptions.restrictions),
111-
custodian: z.boolean().optional().describe(solidityStablecoinDescriptions.custodian),
111+
freezable: z.boolean().optional().describe(solidityStablecoinDescriptions.freezable),
112112
} as const satisfies z.ZodRawShape;
113113

114114
export const rwaSchema = stablecoinSchema;

packages/mcp/src/solidity/tools/rwa.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ test('all', async t => {
5454
crossChainBridging: 'custom',
5555
premintChainId: '10',
5656
restrictions: 'allowlist',
57-
custodian: true,
57+
freezable: true,
5858
namespacePrefix: 'myProject',
5959
info: {
6060
license: 'MIT',

packages/mcp/src/solidity/tools/rwa.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function registerSolidityRWA(server: McpServer) {
2626
access,
2727
info,
2828
restrictions,
29-
custodian,
29+
freezable,
3030
}) => {
3131
const opts: StablecoinOptions = {
3232
name,
@@ -44,7 +44,7 @@ export function registerSolidityRWA(server: McpServer) {
4444
access,
4545
info,
4646
restrictions,
47-
custodian,
47+
freezable,
4848
};
4949
return {
5050
content: [

0 commit comments

Comments
 (0)