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

Draft
wants to merge 24 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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/wizard-mcp': patch
---

Add option for Superchain interop message passing in Custom contracts
6 changes: 6 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,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"',
};
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
114 changes: 114 additions & 0 deletions packages/core/solidity/src/add-superchain-messaging.ts
Original file line number Diff line number Diff line change
@@ -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);
}
36 changes: 35 additions & 1 deletion packages/core/solidity/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -83,9 +95,11 @@ 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>();

constructor(name: string) {
this.name = toIdentifier(name, true);
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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;
}
}
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
99 changes: 99 additions & 0 deletions packages/core/solidity/src/custom.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file modified packages/core/solidity/src/custom.test.ts.snap
Binary file not shown.
Loading
Loading