diff --git a/.changeset/some-fans-tell.md b/.changeset/some-fans-tell.md new file mode 100644 index 000000000..283fac3cb --- /dev/null +++ b/.changeset/some-fans-tell.md @@ -0,0 +1,7 @@ +--- +'@openzeppelin/wizard': patch +'@openzeppelin/wizard-common': patch +'@openzeppelin/wizard-mcp': patch +--- + +Add option for Superchain interop message passing in Custom contracts diff --git a/packages/common/src/ai/descriptions/solidity.ts b/packages/common/src/ai/descriptions/solidity.ts index f6438fdd3..56155775d 100644 --- a/packages/common/src/ai/descriptions/solidity.ts +++ b/packages/common/src/ai/descriptions/solidity.ts @@ -95,3 +95,9 @@ export const solidityGovernorDescriptions = { storage: 'Enable storage of proposal details and enumerability of proposals', settings: 'Allow governance to update voting settings (delay, period, proposal threshold)', }; + +export const solidityCustomDescriptions = { + crossChainMessaging: 'Whether to add an example for Superchain interop message passing', + crossChainFunctionName: + 'The name of a custom function that will be callable from another chain, default is "myFunction"', +}; diff --git a/packages/core/solidity/package.json b/packages/core/solidity/package.json index 804075fab..3c3e56632 100644 --- a/packages/core/solidity/package.json +++ b/packages/core/solidity/package.json @@ -25,6 +25,7 @@ "@openzeppelin/community-contracts": "https://github.com/OpenZeppelin/openzeppelin-community-contracts", "@openzeppelin/contracts": "^5.3.0", "@openzeppelin/contracts-upgradeable": "^5.3.0", + "@eth-optimism/contracts-bedrock": "^0.17.3", "@types/node": "^20.0.0", "@types/semver": "^7.5.7", "ava": "^6.0.0", diff --git a/packages/core/solidity/src/add-superchain-messaging.ts b/packages/core/solidity/src/add-superchain-messaging.ts new file mode 100644 index 000000000..89225a3c0 --- /dev/null +++ b/packages/core/solidity/src/add-superchain-messaging.ts @@ -0,0 +1,114 @@ +import type { ContractBuilder } from './contract'; +import { type BaseFunction } from './contract'; +import { OptionsError } from './error'; +import type { Access } from './set-access-control'; +import { requireAccessControl } from './set-access-control'; +import { toIdentifier } from './utils/to-identifier'; + +export function addSuperchainMessaging(c: ContractBuilder, functionName: string, access: Access, pausable: boolean) { + const sanitizedFunctionName = safeSanitizeFunctionName(functionName); + + addCustomErrors(c); + addCrossDomainMessengerImmutable(c); + addOnlyCrossDomainCallbackModifier(c); + addSourceFunction(sanitizedFunctionName, access, c, pausable); + addDestinationFunction(sanitizedFunctionName, c, pausable); +} + +function safeSanitizeFunctionName(functionName: string) { + const sanitizedFunctionName = toIdentifier(functionName, false); + if (sanitizedFunctionName.length === 0) { + throw new OptionsError({ + crossChainFunctionName: 'Not a valid function name', + }); + } + return sanitizedFunctionName; +} + +function addCustomErrors(c: ContractBuilder) { + c.addCustomError('CallerNotL2ToL2CrossDomainMessenger'); + c.addCustomError('InvalidCrossDomainSender'); + c.addCustomError('InvalidDestination'); +} + +function addCrossDomainMessengerImmutable(c: ContractBuilder) { + c.addImportOnly({ + name: 'IL2ToL2CrossDomainMessenger', + path: '@eth-optimism/contracts-bedrock/src/L2/IL2ToL2CrossDomainMessenger.sol', + transpiled: false, + }); + c.addImportOnly({ + name: 'Predeploys', + path: '@eth-optimism/contracts-bedrock/src/libraries/Predeploys.sol', + transpiled: false, + }); + c.addVariable( + 'IL2ToL2CrossDomainMessenger public immutable messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);', + ); +} + +function addOnlyCrossDomainCallbackModifier(c: ContractBuilder) { + c.addModifierDefinition({ + name: 'onlyCrossDomainCallback', + code: [ + 'if (msg.sender != address(messenger)) revert CallerNotL2ToL2CrossDomainMessenger();', + 'if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender();', + '_;', + ], + }); +} + +function addSourceFunction(sanitizedFunctionName: string, access: Access, c: ContractBuilder, pausable: boolean) { + const sourceFn: BaseFunction = { + name: `call${sanitizedFunctionName.replace(/^(.)/, c => c.toUpperCase())}`, + kind: 'public' as const, + args: [], + }; + + if (access) { + requireAccessControl(c, sourceFn, access, 'CROSSCHAIN_CALLER', 'crossChainCaller'); + } else { + c.setFunctionComments(['// NOTE: Anyone can call this function'], sourceFn); + } + + if (pausable) { + c.addModifier('whenNotPaused', sourceFn); + } + + c.setFunctionBody( + [ + 'if (_toChainId == block.chainid) revert InvalidDestination();', + `messenger.sendMessage(_toChainId, address(this), abi.encodeCall(this.${sanitizedFunctionName}, (/* TODO: Add arguments */)));`, + ], + sourceFn, + ); +} + +function addDestinationFunction(sanitizedFunctionName: string, c: ContractBuilder, pausable: boolean) { + const destFn: BaseFunction = { + name: sanitizedFunctionName, + kind: 'external' as const, + args: [], + argInlineComment: 'TODO: Add arguments', + }; + c.setFunctionComments( + [ + '/**', + ' * @dev IMPORTANT: You must either design the deployer to allow only a specific trusted contract,', + ' * such as this contract, to be deployed through it, or use CREATE2 from a deployer contract', + ' * that is itself deployed by an EOA you control.', + ' * This precaution is critical because if an unauthorized contract is deployed at the same', + ' * address on any Superchain network, it could allow malicious actors to invoke your function', + ' * from another chain.', + ' */', + ], + destFn, + ); + + c.addModifier('onlyCrossDomainCallback', destFn); + if (pausable) { + c.addModifier('whenNotPaused', destFn); + } + + c.addFunctionCode('// TODO: Implement logic for the function that will be called from another chain', destFn); +} diff --git a/packages/core/solidity/src/contract.ts b/packages/core/solidity/src/contract.ts index 6ed276124..9fa6c666b 100644 --- a/packages/core/solidity/src/contract.ts +++ b/packages/core/solidity/src/contract.ts @@ -11,6 +11,12 @@ export interface Contract { constructorArgs: FunctionArgument[]; variables: string[]; upgradeable: boolean; + customErrors: CustomError[]; + modifierDefinitions: ModifierDefinition[]; +} + +export interface CustomError { + name: string; } export type Value = string | number | { lit: string } | { note: string; value: Value }; @@ -35,9 +41,15 @@ export interface Using { usingFor: string; } +export interface ModifierDefinition { + name: string; + code: string[]; +} + export interface BaseFunction { name: string; args: FunctionArgument[]; + argInlineComment?: string; returns?: string[]; kind: FunctionKind; mutability?: FunctionMutability; @@ -52,7 +64,7 @@ export interface ContractFunction extends BaseFunction { comments: string[]; } -export type FunctionKind = 'internal' | 'public'; +export type FunctionKind = 'internal' | 'public' | 'external'; export type FunctionMutability = (typeof mutabilityRank)[number]; // Order is important @@ -83,9 +95,11 @@ export class ContractBuilder implements Contract { readonly constructorArgs: FunctionArgument[] = []; readonly constructorCode: string[] = []; readonly variableSet: Set = new Set(); + readonly customErrorSet: Set = new Set(); private parentMap: Map = new Map(); private functionMap: Map = new Map(); + private modifierDefinitionsMap: Map = new Map(); constructor(name: string) { this.name = toIdentifier(name, true); @@ -117,6 +131,14 @@ export class ContractBuilder implements Contract { return [...this.variableSet]; } + get customErrors(): CustomError[] { + return [...this.customErrorSet].map(name => ({ name })); + } + + get modifierDefinitions(): ModifierDefinition[] { + return [...this.modifierDefinitionsMap.values()]; + } + addParent(contract: ImportContract, params: Value[] = []): boolean { const present = this.parentMap.has(contract.name); this.parentMap.set(contract.name, { contract, params }); @@ -219,4 +241,16 @@ export class ContractBuilder implements Contract { this.variableSet.add(code); return !present; } + + addCustomError(name: string): boolean { + const present = this.customErrorSet.has(name); + this.customErrorSet.add(name); + return !present; + } + + addModifierDefinition(modifier: ModifierDefinition): boolean { + const present = this.modifierDefinitionsMap.has(modifier.name); + this.modifierDefinitionsMap.set(modifier.name, modifier); + return !present; + } } diff --git a/packages/core/solidity/src/custom.test.ts b/packages/core/solidity/src/custom.test.ts index 6262bf26a..b760a17b1 100644 --- a/packages/core/solidity/src/custom.test.ts +++ b/packages/core/solidity/src/custom.test.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import type { OptionsError } from '.'; import { custom } from '.'; import type { CustomOptions } from './custom'; @@ -66,6 +67,27 @@ testCustom('access control managed', { access: 'managed', }); +testCustom('superchain messaging', { + crossChainMessaging: 'superchain', +}); + +testCustom('superchain messaging ownable pausable', { + crossChainMessaging: 'superchain', + access: 'ownable', + pausable: true, +}); + +test('superchain messaging, invalid function name', async t => { + const error = t.throws(() => + buildCustom({ + name: 'MyContract', + crossChainMessaging: 'superchain', + crossChainFunctionName: ' ', + }), + ); + t.is((error as OptionsError).messages.crossChainFunctionName, 'Not a valid function name'); +}); + testCustom('upgradeable uups with access control disabled', { // API should override access to true since it is required for UUPS access: false, @@ -81,6 +103,8 @@ testAPIEquivalence('custom API full upgradeable', { access: 'roles', pausable: true, upgradeable: 'uups', + crossChainMessaging: 'superchain', + crossChainFunctionName: 'myCustomFunction', }); testAPIEquivalence('custom API full upgradeable with managed', { @@ -88,6 +112,8 @@ testAPIEquivalence('custom API full upgradeable with managed', { access: 'managed', pausable: true, upgradeable: 'uups', + crossChainMessaging: 'superchain', + crossChainFunctionName: 'myCustomFunction', }); test('custom API assert defaults', async t => { diff --git a/packages/core/solidity/src/custom.test.ts.md b/packages/core/solidity/src/custom.test.ts.md index e23c2db14..05abe88bc 100644 --- a/packages/core/solidity/src/custom.test.ts.md +++ b/packages/core/solidity/src/custom.test.ts.md @@ -160,6 +160,105 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## superchain messaging + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.27;␊ + ␊ + import {IL2ToL2CrossDomainMessenger} from "@eth-optimism/contracts-bedrock/src/L2/IL2ToL2CrossDomainMessenger.sol";␊ + import {Predeploys} from "@eth-optimism/contracts-bedrock/src/libraries/Predeploys.sol";␊ + ␊ + contract MyContract {␊ + IL2ToL2CrossDomainMessenger public immutable messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);␊ + ␊ + error CallerNotL2ToL2CrossDomainMessenger();␊ + error InvalidCrossDomainSender();␊ + error InvalidDestination();␊ + ␊ + modifier onlyCrossDomainCallback() {␊ + if (msg.sender != address(messenger)) revert CallerNotL2ToL2CrossDomainMessenger();␊ + if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender();␊ + _;␊ + }␊ + ␊ + // NOTE: Anyone can call this function␊ + function callMyFunction() public {␊ + if (_toChainId == block.chainid) revert InvalidDestination();␊ + messenger.sendMessage(_toChainId, address(this), abi.encodeCall(this.myFunction, (/* TODO: Add arguments */)));␊ + }␊ + ␊ + /**␊ + * @dev IMPORTANT: You must either design the deployer to allow only a specific trusted contract,␊ + * such as this contract, to be deployed through it, or use CREATE2 from a deployer contract␊ + * that is itself deployed by an EOA you control.␊ + * This precaution is critical because if an unauthorized contract is deployed at the same␊ + * address on any Superchain network, it could allow malicious actors to invoke your function␊ + * from another chain.␊ + */␊ + function myFunction(/* TODO: Add arguments */) external onlyCrossDomainCallback {␊ + // TODO: Implement logic for the function that will be called from another chain␊ + }␊ + }␊ + ` + +## superchain messaging ownable pausable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.0.0␊ + pragma solidity ^0.8.27;␊ + ␊ + import {IL2ToL2CrossDomainMessenger} from "@eth-optimism/contracts-bedrock/src/L2/IL2ToL2CrossDomainMessenger.sol";␊ + import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊ + import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";␊ + import {Predeploys} from "@eth-optimism/contracts-bedrock/src/libraries/Predeploys.sol";␊ + ␊ + contract MyContract is Ownable, Pausable {␊ + IL2ToL2CrossDomainMessenger public immutable messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);␊ + ␊ + error CallerNotL2ToL2CrossDomainMessenger();␊ + error InvalidCrossDomainSender();␊ + error InvalidDestination();␊ + ␊ + modifier onlyCrossDomainCallback() {␊ + if (msg.sender != address(messenger)) revert CallerNotL2ToL2CrossDomainMessenger();␊ + if (messenger.crossDomainMessageSender() != address(this)) revert InvalidCrossDomainSender();␊ + _;␊ + }␊ + ␊ + constructor(address initialOwner) Ownable(initialOwner) {}␊ + ␊ + function callMyFunction() public onlyOwner whenNotPaused {␊ + if (_toChainId == block.chainid) revert InvalidDestination();␊ + messenger.sendMessage(_toChainId, address(this), abi.encodeCall(this.myFunction, (/* TODO: Add arguments */)));␊ + }␊ + ␊ + /**␊ + * @dev IMPORTANT: You must either design the deployer to allow only a specific trusted contract,␊ + * such as this contract, to be deployed through it, or use CREATE2 from a deployer contract␊ + * that is itself deployed by an EOA you control.␊ + * This precaution is critical because if an unauthorized contract is deployed at the same␊ + * address on any Superchain network, it could allow malicious actors to invoke your function␊ + * from another chain.␊ + */␊ + function myFunction(/* TODO: Add arguments */) external onlyCrossDomainCallback whenNotPaused {␊ + // TODO: Implement logic for the function that will be called from another chain␊ + }␊ + ␊ + function pause() public onlyOwner {␊ + _pause();␊ + }␊ + ␊ + function unpause() public onlyOwner {␊ + _unpause();␊ + }␊ + }␊ + ` + ## upgradeable uups with access control disabled > Snapshot 1 diff --git a/packages/core/solidity/src/custom.test.ts.snap b/packages/core/solidity/src/custom.test.ts.snap index 62f68ccd2..41b638e40 100644 Binary files a/packages/core/solidity/src/custom.test.ts.snap and b/packages/core/solidity/src/custom.test.ts.snap differ diff --git a/packages/core/solidity/src/custom.ts b/packages/core/solidity/src/custom.ts index 5baaf31b5..43a317e05 100644 --- a/packages/core/solidity/src/custom.ts +++ b/packages/core/solidity/src/custom.ts @@ -7,14 +7,22 @@ import { setInfo } from './set-info'; import { setAccessControl } from './set-access-control'; import { addPausable } from './add-pausable'; import { printContract } from './print'; +import { addSuperchainMessaging } from './add-superchain-messaging'; + +export const CrossChainMessagingOptions = [false, 'superchain'] as const; +export type CrossChainMessaging = (typeof CrossChainMessagingOptions)[number]; export interface CustomOptions extends CommonOptions { name: string; + crossChainMessaging?: CrossChainMessaging; + crossChainFunctionName?: string; pausable?: boolean; } export const defaults: Required = { name: 'MyContract', + crossChainMessaging: false, + crossChainFunctionName: 'myFunction', pausable: false, access: commonDefaults.access, upgradeable: commonDefaults.upgradeable, @@ -25,6 +33,8 @@ function withDefaults(opts: CustomOptions): Required { return { ...opts, ...withCommonDefaults(opts), + crossChainMessaging: opts.crossChainMessaging ?? defaults.crossChainMessaging, + crossChainFunctionName: opts.crossChainFunctionName ?? defaults.crossChainFunctionName, pausable: opts.pausable ?? defaults.pausable, }; } @@ -44,6 +54,10 @@ export function buildCustom(opts: CustomOptions): Contract { const { access, upgradeable, info } = allOpts; + if (allOpts.crossChainMessaging === 'superchain') { + addSuperchainMessaging(c, allOpts.crossChainFunctionName, allOpts.access, allOpts.pausable); + } + if (allOpts.pausable) { addPausable(c, access, []); } diff --git a/packages/core/solidity/src/erc20.test.ts.md b/packages/core/solidity/src/erc20.test.ts.md index d144f5651..58f2ee030 100644 --- a/packages/core/solidity/src/erc20.test.ts.md +++ b/packages/core/solidity/src/erc20.test.ts.md @@ -549,6 +549,7 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit {␊ address public immutable TOKEN_BRIDGE;␊ + ␊ error Unauthorized();␊ ␊ constructor(address tokenBridge)␊ @@ -580,6 +581,7 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit, Ownable {␊ address public immutable TOKEN_BRIDGE;␊ + ␊ error Unauthorized();␊ ␊ constructor(address tokenBridge, address initialOwner)␊ @@ -613,6 +615,7 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, ERC20Burnable, Ownable, ERC20Permit {␊ address public immutable TOKEN_BRIDGE;␊ + ␊ error Unauthorized();␊ ␊ constructor(address tokenBridge, address initialOwner)␊ @@ -649,6 +652,7 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, AccessControl, ERC20Permit {␊ bytes32 public constant TOKEN_BRIDGE_ROLE = keccak256("TOKEN_BRIDGE_ROLE");␊ + ␊ error Unauthorized();␊ ␊ constructor(address defaultAdmin, address tokenBridge)␊ @@ -720,6 +724,7 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit {␊ address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;␊ + ␊ error Unauthorized();␊ ␊ constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {}␊ @@ -750,6 +755,7 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit, Ownable {␊ address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;␊ + ␊ error Unauthorized();␊ ␊ constructor(address initialOwner)␊ @@ -784,6 +790,7 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit, AccessControl {␊ address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;␊ + ␊ error Unauthorized();␊ ␊ constructor(address defaultAdmin)␊ @@ -830,6 +837,7 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit, AccessManaged {␊ address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;␊ + ␊ error Unauthorized();␊ ␊ constructor(address initialAuthority)␊ @@ -884,6 +892,7 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit {␊ address public immutable TOKEN_BRIDGE;␊ + ␊ error Unauthorized();␊ ␊ constructor(address tokenBridge, address recipient)␊ @@ -917,6 +926,7 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, ERC20Permit {␊ address internal constant SUPERCHAIN_TOKEN_BRIDGE = 0x4200000000000000000000000000000000000028;␊ + ␊ error Unauthorized();␊ ␊ constructor(address recipient)␊ @@ -960,9 +970,10 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyToken is ERC20, ERC20Bridgeable, AccessControl, ERC20Burnable, ERC20Pausable, ERC1363, ERC20Permit, ERC20Votes, ERC20FlashMint {␊ bytes32 public constant TOKEN_BRIDGE_ROLE = keccak256("TOKEN_BRIDGE_ROLE");␊ - error Unauthorized();␊ bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");␊ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");␊ + ␊ + error Unauthorized();␊ ␊ constructor(address defaultAdmin, address tokenBridge, address recipient, address pauser, address minter)␊ ERC20("MyToken", "MTK")␊ diff --git a/packages/core/solidity/src/erc20.test.ts.snap b/packages/core/solidity/src/erc20.test.ts.snap index 33e9034f9..121c3e47d 100644 Binary files a/packages/core/solidity/src/erc20.test.ts.snap and b/packages/core/solidity/src/erc20.test.ts.snap differ diff --git a/packages/core/solidity/src/erc20.ts b/packages/core/solidity/src/erc20.ts index e9abf7525..509ebaac7 100644 --- a/packages/core/solidity/src/erc20.ts +++ b/packages/core/solidity/src/erc20.ts @@ -338,7 +338,7 @@ function addCrossChainBridging( throw new Error('Unknown value for `crossChainBridging`'); } } - c.addVariable('error Unauthorized();'); + c.addCustomError('Unauthorized'); } function addCustomBridging(c: ContractBuilder, access: Access) { diff --git a/packages/core/solidity/src/generate/custom.ts b/packages/core/solidity/src/generate/custom.ts index 40207666a..54230a9ca 100644 --- a/packages/core/solidity/src/generate/custom.ts +++ b/packages/core/solidity/src/generate/custom.ts @@ -1,4 +1,4 @@ -import type { CustomOptions } from '../custom'; +import { CrossChainMessagingOptions, type CustomOptions } from '../custom'; import { accessOptions } from '../set-access-control'; import { infoOptions } from '../set-info'; import { upgradeableOptions } from '../set-upgradeable'; @@ -8,6 +8,8 @@ const booleans = [true, false]; const blueprint = { name: ['MyContract'], + crossChainMessaging: CrossChainMessagingOptions, + crossChainFunctionName: ['myFunction'], pausable: booleans, access: accessOptions, upgradeable: upgradeableOptions, diff --git a/packages/core/solidity/src/print.ts b/packages/core/solidity/src/print.ts index c9e85615a..033ca97f7 100644 --- a/packages/core/solidity/src/print.ts +++ b/packages/core/solidity/src/print.ts @@ -6,6 +6,8 @@ import type { Value, NatspecTag, ImportContract, + CustomError, + ModifierDefinition, } from './contract'; import type { Options, Helpers } from './options'; import { withHelpers } from './options'; @@ -41,6 +43,8 @@ export function printContract(contract: Contract, opts?: Options): string { spaceBetween( contract.variables, + printCustomErrors(contract.customErrors), + printModifierDefinitions(contract.modifierDefinitions), printConstructor(contract, helpers), ...fns.code, ...fns.modifiers, @@ -54,6 +58,14 @@ export function printContract(contract: Contract, opts?: Options): string { ); } +function printCustomErrors(errors: CustomError[]): Lines[] { + return errors.map(e => `error ${e.name}();`); +} + +function printModifierDefinitions(modifierDefinitions: ModifierDefinition[]): Lines[] { + return modifierDefinitions.flatMap(def => [`modifier ${def.name}() {`, def.code, '}']); +} + function printInheritance(contract: Contract, { transformName }: Helpers): [] | [string] { if (contract.parents.length > 0) { return ['is ' + contract.parents.map(p => transformName(p.contract)).join(', ')]; @@ -77,7 +89,7 @@ function printConstructor(contract: Contract, helpers: Helpers): Lines[] { ) : contract.constructorCode; const head = helpers.upgradeable ? 'function initialize' : 'constructor'; - const constructor = printFunction2([], head, args, modifiers, body); + const constructor = printFunction2([], head, args, undefined, modifiers, body); if (!helpers.upgradeable) { return constructor; } else { @@ -189,6 +201,7 @@ function printFunction(fn: ContractFunction, helpers: Helpers): Lines[] { fn.comments, 'function ' + fn.name, fn.args.map(a => printArgument(a, helpers)), + fn.argInlineComment, modifiers, code, ); @@ -203,6 +216,7 @@ function printFunction2( comments: string[], kindedName: string, args: string[], + argInlineComment: string | undefined, modifiers: string[], code: Lines[], ): Lines[] { @@ -213,9 +227,11 @@ function printFunction2( const braces = code.length > 0 ? '{' : '{}'; if (headingLength <= 72) { - fn.push([`${kindedName}(${args.join(', ')})`, ...modifiers, braces].join(' ')); + fn.push( + [`${kindedName}(${args.join(', ')}${formatInlineComment(argInlineComment)})`, ...modifiers, braces].join(' '), + ); } else { - fn.push(`${kindedName}(${args.join(', ')})`, modifiers, braces); + fn.push(`${kindedName}(${args.join(', ')}${formatInlineComment(argInlineComment)})`, modifiers, braces); } if (code.length > 0) { @@ -225,6 +241,10 @@ function printFunction2( return fn; } +function formatInlineComment(comment: string | undefined): string { + return comment ? `/* ${comment} */` : ''; +} + function printArgument(arg: FunctionArgument, { transformName }: Helpers): string { let type: string; if (typeof arg.type === 'string') { diff --git a/packages/core/solidity/src/stablecoin.test.ts.md b/packages/core/solidity/src/stablecoin.test.ts.md index d0b292ea5..af2d36deb 100644 --- a/packages/core/solidity/src/stablecoin.test.ts.md +++ b/packages/core/solidity/src/stablecoin.test.ts.md @@ -606,11 +606,12 @@ Generated by [AVA](https://avajs.dev). ␊ contract MyStablecoin is ERC20, ERC20Bridgeable, AccessControl, ERC20Burnable, ERC20Pausable, ERC1363, ERC20Permit, ERC20Votes, ERC20FlashMint, ERC20Custodian, ERC20Allowlist {␊ bytes32 public constant TOKEN_BRIDGE_ROLE = keccak256("TOKEN_BRIDGE_ROLE");␊ - error Unauthorized();␊ bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");␊ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");␊ bytes32 public constant CUSTODIAN_ROLE = keccak256("CUSTODIAN_ROLE");␊ bytes32 public constant LIMITER_ROLE = keccak256("LIMITER_ROLE");␊ + ␊ + error Unauthorized();␊ ␊ constructor(address defaultAdmin, address tokenBridge, address recipient, address pauser, address minter, address custodian, address limiter)␊ ERC20("MyStablecoin", "MST")␊ diff --git a/packages/core/solidity/src/stablecoin.test.ts.snap b/packages/core/solidity/src/stablecoin.test.ts.snap index 02308f8db..d9161c1ef 100644 Binary files a/packages/core/solidity/src/stablecoin.test.ts.snap and b/packages/core/solidity/src/stablecoin.test.ts.snap differ diff --git a/packages/mcp/src/solidity/schemas.ts b/packages/mcp/src/solidity/schemas.ts index 617f2fb25..e9b9935d2 100644 --- a/packages/mcp/src/solidity/schemas.ts +++ b/packages/mcp/src/solidity/schemas.ts @@ -9,6 +9,7 @@ import { solidityGovernorDescriptions, solidityAccountDescriptions, solidityStablecoinDescriptions, + solidityCustomDescriptions, } from '@openzeppelin/wizard-common'; import type { KindedOptions } from '@openzeppelin/wizard'; @@ -176,5 +177,11 @@ export const governorSchema = { export const customSchema = { name: z.string().describe(commonDescriptions.name), pausable: z.boolean().optional().describe(commonDescriptions.pausable), + crossChainMessaging: z + .literal('superchain') + .or(z.literal(false)) + .optional() + .describe(solidityCustomDescriptions.crossChainMessaging), + crossChainFunctionName: z.string().optional().describe(solidityCustomDescriptions.crossChainFunctionName), ...commonSchema, } as const satisfies z.ZodRawShape; diff --git a/packages/mcp/src/solidity/tools/custom.test.ts b/packages/mcp/src/solidity/tools/custom.test.ts index d2e2baf6e..2483d90bc 100644 --- a/packages/mcp/src/solidity/tools/custom.test.ts +++ b/packages/mcp/src/solidity/tools/custom.test.ts @@ -43,6 +43,8 @@ test('all', async t => { pausable: true, access: 'roles', upgradeable: 'uups', + crossChainMessaging: 'superchain', + crossChainFunctionName: 'myCustomFunction', info: { license: 'MIT', securityContact: 'security@example.com', diff --git a/packages/mcp/src/solidity/tools/custom.ts b/packages/mcp/src/solidity/tools/custom.ts index cd9bba4f2..ddda8bdd0 100644 --- a/packages/mcp/src/solidity/tools/custom.ts +++ b/packages/mcp/src/solidity/tools/custom.ts @@ -10,10 +10,12 @@ export function registerSolidityCustom(server: McpServer): RegisteredTool { 'solidity-custom', makeDetailedPrompt(solidityPrompts.Custom), customSchema, - async ({ name, pausable, access, upgradeable, info }) => { + async ({ name, pausable, crossChainMessaging, crossChainFunctionName, access, upgradeable, info }) => { const opts: CustomOptions = { name, pausable, + crossChainMessaging, + crossChainFunctionName, access, upgradeable, info, diff --git a/packages/ui/api/ai-assistant/function-definitions/solidity.ts b/packages/ui/api/ai-assistant/function-definitions/solidity.ts index 00df94474..207177285 100644 --- a/packages/ui/api/ai-assistant/function-definitions/solidity.ts +++ b/packages/ui/api/ai-assistant/function-definitions/solidity.ts @@ -9,6 +9,7 @@ import { solidityERC1155Descriptions, solidityStablecoinDescriptions, solidityGovernorDescriptions, + solidityCustomDescriptions, } from '../../../../common/src/ai/descriptions/solidity.ts'; export const solidityERC20AIFunctionDefinition = { @@ -315,13 +316,20 @@ export const solidityCustomAIFunctionDefinition = { description: solidityPrompts.Custom, parameters: { type: 'object', - properties: addFunctionPropertiesFrom(commonFunctionDescription, [ - 'name', - 'pausable', - 'access', - 'upgradeable', - 'info', - ]), + properties: { + ...addFunctionPropertiesFrom(commonFunctionDescription, ['name', 'pausable', 'access', 'upgradeable', 'info']), + crossChainMessaging: { + anyOf: [ + { type: 'string', enum: ['superchain'] }, + { type: 'boolean', enum: [false] }, + ], + description: solidityCustomDescriptions.crossChainMessaging, + }, + crossChainFunctionName: { + type: 'string', + description: solidityCustomDescriptions.crossChainFunctionName, + }, + }, required: ['name'], additionalProperties: false, }, diff --git a/packages/ui/src/common/ExpandableCheckbox.svelte b/packages/ui/src/common/ExpandableCheckbox.svelte index 6a164677d..ec7759413 100644 --- a/packages/ui/src/common/ExpandableCheckbox.svelte +++ b/packages/ui/src/common/ExpandableCheckbox.svelte @@ -1,8 +1,10 @@ + + + + diff --git a/packages/ui/src/solidity/CustomControls.svelte b/packages/ui/src/solidity/CustomControls.svelte index bb54fd2f5..7b1ae85a0 100644 --- a/packages/ui/src/solidity/CustomControls.svelte +++ b/packages/ui/src/solidity/CustomControls.svelte @@ -1,12 +1,15 @@ @@ -41,6 +47,12 @@ + + diff --git a/yarn.lock b/yarn.lock index 45734ca13..819afa031 100644 --- a/yarn.lock +++ b/yarn.lock @@ -321,6 +321,11 @@ "@eslint/core" "^0.12.0" levn "^0.4.1" +"@eth-optimism/contracts-bedrock@^0.17.3": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@eth-optimism/contracts-bedrock/-/contracts-bedrock-0.17.3.tgz#ab3085e90d50e66ddbf07512ca1d666b5bab625b" + integrity sha512-cHPO7ntWeOBDLO7ZFFSZGI1D7CDqf4loRbRVJrFIsqLEz+hysWIqA0e3dv2GnrjVeuYePBfI2NTBEG4uwz9bQQ== + "@ethersproject/abi@^5.1.2": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449"