Skip to content
Open
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
50 changes: 50 additions & 0 deletions contracts/script/crosschain/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {IStateBridge} from "../../src/crosschain/interfaces/IStateBridge.sol";
import {PermissionedGatewayAdapter} from "../../src/crosschain/adapters/PermissionedGatewayAdapter.sol";
import {EthereumMPTGatewayAdapter} from "../../src/crosschain/adapters/EthereumMPTGatewayAdapter.sol";
import {LightClientGatewayAdapter} from "../../src/crosschain/adapters/LightClientGatewayAdapter.sol";
import {OpStackGatewayAdapter} from "../../src/crosschain/adapters/OpStackGatewayAdapter.sol";
import {Verifier} from "../../src/core/Verifier.sol";

/// @title BridgeDeployer
Expand Down Expand Up @@ -71,6 +72,7 @@ contract Deploy is Script {
uint256 internal _wcChainId;
address internal _l1BridgeProxy;
uint256 internal _l1ChainId;
address internal _l1GatewayAddr;
address internal _broadcaster;
string[] internal _deployFilter;

Expand Down Expand Up @@ -307,6 +309,10 @@ contract Deploy is Script {
if (_hasKey(_config, string.concat(np, ".zkGateway"))) {
_deployLightClientGateway(deployments, network, owner);
}

if (_hasKey(_config, string.concat(np, ".opStackGateway"))) {
_deployOpStackGateway(deployments, network);
}
}

function _deployPermissionedGateway(string memory deployments, string memory network, address owner) internal {
Expand Down Expand Up @@ -338,6 +344,7 @@ contract Deploy is Script {
address existing = _tryLoadAddress(deployments, deployKey);
if (existing != address(0)) {
console2.log(" EthereumMPTGateway already deployed at", existing);
_l1GatewayAddr = existing;
return;
}

Expand All @@ -356,6 +363,7 @@ contract Deploy is Script {
address addr = _deployer.deploy(salt, initCode);
console2.log(" EthereumMPTGateway:", addr);

_l1GatewayAddr = addr;
_gwAddrs.push(addr);
_gwTypes.push("ethereumMPTGateway");
}
Expand Down Expand Up @@ -408,6 +416,48 @@ contract Deploy is Script {
_gwTypes.push("lightClientGateway");
}

/// @dev OP Stack `L2CrossDomainMessenger` predeploy, identical across OP Stack chains.
address internal constant L2_CROSS_DOMAIN_MESSENGER = 0x4200000000000000000000000000000000000007;

/// @dev Deploys the native OP Stack gateway on an L2. Receives state pushed from L1 via the
/// `EthereumMPTGatewayAdapter` (the trusted L1 cross-domain sender), so the L1 network must
/// be deployed first.
function _deployOpStackGateway(string memory deployments, string memory network) internal {
string memory deployKey = string.concat(".", network, ".gateways.opStackGateway");

address existing = _tryLoadAddress(deployments, deployKey);
if (existing != address(0)) {
console2.log(" OpStackGateway already deployed at", existing);
return;
}

require(_l1BridgeProxy != address(0), "L1 bridge not deployed - deploy L1 network first");
require(_l1GatewayAddr != address(0), "L1 MPT gateway not deployed - deploy L1 network first");

// The L2CrossDomainMessenger is a predeploy at a fixed address, but allow a config override.
string memory gp = string.concat(".", network, ".opStackGateway");
address messenger = _tryLoadAddress(_config, string.concat(gp, ".messenger"));
if (messenger == address(0)) {
messenger = L2_CROSS_DOMAIN_MESSENGER;
}

console2.log("--- Deploying OpStackGateway ---");
console2.log(" Messenger:", messenger);
console2.log(" L1 sender:", _l1GatewayAddr);

bytes32 salt = keccak256(abi.encodePacked(vm.parseJsonBytes32(_config, ".salts.opStackGateway"), network));
bytes memory initCode = abi.encodePacked(
type(OpStackGatewayAdapter).creationCode,
abi.encode(messenger, _l1GatewayAddr, bridgeProxy, _l1BridgeProxy, _l1ChainId)
);

address addr = _deployer.deploy(salt, initCode);
console2.log(" OpStackGateway:", addr);

_gwAddrs.push(addr);
_gwTypes.push("opStackGateway");
}

////////////////////////////////////////////////////////////
// PERSISTENCE //
////////////////////////////////////////////////////////////
Expand Down
3 changes: 2 additions & 1 deletion contracts/script/crosschain/config/production.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"ownedGateway": "0x0000000000000000000000000000000000000000000000000000000000002300",
"l1Gateway": "0x0000000000000000000000000000000000000000000000000000000000002400",
"zkGateway": "0x0000000000000000000000000000000000000000000000000000000000002500",
"verifier": "0x0000000000000000000000000000000000000000000000000000000000002600"
"verifier": "0x0000000000000000000000000000000000000000000000000000000000002600",
"opStackGateway": "0x0000000000000000000000000000000000000000000000000000000000002700"
},

"worldchain": {
Expand Down
8 changes: 5 additions & 3 deletions contracts/script/crosschain/config/staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"ownedGateway": "0x0000000000000000000000000000000000000000000000000000000000002300",
"l1Gateway": "0x0000000000000000000000000000000000000000000000000000000000002400",
"zkGateway": "0x0000000000000000000000000000000000000000000000000000000000002500",
"verifier": "0x0000000000000000000000000000000000000000000000000000000000002600"
"verifier": "0x0000000000000000000000000000000000000000000000000000000000002600",
"opStackGateway": "0x0000000000000000000000000000000000000000000000000000000000002700"
},

"worldchain": {
Expand All @@ -23,7 +24,7 @@
"oprfRegistry": "0xb2C02253ee7bFEDF50F5D015658857099980E91F"
},

"networks": ["base"],
"networks": ["ethereum", "base"],

"ethereum": {
"chainId": 1,
Expand All @@ -40,7 +41,8 @@
"chainId": 8453,
"alchemySlug": "base-mainnet",
"verifier": "0x0000000000000000000000000000000000000000",
"ownedGateway": {}
"ownedGateway": {},
"opStackGateway": {}
},

"arbitrum": {
Expand Down
4 changes: 4 additions & 0 deletions contracts/src/crosschain/Error.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ error StorageValueTooLarge();
/// @dev Thrown when the proven chain head does not match the expected keccak chain head.
error InvalidChainHead();

/// @dev Thrown when a native cross-domain message is not relayed by the configured messenger,
/// or its L1 cross-domain sender is not the trusted L1 gateway.
error InvalidCrossDomainSender();

error InvalidContractName();

error InvalidContractVersion();
Expand Down
43 changes: 43 additions & 0 deletions contracts/src/crosschain/adapters/EthereumMPTGatewayAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@
pragma solidity ^0.8.28;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC7786GatewaySource} from "@openzeppelin/contracts/interfaces/draft-IERC7786.sol";
import {IDisputeGameFactory} from "interfaces/dispute/IDisputeGameFactory.sol";
import {IDisputeGame} from "interfaces/dispute/IDisputeGame.sol";
import {ICrossDomainMessenger} from "interfaces/universal/ICrossDomainMessenger.sol";
import {GameStatus, Claim, GameType} from "@optimism-bedrock/src/dispute/lib/Types.sol";
import {Lib} from "@world-id-bridge/lib/Lib.sol";
import "@world-id-bridge/lib/StateBridge.sol";
import {WorldIDGateway} from "@world-id-bridge/lib/Gateway.sol";
import "@world-id-bridge/Error.sol";

/// @notice Emitted when verified World Chain state is forwarded to a native OP Stack L2 gateway
/// through the `L1CrossDomainMessenger`.
event ForwardedToL2(address indexed messenger, address indexed l2Adapter, bytes32 chainHead);

/// @title EthereumMPTGatewayAdapter
/// @author World Contributors
/// @notice Trustless L1 verification adapter that authenticates World Chain state via the
Expand Down Expand Up @@ -83,6 +89,43 @@ contract EthereumMPTGatewayAdapter is WorldIDGateway, Ownable {
Lib.proveStorageSlot(ANCHOR_BRIDGE, STATE_BRIDGE_STORAGE_SLOT, wcAccountProof, wcStorageProof, wcStateRoot);
}

/// @notice Verifies World Chain state (dispute game + MPT), then forwards the proven chain
/// head to a native OP Stack L2 gateway through the `L1CrossDomainMessenger`.
/// @dev This adapter is the L1 cross-domain sender that the destination `OpStackGatewayAdapter`
/// trusts: the L2 message is delivered with this contract as `xDomainMessageSender`. The
/// proof is re-verified here so only authentic chain heads can ever be forwarded, making the
/// call permissionless like `sendMessage`. The same `payload` (commitment delta) is delivered
/// to L2; the destination satellite checks it hashes to `chainHead`.
/// @param messenger The `L1CrossDomainMessenger` for the destination rollup.
/// @param l2Adapter The `OpStackGatewayAdapter` address on the destination L2.
/// @param recipient ERC-7930 interoperable address of the destination `WorldIDSatellite`.
/// @param payload ABI-encoded `Lib.Commitment[]` delta to apply on L2.
/// @param attributes Gateway attributes carrying the MPT proof (`l1ProofAttributes`).
/// @param minGasLimit Minimum L2 gas for the relayed `sendMessage` call.
function forwardToL2(
address messenger,
address l2Adapter,
bytes calldata recipient,
bytes calldata payload,
bytes[] calldata attributes,
uint32 minGasLimit
) external virtual {
if (messenger == address(0) || l2Adapter == address(0)) revert ZeroAddress();
if (payload.length == 0) revert EmptyPayload();

bytes memory attributeData = validateAttributes(attributes);
bytes32 chainHead = _verifyAndExtract(payload, attributeData);

bytes[] memory l2Attributes = new bytes[](1);
l2Attributes[0] = abi.encodePacked(bytes4(keccak256("chainHead(bytes32)")), abi.encode(chainHead));

bytes memory message = abi.encodeCall(IERC7786GatewaySource.sendMessage, (recipient, payload, l2Attributes));

ICrossDomainMessenger(messenger).sendMessage(l2Adapter, message, minGasLimit);

emit ForwardedToL2(messenger, l2Adapter, chainHead);
}

////////////////////////////////////////////////////////////
// ADMIN FUNCTIONS //
////////////////////////////////////////////////////////////
Expand Down
61 changes: 61 additions & 0 deletions contracts/src/crosschain/adapters/OpStackGatewayAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ICrossDomainMessenger} from "interfaces/universal/ICrossDomainMessenger.sol";
import {WorldIDGateway} from "@world-id-bridge/lib/Gateway.sol";
import "@world-id-bridge/Error.sol";

/// @title OpStackGatewayAdapter
/// @author World Contributors
/// @notice Native OP Stack L1->L2 verification adapter. Receives the proven L1 `StateBridge`
/// chain head pushed natively from L1 through the OP Stack deposit path
/// (`L1CrossDomainMessenger` -> `OptimismPortal` -> `L2CrossDomainMessenger`).
///
/// Trust derives entirely from the canonical rollup messaging: this adapter only accepts calls
/// relayed by the local `L2CrossDomainMessenger` whose L1 cross-domain sender is the configured
/// `L1_SENDER` (the `EthereumMPTGatewayAdapter`). No proof is verified locally — the L1 sender
/// has already verified World Chain state against the L1 dispute game before forwarding.
///
/// This is a drop-in alternative to the `LightClientGatewayAdapter`: same role (anchor is the
/// L1 `StateBridge`, deployed on the destination L2, delivers to the local `WorldIDSatellite`),
/// but L1 state arrives via native rollup messaging instead of an SP1 light-client proof.
contract OpStackGatewayAdapter is WorldIDGateway {
/// @inheritdoc WorldIDGateway
bytes4 public constant override ATTRIBUTE = bytes4(keccak256("chainHead(bytes32)"));

/// @notice The local `L2CrossDomainMessenger` predeploy
/// (`0x4200000000000000000000000000000000000007` on OP Stack chains).
ICrossDomainMessenger public immutable MESSENGER;

/// @notice The trusted L1 sender (the `EthereumMPTGatewayAdapter`) authorized to push state.
address public immutable L1_SENDER;

/// @param messenger_ The `L2CrossDomainMessenger` predeploy address on this chain.
/// @param l1Sender_ The trusted L1 gateway (`EthereumMPTGatewayAdapter`) address.
/// @param bridge_ The `WorldIDSatellite` contract on this chain.
/// @param l1Bridge_ The L1 `StateBridge` address (anchor / source of truth).
/// @param l1ChainId_ The L1 chain ID (e.g. 1 for mainnet).
constructor(address messenger_, address l1Sender_, address bridge_, address l1Bridge_, uint256 l1ChainId_)
WorldIDGateway(bridge_, l1Bridge_, l1ChainId_)
{
if (messenger_ == address(0)) revert ZeroAddress();
if (l1Sender_ == address(0)) revert ZeroAddress();
MESSENGER = ICrossDomainMessenger(messenger_);
L1_SENDER = l1Sender_;
}

/// @dev Authenticates the native cross-domain message and extracts the proven chain head.
/// Reverts unless the call was relayed by `MESSENGER` and originated from `L1_SENDER` on L1.
/// Expects a single attribute: `chainHead(bytes32)`.
function _verifyAndExtract(bytes calldata, bytes memory proofData)
internal
virtual
override
returns (bytes32 chainHead)
{
if (msg.sender != address(MESSENGER)) revert InvalidCrossDomainSender();
if (MESSENGER.xDomainMessageSender() != L1_SENDER) revert InvalidCrossDomainSender();

(chainHead) = abi.decode(proofData, (bytes32));
}
}
Loading
Loading