Skip to content

Add Superchain interop message passing #595

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 58 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
b6f21a9
prototype
ericglau Jun 17, 2025
9f8d50b
Adjust look
ericglau Jul 3, 2025
8bddafb
Add placeholder checkbox
ericglau Jul 3, 2025
9248727
Add separate options
ericglau Jul 4, 2025
7a02121
Use svelte component for crosschain messaging
ericglau Jul 4, 2025
ce336b9
Simplify options passing
ericglau Jul 4, 2025
4302bc9
Sanitize
ericglau Jul 4, 2025
bb68267
Adjust min width
ericglau Jul 4, 2025
b4a85d9
Track errors
ericglau Jul 4, 2025
2d36384
Update help info
ericglau Jul 4, 2025
1f1d881
Update access control
ericglau Jul 4, 2025
2916ce8
Add custom errors, doc comments
ericglau Jul 4, 2025
ae753fd
Add modifier def
ericglau Jul 4, 2025
5206bfd
avoid infer transpile
ericglau Jul 4, 2025
7d78177
Refactor
ericglau Jul 4, 2025
22b67cf
Lint
ericglau Jul 4, 2025
21d2c52
Update snapshots
ericglau Jul 4, 2025
048bffb
Add contracts-bedrock
ericglau Jul 4, 2025
6e26410
Merge remote-tracking branch 'upstream/master' into superchaininterop
ericglau Jul 4, 2025
3c52b72
add tests
ericglau Jul 4, 2025
c927b52
Update AI fn defs
ericglau Jul 4, 2025
84ca34d
Add changeset
ericglau Jul 4, 2025
d45742c
Fix import
ericglau Jul 4, 2025
4c6e35f
lint svelte
ericglau Jul 4, 2025
019c9ba
Add tooltip when enabled
ericglau Jul 28, 2025
6beff7c
Merge branch 'master' into superchaininterop
ericglau Jul 28, 2025
2e3dd4d
Fix compile
ericglau Jul 28, 2025
c12b388
WIP test get imports
ericglau Jul 28, 2025
65cce15
Fix minimum cover to get all imports
ericglau Jul 29, 2025
716aed2
Lint
ericglau Jul 29, 2025
5bb6fe5
Update tooltip and natspec messages
ericglau Jul 29, 2025
beeb31a
Fix reactivity for ai assistant
ericglau Jul 29, 2025
d415620
Lint
ericglau Jul 29, 2025
68f8293
Update prompts and schemas for clarity
ericglau Jul 30, 2025
9d40c46
Add zip hardhat environments and test cases
ericglau Jul 30, 2025
af6c210
Add natspec tag to allow immutable
ericglau Jul 30, 2025
e0efb37
Lint
ericglau Jul 30, 2025
4f809de
WIP Add zip foundry test
ericglau Jul 30, 2025
8b01556
Add Optimism dependencies to Foundry package
ericglau Jul 30, 2025
6310e3e
Use Optimism npm package version
ericglau Jul 30, 2025
9d59528
Fix remapping
ericglau Jul 30, 2025
17ad272
Add fallback when modifying remappings
ericglau Jul 30, 2025
ed36ec9
Rename instead of add remapping
ericglau Jul 30, 2025
4ed8218
Lint
ericglau Jul 30, 2025
7de36dc
Update changeset
ericglau Jul 31, 2025
83cc34c
Merge branch 'master' into superchaininterop
ericglau Jul 31, 2025
fa8f0c3
Update note for clarity, use natspec
ericglau Jul 31, 2025
2f81bcd
Add soldeer lock
ericglau Aug 1, 2025
67d075b
Load soldeer lock differently for UI and tests
ericglau Aug 2, 2025
ca4ebd2
Lint
ericglau Aug 2, 2025
b922cb5
Simplify rollup plugin
ericglau Aug 2, 2025
36fce55
Rename arg toChainId
ericglau Aug 2, 2025
b4f508b
Merge branch 'master' into superchaininterop
CoveMB Aug 5, 2025
c4b49d7
Change string concat from code review
ericglau Aug 6, 2025
15561ea
Update TODO to be more actionable
ericglau Aug 7, 2025
0db2964
Update snapshots
ericglau Aug 7, 2025
a815fae
Remove redundant check when toChainId is the current chain
ericglau Aug 7, 2025
1369d15
Inject Optimism links for imports
ericglau Aug 8, 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/some-fans-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@openzeppelin/wizard': patch
'@openzeppelin/wizard-common': patch
'@openzeppelin/contracts-mcp': patch
---

