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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ interface IL1CrossDomainMessenger is ICrossDomainMessenger, IProxyAdminOwnedBase
function version() external view returns (string memory);
function superchainConfig() external view returns (ISuperchainConfig);
function upgrade(ISystemConfig _systemConfig) external;
function sendMintMessage(
address _target,
bytes calldata _message,
uint256 _mintValue,
uint32 _minGasLimit
) external;
function setMinter(address _minter) external;

function __constructor__() external;
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase {
function version() external pure returns (string memory);
function migrateLiquidity() external;

function setMinter(address _minter) external;
function mintTransaction(address _to, uint256 _value) external;
function mintTransaction(address _to, uint256 _mintValue, uint64 _gasLimit, bytes memory _data) external;
function setNativeDeposit(bool _disable) external;

function __constructor__(uint256 _proofMaturityDelaySeconds) external;
Expand Down
64 changes: 64 additions & 0 deletions packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import { IOptimismPortal2 as IOptimismPortal } from "interfaces/L1/IOptimismPort
/// for sending and receiving data on the L1 side. Users are encouraged to use this
/// interface instead of interacting with lower-level contracts directly.
contract L1CrossDomainMessenger is CrossDomainMessenger, ProxyAdminOwnedBase, ReinitializableBase, ISemver {
/// @notice Thrown when the caller is not the minter.
error L1CrossDomainMessenger_NotMinter();

/// @custom:legacy
/// @custom:spacer superchainConfig
/// @notice Spacer taking up the legacy `superchainConfig` slot.
Expand All @@ -42,6 +45,9 @@ contract L1CrossDomainMessenger is CrossDomainMessenger, ProxyAdminOwnedBase, Re
/// @notice Contract of the SystemConfig.
ISystemConfig public systemConfig;

/// @notice Emitted when a minter is set.
event MinterSet(address indexed minter);

/// @notice Constructs the L1CrossDomainMessenger contract.
constructor() ReinitializableBase(2) {
_disableInitializers();
Expand Down Expand Up @@ -89,6 +95,64 @@ contract L1CrossDomainMessenger is CrossDomainMessenger, ProxyAdminOwnedBase, Re
return portal;
}

// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.L1CrossDomainMessenger.QKCConfigStorage")) - 1)) &
// ~bytes32(uint256(0xff))
bytes32 private constant _QKC_CONFIG_STORAGE_LOCATION =
0x21f30a216d738aeb55799dae7148f127e3b8f70b0224a5edb846c108cd573c00;
/// @custom:storage-location erc7201:openzeppelin.storage.L1CrossDomainMessenger.QKCConfigStorage

struct QKCConfigStorage {
/// @notice The minter for migrating existing L1 token to L2 native token.
address minter;
}

function _getQKCConfigStorage() private pure returns (QKCConfigStorage storage $) {
assembly {
$.slot := _QKC_CONFIG_STORAGE_LOCATION
}
}

/// @notice Add a minter to the L1CrossDomainMessenger contract. To disable, set an empty value.
function setMinter(address _minter) external {
_assertOnlyProxyAdminOrProxyAdminOwner();
QKCConfigStorage storage $ = _getQKCConfigStorage();
$.minter = _minter;
emit MinterSet(_minter);
}

/// @notice Triggers a QKC mint message via the relayMessage function on L2. Can only be called by the minter.
/// @dev This function can only be called by the minter.
/// @param _target Target contract or wallet address.
/// @param _message Message to trigger the target address with.
/// @param _mintValue Value to mint.
/// @param _minGasLimit Minimum gas limit that the message can be executed with.
function sendMintMessage(
address _target,
bytes calldata _message,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the method is intended solely for minting QKC, we should omit this parameter

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It tries to be flexible, e.g., it's possible to mint and swap in a single L2 transaction if we want it in future.

uint256 _mintValue,
uint32 _minGasLimit
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're using RECEIVE_DEFAULT_GAS_LIMIT, is there still a need to include the _minGasLimit parameter?

Copy link
Author

@blockchaindevsh blockchaindevsh Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the existing sendMessage, it's designed to be flexible, so that user can choose a proper _minGasLimit for the call.

)
external
{
QKCConfigStorage storage $ = _getQKCConfigStorage();
if (msg.sender != $.minter) {
revert L1CrossDomainMessenger_NotMinter();
}

portal.mintTransaction({
_to: address(otherMessenger),
_mintValue: _mintValue,
_gasLimit: baseGas(_message, _minGasLimit),
_data: abi.encodeWithSelector(
this.relayMessage.selector, messageNonce(), msg.sender, _target, _mintValue, _minGasLimit, _message
)
});

unchecked {
++msgNonce;
}
}

/// @inheritdoc CrossDomainMessenger
function _sendMessage(address _to, uint64 _gasLimit, uint256 _value, bytes memory _data) internal override {
portal.depositTransaction{ value: _value }({
Expand Down
45 changes: 24 additions & 21 deletions packages/contracts-bedrock/src/L1/OptimismPortal2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { GameStatus, GameType } from "src/dispute/lib/Types.sol";
import { ISemver } from "interfaces/universal/ISemver.sol";
import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol";
import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol";
import { IL1CrossDomainMessenger } from "interfaces/L1/IL1CrossDomainMessenger.sol";
import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol";
import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol";
import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol";
Expand Down Expand Up @@ -137,8 +138,7 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase
/// @custom:storage-location erc7201:openzeppelin.storage.OptimismPortal2.QKCConfigStorage

struct QKCConfigStorage {
/// @notice The minter for migrating existing L1 token to L2 native token.
address minter;
address spacer_for_minter;// should not be used again
bool disableNativeDeposit;
}

Expand Down Expand Up @@ -191,8 +191,6 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase
IAnchorStateRegistry newAnchorStateRegistry
);

/// @notice Emitted when a minter is set.
event MinterSet(address indexed minter);
/// @notice Emitted when native deposit is disabled.
event NativeDepositDisabled();
/// @notice Emitted when native deposit is enabled.
Expand Down Expand Up @@ -760,34 +758,39 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase
}
}

/// @notice Add a minter to the OptimismPortal contract. To disable, set an empty value.
function setMinter(address _minter) external {
if (msg.sender != proxyAdminOwner()) {
revert OptimismPortal_Unauthorized();
}
QKCConfigStorage storage $ = _getQKCConfigStorage();
$.minter = _minter;
emit MinterSet(_minter);
}

/// @notice Mint a specific amount of L2 native token to an address.
function mintTransaction(address _to, uint256 _value) external metered(RECEIVE_DEFAULT_GAS_LIMIT) {
QKCConfigStorage storage $ = _getQKCConfigStorage();
if (msg.sender != $.minter) {
/// @dev This function is only callable by L1CrossDomainMessenger.
function mintTransaction(
Copy link

@qzhodl qzhodl Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears that mintTransaction and depositTransaction share most of the same code. We could implement a shared _depositTransaction internal function containing the common logic, while allowing mintTransaction and depositTransaction to each handle their specific access control requirements.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep depositTransaction untouched, otherwise it'll be a pain to merge in future.

address _to,
uint256 _mintValue,
uint64 _gasLimit,
bytes memory _data
)
external
metered(_gasLimit)
{
// Can only be called by L1CrossDomainMessenger
address l1CrossDomainMessenger = systemConfig.l1CrossDomainMessenger();
if (msg.sender != l1CrossDomainMessenger) {
revert OptimismPortal_Unauthorized();
}

if (_to == address(0)) {
revert OptimismPortal_BadTarget();
// Prevent depositing transactions that have too small of a gas limit. Users should pay
// more for more resource usage.
if (_gasLimit < minimumGasLimit(uint64(_data.length))) {
revert OptimismPortal_GasLimitTooLow();
}

address from = AddressAliasHelper.applyL1ToL2Alias(msg.sender);

// Compute the opaque data that will be emitted as part of the TransactionDeposited event.
// We use opaque data so that we can update the TransactionDeposited event in the future
// without breaking the current interface.
bytes memory opaqueData = abi.encodePacked(_value, _value, RECEIVE_DEFAULT_GAS_LIMIT, false, bytes(""));
bytes memory opaqueData = abi.encodePacked(_mintValue, _mintValue, _gasLimit, false, _data);

// Emit a TransactionDeposited event so that the rollup node can derive a deposit
// transaction for this deposit.
emit TransactionDeposited(Constants.QKC_DEPOSITOR_ACCOUNT, _to, DEPOSIT_VERSION, opaqueData);
emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData);
}

/// @notice set native deposit flag. Pass true to disable.
Expand Down