Solidity: Add option for Superchain interop message passing in Custom contracts
7 changes: 7 additions & 0 deletions packages/common/src/ai/descriptions/solidity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,10 @@ 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. Options are "superchain" or false',
crossChainFunctionName:
'The name of a custom function that will be callable from another chain, default is "myFunction". Only used if crossChainMessaging is set to "superchain"',
};
1 change: 1 addition & 0 deletions packages/core/solidity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/core/solidity/remappings.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@openzeppelin/community-contracts/=node_modules/@openzeppelin/community-contracts/contracts/
@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/
@eth-optimism/contracts-bedrock/=node_modules/@eth-optimism/contracts-bedrock/
119 changes: 119 additions & 0 deletions packages/core/solidity/src/add-superchain-messaging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
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,
});

const allowImmutableNatspec = {
key: '@custom:oz-upgrades-unsafe-allow',
value: 'state-variable-immutable',
};
c.addVariable(
'IL2ToL2CrossDomainMessenger public immutable messenger = IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);',
[allowImmutableNatspec],
);
}

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: [{ name: 'toChainId', type: 'uint256' }],
};

if (access) {
requireAccessControl(c, sourceFn, access, 'CROSSCHAIN_CALLER', 'crossChainCaller');
} else {
c.setFunctionComments(['/// @dev NOTE: This function is unprotected. 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: This function trusts contracts at the same address on other chains.',
' * If an unauthorized contract is deployed at the same address on any chain in the Superchain, it could allow',
' * malicious actors to invoke your function from that chain.',
" * To prevent this, you must either design the deployer to allow only this contract's bytecode to be deployed",
' * through it, or use CREATE2 from a deployer contract that is itself deployed by an EOA you control.',
' */',
],
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);
}
55 changes: 47 additions & 8 deletions packages/core/solidity/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,19 @@ export interface Contract {
functions: ContractFunction[];
constructorCode: string[];
constructorArgs: FunctionArgument[];
variables: string[];
variables: Variable[];
upgradeable: boolean;
customErrors: CustomError[];
modifierDefinitions: ModifierDefinition[];
}

export interface CustomError {
name: string;
}

export interface Variable {
code: string;
natspecTags?: NatspecTag[];
}

export type Value = string | number | { lit: string } | { note: string; value: Value };
Expand All @@ -35,9 +46,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;
Expand All @@ -52,7 +69,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
Expand Down Expand Up @@ -82,10 +99,12 @@ export class ContractBuilder implements Contract {

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

private parentMap: Map<string, Parent> = new Map<string, Parent>();
private functionMap: Map<string, ContractFunction> = new Map();
private modifierDefinitionsMap: Map<string, ModifierDefinition> = new Map<string, ModifierDefinition>();
private variableMap: Map<string, Variable> = new Map<string, Variable>();

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

get variables(): string[] {
return [...this.variableSet];
get variables(): Variable[] {
return [...this.variableMap.values()];
}

get customErrors(): CustomError[] {
return [...this.customErrorSet].map(name => ({ name }));
}

get modifierDefinitions(): ModifierDefinition[] {
return [...this.modifierDefinitionsMap.values()];
}

addParent(contract: ImportContract, params: Value[] = []): boolean {
Expand Down Expand Up @@ -214,9 +241,21 @@ export class ContractBuilder implements Contract {
/**
* Note: The type in the variable 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);
addVariable(code: string, natspecTags?: NatspecTag[]): boolean {
const present = this.variableMap.has(code);
this.variableMap.set(code, { code, natspecTags });
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;
}
}
26 changes: 26 additions & 0 deletions packages/core/solidity/src/custom.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import test from 'ava';
import type { OptionsError } from '.';
import { custom } from '.';

import type { CustomOptions } from './custom';
Expand Down Expand Up @@ -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,
Expand All @@ -81,13 +103,17 @@ testAPIEquivalence('custom API full upgradeable', {
access: 'roles',
pausable: true,
upgradeable: 'uups',
crossChainMessaging: 'superchain',
crossChainFunctionName: 'myCustomFunction',
});

testAPIEquivalence('custom API full upgradeable with managed', {
name: 'CustomContract',
access: 'managed',
pausable: true,
upgradeable: 'uups',
crossChainMessaging: 'superchain',
crossChainFunctionName: 'myCustomFunction',
});

test('custom API assert defaults', async t => {
Expand Down
Loading
Loading