mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-07 03:23:49 -05:00
Feat/1076 refactor and allow overriding (#1079)
* allow tokenbridge overrides * add L1MessageService overrides * refactor L2 MessageService * refactor L2 MessageService V1 * use correct modifier * refactor LineaRollup for overriding * allow other overrides * reinstate general in pause on tokenbridge * add missing NatSpec * sample overrides * add generic bridge and document placeholder * documentation and folder placement * documentation cleanup * use imported references * use variable pragma for inherited contracts * reset pragmas for some * use base abstract contracts with version overrides * use TokenBridgeBase as abstract * Update contracts/src/bridging/token/TokenBridgeBase.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/bridging/token/TokenBridgeBase.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/bridging/token/TokenBridgeBase.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/bridging/token/interfaces/ITokenBridge.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/messaging/l2/L2MessageServiceBase.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/messaging/l2/v1/interfaces/IL2MessageServiceV1.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/rollup/interfaces/ILineaRollup.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/rollup/LineaRollupBase.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/_testing/unit/bridging/InheritingTokenBridge.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/verifiers/PlonkVerifierDev.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/verifiers/PlonkVerifierForDataAggregation.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/verifiers/PlonkVerifierForMultiTypeDataAggregation.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/verifiers/PlonkVerifierMainnetFull.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * Update contracts/src/verifiers/PlonkVerifierSepoliaFull.sol Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com> Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> * linting * allow submitDataAsCalldata overriding * address missing test coverage * adjust gap name for storage clarity --------- Signed-off-by: The Dark Jester <thedarkjester@users.noreply.github.com> Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com>
This commit is contained in:
52
contracts/docs/inheriting-main-contracts.md
Normal file
52
contracts/docs/inheriting-main-contracts.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Customizing Linea Rollup, Messaging, and Bridging Components
|
||||
|
||||
This guide explains how to customize core rollup, messaging, and bridging functionality by overriding virtual functions in the Linea protocol contracts. Several examples are provided to help you get started. Please note, these are just illustrative samples.
|
||||
|
||||
**Note:** It is recommended that any overrides or modifications from the original audited code should be independently audited for conflicting behavior.
|
||||
|
||||
## 🧩 Customizing Contract Versions
|
||||
|
||||
To modify the ABI version returned, the `CONTRACT_VERSION()` must be overriden with your specific string value. The default is the base version.
|
||||
|
||||
## 🧱 Customizing Linea Rollup Behavior
|
||||
|
||||
To modify the behavior of the rollup mechanism itself (e.g. blob submission for finalization), you can override virtual functions in `LineaRollup.sol`.
|
||||
|
||||
## ✉️ Customizing Message Sending Behavior
|
||||
|
||||
The `MessageService` contract handles L1 ↔ L2 message passing. If you want to adjust how messages are sent or verified (e.g. adding rules, changing fee logic), you can override its virtual functions.
|
||||
|
||||
See [`InheritingLineaRollup.sol`](../src/_testing/unit/rollup/InheritingLineaRollup.sol) for an example where `MessageService` functionality is customized within the rollup context.
|
||||
|
||||
The example provided prevents fee and value from being directly sent to the message service, but instead relies on the samples for the generic Ether bridge - sample are found links below.
|
||||
|
||||
## 🔁 Overriding Token Bridge Behavior
|
||||
|
||||
To implement custom behavior for token bridging (e.g., additional validation, whitelisting, alternative transfer mechanisms), override functions in the `TokenBridge` contract.
|
||||
|
||||
A reference implementation is available in [`InheritingTokenBridge.sol`](../src/_testing/unit/bridging/InheritingTokenBridge.sol), demonstrating how to override core bridge functions for ERC20 tokens.
|
||||
|
||||
The example creates a generic way move specific funds to alternate escrow accounts when bridging. This could be done for various reasons - e.g. independently controlled asset types.
|
||||
|
||||
## 📨 Overriding L2 Message Service Behavior
|
||||
|
||||
If you need to modify how messages are processed or validated on the L2 side, override functions in the `L2MessageService` contract.
|
||||
|
||||
Check out [`InheritingL2MessageService.sol`](../src/_testing/unit/messaging/InheritingL2MessageService.sol) for a practical example of extending L2 message processing behavior.
|
||||
|
||||
This is modified in line with the customized L1MessageService behavior to work with the custom Ether bridges.
|
||||
|
||||
## 🪙 Custom Ether Bridge Examples
|
||||
|
||||
For more advanced use cases involving Ether transfers—such as delayed withdrawals or multi-phase bridging—you can extend and override the send/receive logic in:
|
||||
|
||||
- [`L1GenericBridge.sol`](../src/_testing/unit/bridging/l1/L1GenericBridge.sol)
|
||||
- [`L2GenericBridge.sol`](../src/_testing/unit/bridging/l2/L2GenericBridge.sol)
|
||||
|
||||
These sample files serve as a foundation for creating fully custom Ether bridging flows tailored to your application needs.
|
||||
|
||||
---
|
||||
|
||||
By leveraging inheritance and Solidity's `virtual`/`override` keywords, you can safely extend and adapt Linea's modular bridge and rollup system.
|
||||
|
||||
Additionally, it is worth noting that all the inherited contracts now contain an additional 50 storage slots of padded space for future Linea expansion without breaking the underlying inheritors layouts.
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { IPlonkVerifier } from "../../verifiers/interfaces/IPlonkVerifier.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { LineaScenarioTesting } from "./LineaScenarioTesting.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/// @dev Test scenarios on Linea.
|
||||
contract LineaScenarioTesting {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { IPlonkVerifier } from "../../../verifiers/interfaces/IPlonkVerifier.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { IMessageService } from "../../../messaging/interfaces/IMessageService.sol";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
contract TestL1RevertContract {
|
||||
function errorWithMessage() external pure {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
contract TestReceivingContract {
|
||||
fallback() external payable {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { IMessageService } from "../../../messaging/interfaces/IMessageService.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { IMessageService } from "../../../messaging/interfaces/IMessageService.sol";
|
||||
import { IGenericErrors } from "../../../interfaces/IGenericErrors.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { TokenBridge } from "../../../bridging/token/TokenBridge.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { TokenBridge } from "../../../bridging/token/TokenBridge.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { BridgedToken } from "../../../bridging/token/BridgedToken.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { ReentrancyContract } from "./ReentrancyContract.sol";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
interface ITestExternalCalls {
|
||||
function revertWithError() external pure;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
|
||||
import { TokenBridgeBase } from "../../../bridging/token/TokenBridgeBase.sol";
|
||||
|
||||
contract InheritingL1TokenBridge is TokenBridgeBase {
|
||||
enum BridingStatus {
|
||||
REQUESTED,
|
||||
READY,
|
||||
COMPLETE
|
||||
}
|
||||
|
||||
mapping(address tokenAddress => address escrowAddress) escrowAddresses;
|
||||
mapping(bytes32 bridgingHash => BridingStatus status) public bridgingStatuses;
|
||||
|
||||
function initialize(
|
||||
InitializationData calldata _initializationData
|
||||
)
|
||||
external
|
||||
nonZeroAddress(_initializationData.messageService)
|
||||
nonZeroAddress(_initializationData.tokenBeacon)
|
||||
nonZeroChainId(_initializationData.sourceChainId)
|
||||
nonZeroChainId(_initializationData.targetChainId)
|
||||
initializer
|
||||
{
|
||||
// New custom initialization behavior here.
|
||||
__TokenBridge_init(_initializationData);
|
||||
}
|
||||
|
||||
function _getEscrowAddress(address _token) internal view virtual override returns (address escrowAddress) {
|
||||
// Overriden to allow the movement of specific assets to custom escrow services.
|
||||
|
||||
escrowAddress = escrowAddresses[_token];
|
||||
if (escrowAddress == address(0)) {
|
||||
escrowAddress = address(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Implementor to add security.
|
||||
function setEscrowAddress(address _token, address _escrowAddress) external {
|
||||
// Validate fields
|
||||
// TBC check not already set?
|
||||
escrowAddresses[_token] = _escrowAddress;
|
||||
|
||||
// event emitted
|
||||
}
|
||||
|
||||
function finalizeBridging(
|
||||
address _nativeToken,
|
||||
uint256 _amount,
|
||||
address _recipient,
|
||||
uint256 _chainId,
|
||||
bytes calldata _tokenMetadata
|
||||
) external {
|
||||
// New custom function to finalize withdrawal if a multi-step approach is taken.
|
||||
}
|
||||
|
||||
function _completeBridging(
|
||||
address _nativeToken,
|
||||
uint256 _amount,
|
||||
address _recipient,
|
||||
uint256 _chainId,
|
||||
bytes calldata _tokenMetadata
|
||||
) internal virtual override {
|
||||
// Similar approach to the ETH bridge with hashing and withdrawal flow management.
|
||||
}
|
||||
}
|
||||
135
contracts/src/_testing/unit/bridging/l1/L1GenericBridge.sol
Normal file
135
contracts/src/_testing/unit/bridging/l1/L1GenericBridge.sol
Normal file
@@ -0,0 +1,135 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { MessageServiceBase } from "../../../../messaging/MessageServiceBase.sol";
|
||||
import { L2GenericBridge } from "../l2/L2GenericBridge.sol"; // Ideally a simple interface.
|
||||
|
||||
// Contract could be made upgradable with access control etc by implementors.
|
||||
contract L1GenericBridge is MessageServiceBase {
|
||||
uint256 public withdrawalDelay;
|
||||
address public escrowAddress;
|
||||
|
||||
enum BridingStatus {
|
||||
UNKNOWN,
|
||||
REQUESTED,
|
||||
COMPLETE
|
||||
}
|
||||
|
||||
enum WithdrawalOption {
|
||||
WAIT,
|
||||
IMMEDIATE
|
||||
}
|
||||
|
||||
mapping(bytes32 bridgingHash => BridingStatus status) public bridgingStatuses;
|
||||
|
||||
// Implementors would add access control.
|
||||
function setEscrowAddress(address _escrowAddress) external {
|
||||
if (escrowAddress == _escrowAddress) {
|
||||
revert("already set"); // error averts weird events for set address.
|
||||
}
|
||||
|
||||
escrowAddress = _escrowAddress;
|
||||
// emit events
|
||||
}
|
||||
|
||||
// Implementors would add access control.
|
||||
function setWithdrawalDelay(uint256 _withdrawalDelay) external {
|
||||
if (withdrawalDelay == _withdrawalDelay) {
|
||||
revert("already set"); // error averts weird events for set address.
|
||||
}
|
||||
|
||||
withdrawalDelay = _withdrawalDelay;
|
||||
// emit events
|
||||
}
|
||||
|
||||
// Implementors would add any additional modifiers ( pause etc ).
|
||||
function bridgeEth(address _to) external payable {
|
||||
// get next message number / nonce - this should come from the message service.
|
||||
uint256 nextMessageNumber;
|
||||
|
||||
messageService.sendMessage(
|
||||
remoteSender,
|
||||
0,
|
||||
abi.encodeCall(L2GenericBridge.receiveDepositedEth, (_to, msg.sender, msg.value, nextMessageNumber))
|
||||
);
|
||||
|
||||
// emit events etc
|
||||
}
|
||||
|
||||
// This would be called by the message service (onlyMessagingService)
|
||||
// with a check to make sure it came from the L1 Bridge (onlyAuthorizedRemoteSender)
|
||||
// The message service checks reentry already
|
||||
function receiveDepositedEth(
|
||||
address _to,
|
||||
address _from,
|
||||
uint256 _value,
|
||||
uint256 _nonce,
|
||||
WithdrawalOption _option
|
||||
) external onlyMessagingService onlyAuthorizedRemoteSender {
|
||||
uint256 minWithdrawTime = block.timestamp;
|
||||
|
||||
if (_option == WithdrawalOption.WAIT) {
|
||||
unchecked {
|
||||
minWithdrawTime += withdrawalDelay;
|
||||
}
|
||||
}
|
||||
|
||||
// hashing to be optimized
|
||||
bytes32 bridgingHash = keccak256(abi.encode(_to, _from, _value, _nonce, _option, minWithdrawTime));
|
||||
|
||||
if (bridgingStatuses[bridgingHash] != BridingStatus.UNKNOWN) {
|
||||
// reentry not allowed
|
||||
revert();
|
||||
}
|
||||
|
||||
if (_option == WithdrawalOption.WAIT) {
|
||||
bridgingStatuses[bridgingHash] = BridingStatus.REQUESTED;
|
||||
// emit requested
|
||||
} else {
|
||||
bridgingStatuses[bridgingHash] = BridingStatus.COMPLETE;
|
||||
// call escrowAddress for immediate transfer.
|
||||
}
|
||||
}
|
||||
|
||||
function completeWithdrawal(
|
||||
address _to,
|
||||
address _from,
|
||||
uint256 _value,
|
||||
uint256 _nonce,
|
||||
WithdrawalOption _option,
|
||||
uint256 _minWithdrawTime
|
||||
) external payable {
|
||||
if (block.timestamp < _minWithdrawTime) {
|
||||
revert("too early");
|
||||
}
|
||||
|
||||
if (_value != msg.value) {
|
||||
revert("value is wrong");
|
||||
}
|
||||
|
||||
// hashing to be optimized
|
||||
bytes32 bridgingHash = keccak256(abi.encode(_to, _from, _value, _nonce, _option, _minWithdrawTime));
|
||||
|
||||
if (bridgingStatuses[bridgingHash] != BridingStatus.REQUESTED) {
|
||||
// reentry not allowed
|
||||
revert();
|
||||
}
|
||||
|
||||
bridgingStatuses[bridgingHash] = BridingStatus.COMPLETE;
|
||||
|
||||
(bool callSuccess, bytes memory returnData) = _to.call{ value: _value }("");
|
||||
if (!callSuccess) {
|
||||
if (returnData.length > 0) {
|
||||
assembly {
|
||||
let data_size := mload(returnData)
|
||||
revert(add(32, returnData), data_size)
|
||||
}
|
||||
} else {
|
||||
//revert MessageSendingFailed(_to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// function: add ETH for user withdrawal (admin only)
|
||||
function addWithdrawalEth() external payable {}
|
||||
}
|
||||
33
contracts/src/_testing/unit/bridging/l2/L2GenericBridge.sol
Normal file
33
contracts/src/_testing/unit/bridging/l2/L2GenericBridge.sol
Normal file
@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { MessageServiceBase } from "../../../../messaging/MessageServiceBase.sol";
|
||||
import { L1GenericBridge } from "../l1/L1GenericBridge.sol"; // Ideally a simple interface.
|
||||
|
||||
contract L2GenericBridge is MessageServiceBase {
|
||||
// initialize etc
|
||||
|
||||
// add security
|
||||
function bridgeEth(address _to, L1GenericBridge.WithdrawalOption _option) external payable {
|
||||
// get next message number / nonce;
|
||||
uint256 nextMessageNumber;
|
||||
|
||||
messageService.sendMessage(
|
||||
remoteSender,
|
||||
0,
|
||||
abi.encodeCall(L1GenericBridge.receiveDepositedEth, (_to, msg.sender, msg.value, nextMessageNumber, _option))
|
||||
);
|
||||
}
|
||||
|
||||
// This would be called by the message service (onlyMessagingService)
|
||||
// with a check to make sure it came from the L1 Bridge (onlyAuthorizedRemoteSender)
|
||||
// The message service checks reentry already
|
||||
function receiveDepositedEth(
|
||||
address _to,
|
||||
address _from,
|
||||
uint256 _value,
|
||||
uint256 _nonce
|
||||
) external onlyMessagingService onlyAuthorizedRemoteSender {
|
||||
// Transfer to the Recipient or customize the process.
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { EfficientLeftRightKeccak } from "../../../libraries/EfficientLeftRightKeccak.sol";
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { L2MessageServiceBase } from "../../../messaging/l2/L2MessageServiceBase.sol";
|
||||
|
||||
/// @custom:oz-upgrades-unsafe-allow missing-initializer
|
||||
contract InheritingL2MessageService is L2MessageServiceBase {
|
||||
error DirectETHSendingDisallowed();
|
||||
error FeeSendingDisallowed();
|
||||
error OnlyAllowedSendersToRemoteReceiver();
|
||||
error OnlyFromRemoteReceiverToAllowedSender();
|
||||
|
||||
error MessageSenderStateAlreadySet(address sender, bool state);
|
||||
error MessageReceiverAlreadySet(address receiver);
|
||||
|
||||
event AllowedMessageSenderStateSet(address sender, bool state);
|
||||
event RemoteReceiverSet(address receiver);
|
||||
|
||||
/// @notice The role required to set/remove allowed message senders.
|
||||
bytes32 public constant ALLOWED_MESSAGESENDER_SETTING_ROLE = keccak256("ALLOWED_MESSAGESENDER_SETTING_ROLE"); // should be security council restricted
|
||||
|
||||
/// @notice The role required to set the remote receiver.
|
||||
bytes32 public constant REMOTE_RECEIVER_SETTER_ROLE = keccak256("REMOTE_RECEIVER_SETTER_ROLE"); // should be security council restricted
|
||||
|
||||
mapping(address remoteReceiver => bool isRemoteReceiver) public remoteReceivers;
|
||||
mapping(address sender => bool isAllowedToSendToRemoteReceiver) public allowedMessageSenders;
|
||||
|
||||
/// @custom:oz-upgrades-unsafe-allow constructor
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes underlying message service dependencies.
|
||||
* @param _rateLimitPeriod The period to rate limit against.
|
||||
* @param _rateLimitAmount The limit allowed for withdrawing the period.
|
||||
* @param _defaultAdmin The account to be given DEFAULT_ADMIN_ROLE on initialization.
|
||||
* @param _roleAddresses The list of addresses to grant roles to.
|
||||
* @param _pauseTypeRoles The list of pause type roles.
|
||||
* @param _unpauseTypeRoles The list of unpause type roles.
|
||||
*/
|
||||
function initialize(
|
||||
uint256 _rateLimitPeriod,
|
||||
uint256 _rateLimitAmount,
|
||||
address _defaultAdmin,
|
||||
RoleAddress[] calldata _roleAddresses,
|
||||
PauseTypeRole[] calldata _pauseTypeRoles,
|
||||
PauseTypeRole[] calldata _unpauseTypeRoles
|
||||
) external initializer {
|
||||
__L2MessageService_init(
|
||||
_rateLimitPeriod,
|
||||
_rateLimitAmount,
|
||||
_defaultAdmin,
|
||||
_roleAddresses,
|
||||
_pauseTypeRoles,
|
||||
_unpauseTypeRoles
|
||||
);
|
||||
}
|
||||
|
||||
function setRemoteReceiver(address _receiver) external onlyRole(REMOTE_RECEIVER_SETTER_ROLE) {
|
||||
if (remoteReceivers[_receiver]) {
|
||||
revert MessageReceiverAlreadySet(_receiver);
|
||||
}
|
||||
|
||||
remoteReceivers[_receiver] = true;
|
||||
emit RemoteReceiverSet(_receiver);
|
||||
}
|
||||
|
||||
function setAllowedMessageSenderState(
|
||||
address _allowedSender,
|
||||
bool _isAllowedToSend
|
||||
) external onlyRole(ALLOWED_MESSAGESENDER_SETTING_ROLE) {
|
||||
if (allowedMessageSenders[_allowedSender] == _isAllowedToSend) {
|
||||
revert MessageSenderStateAlreadySet(_allowedSender, _isAllowedToSend); // this avoids confusing events
|
||||
}
|
||||
|
||||
emit AllowedMessageSenderStateSet(_allowedSender, _isAllowedToSend);
|
||||
}
|
||||
|
||||
function sendMessage(
|
||||
address _to,
|
||||
uint256 _fee,
|
||||
bytes calldata _calldata
|
||||
) external payable override whenTypeAndGeneralNotPaused(PauseType.L2_L1) {
|
||||
if (msg.value > 0) {
|
||||
revert DirectETHSendingDisallowed();
|
||||
}
|
||||
|
||||
if (_fee > 0) {
|
||||
revert FeeSendingDisallowed();
|
||||
}
|
||||
|
||||
if (remoteReceivers[_to] && !allowedMessageSenders[msg.sender]) {
|
||||
revert OnlyAllowedSendersToRemoteReceiver();
|
||||
}
|
||||
|
||||
_sendMessage(_to, _fee, _calldata);
|
||||
}
|
||||
|
||||
function _claimMessage(
|
||||
address _from,
|
||||
address _to,
|
||||
uint256 _fee,
|
||||
uint256 _value,
|
||||
bytes calldata _calldata,
|
||||
uint256 _nonce
|
||||
) internal override {
|
||||
// This may be an issue with migration - TBD
|
||||
if (_value > 0) {
|
||||
revert DirectETHSendingDisallowed();
|
||||
}
|
||||
|
||||
// TBC: if running an L1 postman...
|
||||
// if (_params.fee > 0) {
|
||||
// revert DirectETHSendingDisallowed();
|
||||
// }
|
||||
|
||||
// is this from the remote bridge? and are they sending back to someone they are allowed to send back to?
|
||||
// NB: TBD - This might need to be done as pairs vs. open ended.
|
||||
if (remoteReceivers[_from] && !allowedMessageSenders[_to]) {
|
||||
revert OnlyFromRemoteReceiverToAllowedSender();
|
||||
}
|
||||
|
||||
super._claimMessage(_from, _to, _fee, _value, _calldata, _nonce);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { L1MessageManager } from "../../../messaging/l1/L1MessageManager.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { L1MessageService } from "../../../messaging/l1/L1MessageService.sol";
|
||||
import { TestSetPauseTypeRoles } from "../security/TestSetPauseTypeRoles.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { L1MessageService } from "../../../messaging/l1/L1MessageService.sol";
|
||||
import { IL1MessageService } from "../../../messaging/l1/interfaces/IL1MessageService.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import { L2MessageManager } from "../../../messaging/l2/L2MessageManager.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { L2MessageService } from "../../../messaging/l2/L2MessageService.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { MessageServiceBase } from "../../../messaging/MessageServiceBase.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { SparseMerkleTreeVerifier } from "../../../messaging/libraries/SparseMerkleTreeVerifier.sol";
|
||||
import { EfficientLeftRightKeccak } from "../../../libraries/EfficientLeftRightKeccak.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
contract ErrorAndDestructionTesting {
|
||||
function externalRevert() external pure {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/// @dev Not intended for mainnet or testnet deployment, only for local testing
|
||||
contract OpcodeTestContract {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { ErrorAndDestructionTesting } from "./ErrorAndDestructionTesting.sol";
|
||||
|
||||
|
||||
104
contracts/src/_testing/unit/rollup/InheritingLineaRollup.sol
Normal file
104
contracts/src/_testing/unit/rollup/InheritingLineaRollup.sol
Normal file
@@ -0,0 +1,104 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { LineaRollupBase } from "../../../rollup/LineaRollupBase.sol";
|
||||
import { L1MessageService } from "../../../messaging/l1/L1MessageService.sol";
|
||||
import { IMessageService } from "../../../messaging/interfaces/IMessageService.sol";
|
||||
|
||||
/// @custom:oz-upgrades-unsafe-allow missing-initializer
|
||||
contract InheritingRollup is LineaRollupBase {
|
||||
error DirectETHSendingDisallowed();
|
||||
error FeeSendingDisallowed();
|
||||
error OnlyAllowedSendersToRemoteReceiver();
|
||||
error OnlyFromRemoteReceiverToAllowedSender();
|
||||
|
||||
error MessageSenderStateAlreadySet(address sender, bool state);
|
||||
error MessageReceiverAlreadySet(address receiver);
|
||||
|
||||
event AllowedMessageSenderStateSet(address sender, bool state);
|
||||
event RemoteReceiverSet(address receiver);
|
||||
|
||||
/// @notice The role required to set/remove allowed message senders.
|
||||
bytes32 public constant ALLOWED_MESSAGESENDER_SETTING_ROLE = keccak256("ALLOWED_MESSAGESENDER_SETTING_ROLE"); // should be security council restricted
|
||||
|
||||
/// @notice The role required to set the remote receiver.
|
||||
bytes32 public constant REMOTE_RECEIVER_SETTER_ROLE = keccak256("REMOTE_RECEIVER_SETTER_ROLE"); // should be security council restricted
|
||||
|
||||
mapping(address remoteReceiver => bool isRemoteReceiver) public remoteReceivers;
|
||||
mapping(address sender => bool isAllowedToSendToRemoteReceiver) public allowedMessageSenders;
|
||||
|
||||
/// @custom:oz-upgrades-unsafe-allow constructor
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
function initialize(InitializationData calldata _initializationData) external initializer {
|
||||
__LineaRollup_init(_initializationData);
|
||||
}
|
||||
|
||||
function setAllowedMessageSenderState(
|
||||
address _allowedSender,
|
||||
bool _isAllowedToSend
|
||||
) external onlyRole(ALLOWED_MESSAGESENDER_SETTING_ROLE) {
|
||||
if (allowedMessageSenders[_allowedSender] == _isAllowedToSend) {
|
||||
revert MessageSenderStateAlreadySet(_allowedSender, _isAllowedToSend); // this avoids confusing events
|
||||
}
|
||||
|
||||
emit AllowedMessageSenderStateSet(_allowedSender, _isAllowedToSend);
|
||||
}
|
||||
|
||||
function setRemoteReceiver(address _receiver) external onlyRole(REMOTE_RECEIVER_SETTER_ROLE) {
|
||||
if (remoteReceivers[_receiver]) {
|
||||
revert MessageReceiverAlreadySet(_receiver);
|
||||
}
|
||||
|
||||
remoteReceivers[_receiver] = true;
|
||||
emit RemoteReceiverSet(_receiver);
|
||||
}
|
||||
|
||||
function sendMessage(
|
||||
address _to,
|
||||
uint256 _fee,
|
||||
bytes calldata _calldata
|
||||
) external payable override(L1MessageService, IMessageService) whenTypeAndGeneralNotPaused(PauseType.L1_L2) {
|
||||
if (msg.value > 0) {
|
||||
revert DirectETHSendingDisallowed();
|
||||
}
|
||||
|
||||
if (_fee > 0) {
|
||||
revert FeeSendingDisallowed();
|
||||
}
|
||||
|
||||
if (remoteReceivers[_to] && !allowedMessageSenders[msg.sender]) {
|
||||
revert OnlyAllowedSendersToRemoteReceiver();
|
||||
}
|
||||
|
||||
_sendMessage(_to, _fee, _calldata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Claims and delivers a cross-chain message using a Merkle proof.
|
||||
* @dev if tree depth is empty, it will revert with L2MerkleRootDoesNotExist.
|
||||
* @dev if tree depth is different than proof size, it will revert with ProofLengthDifferentThanMerkleDepth.
|
||||
* @param _params Collection of claim data with proof and supporting data.
|
||||
*/
|
||||
function _claimMessageWithProof(ClaimMessageWithProofParams calldata _params) internal virtual override {
|
||||
// custom code here
|
||||
|
||||
if (_params.value > 0) {
|
||||
revert DirectETHSendingDisallowed();
|
||||
}
|
||||
|
||||
if (_params.fee > 0) {
|
||||
revert DirectETHSendingDisallowed();
|
||||
}
|
||||
|
||||
// is this from the remote bridge? and are they sending back to someone they are allowed to send back to?
|
||||
// NB: TBD - This could also be done as pairs vs. open ended.
|
||||
if (remoteReceivers[_params.from] && !allowedMessageSenders[_params.to]) {
|
||||
revert OnlyFromRemoteReceiverToAllowedSender();
|
||||
}
|
||||
|
||||
super._claimMessageWithProof(_params);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import { LineaRollup } from "../../../rollup/LineaRollup.sol";
|
||||
|
||||
/// @custom:oz-upgrades-unsafe-allow missing-initializer
|
||||
contract TestLineaRollup is LineaRollup {
|
||||
function setFallbackOperatorAddress(address _fallbackOperator) external {
|
||||
fallbackOperator = _fallbackOperator;
|
||||
}
|
||||
|
||||
function addRollingHash(uint256 _messageNumber, bytes32 _messageHash) external {
|
||||
_addRollingHash(_messageNumber, _messageHash);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { PauseManager } from "../../../security/pausing/PauseManager.sol";
|
||||
import { TestSetPauseTypeRoles } from "./TestSetPauseTypeRoles.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import { RateLimiter } from "../../../security/limiting/RateLimiter.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { PauseManager } from "../../../security/pausing/PauseManager.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/// @dev Test contract to test LXP-L minting
|
||||
interface ITransferSurgeXP {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
// Code generated by gnark DO NOT EDIT
|
||||
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
contract TestPlonkVerifierForDataAggregation {
|
||||
uint256 private constant R_MOD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { ERC20PermitUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { BridgedToken } from "./BridgedToken.sol";
|
||||
|
||||
|
||||
@@ -1,134 +1,15 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
|
||||
import { ITokenBridge } from "./interfaces/ITokenBridge.sol";
|
||||
import { IMessageService } from "../../messaging/interfaces/IMessageService.sol";
|
||||
import { TokenBridgeBase } from "./TokenBridgeBase.sol";
|
||||
|
||||
import { IERC20PermitUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20PermitUpgradeable.sol";
|
||||
import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol";
|
||||
import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
|
||||
import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
|
||||
import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
|
||||
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
|
||||
|
||||
import { BridgedToken } from "./BridgedToken.sol";
|
||||
import { MessageServiceBase } from "../../messaging/MessageServiceBase.sol";
|
||||
|
||||
import { TokenBridgePauseManager } from "../../security/pausing/TokenBridgePauseManager.sol";
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { StorageFiller39 } from "./utils/StorageFiller39.sol";
|
||||
import { PermissionsManager } from "../../security/access/PermissionsManager.sol";
|
||||
|
||||
import { EfficientLeftRightKeccak } from "../../libraries/EfficientLeftRightKeccak.sol";
|
||||
/**
|
||||
* @title Linea Canonical Token Bridge
|
||||
* @notice Contract to manage cross-chain ERC-20 bridging.
|
||||
* @author ConsenSys Software Inc.
|
||||
* @custom:security-contact security-report@linea.build
|
||||
*/
|
||||
contract TokenBridge is
|
||||
ITokenBridge,
|
||||
ReentrancyGuardUpgradeable,
|
||||
AccessControlUpgradeable,
|
||||
MessageServiceBase,
|
||||
TokenBridgePauseManager,
|
||||
PermissionsManager,
|
||||
StorageFiller39
|
||||
{
|
||||
using EfficientLeftRightKeccak for *;
|
||||
using SafeERC20Upgradeable for IERC20Upgradeable;
|
||||
|
||||
/// @dev This is the ABI version and not the reinitialize version.
|
||||
string public constant CONTRACT_VERSION = "1.1";
|
||||
|
||||
/// @notice Role used for setting the message service address.
|
||||
bytes32 public constant SET_MESSAGE_SERVICE_ROLE = keccak256("SET_MESSAGE_SERVICE_ROLE");
|
||||
|
||||
/// @notice Role used for setting a reserved token address.
|
||||
bytes32 public constant SET_RESERVED_TOKEN_ROLE = keccak256("SET_RESERVED_TOKEN_ROLE");
|
||||
|
||||
/// @notice Role used for removing a reserved token address.
|
||||
bytes32 public constant REMOVE_RESERVED_TOKEN_ROLE = keccak256("REMOVE_RESERVED_TOKEN_ROLE");
|
||||
|
||||
/// @notice Role used for setting a custom token contract address.
|
||||
bytes32 public constant SET_CUSTOM_CONTRACT_ROLE = keccak256("SET_CUSTOM_CONTRACT_ROLE");
|
||||
|
||||
// Special addresses used in the mappings to mark specific states for tokens.
|
||||
/// @notice EMPTY means a token is not present in the mapping.
|
||||
address internal constant EMPTY = address(0x0);
|
||||
/// @notice RESERVED means a token is reserved and cannot be bridged.
|
||||
address internal constant RESERVED_STATUS = address(0x111);
|
||||
/// @notice NATIVE means a token is native to the current local chain.
|
||||
address internal constant NATIVE_STATUS = address(0x222);
|
||||
/// @notice DEPLOYED means the bridged token contract has been deployed on the remote chain.
|
||||
address internal constant DEPLOYED_STATUS = address(0x333);
|
||||
|
||||
// solhint-disable-next-line var-name-mixedcase
|
||||
/// @dev The permit selector to be used when decoding the permit.
|
||||
bytes4 internal constant _PERMIT_SELECTOR = IERC20PermitUpgradeable.permit.selector;
|
||||
|
||||
/// @notice These 3 variables are used for the token metadata.
|
||||
bytes private constant METADATA_NAME = abi.encodeCall(IERC20MetadataUpgradeable.name, ());
|
||||
bytes private constant METADATA_SYMBOL = abi.encodeCall(IERC20MetadataUpgradeable.symbol, ());
|
||||
bytes private constant METADATA_DECIMALS = abi.encodeCall(IERC20MetadataUpgradeable.decimals, ());
|
||||
|
||||
/// @dev These 3 values are used when checking for token decimals and string values.
|
||||
uint256 private constant VALID_DECIMALS_ENCODING_LENGTH = 32;
|
||||
uint256 private constant SHORT_STRING_ENCODING_LENGTH = 32;
|
||||
uint256 private constant MINIMUM_STRING_ABI_DECODE_LENGTH = 64;
|
||||
|
||||
/// @notice The token beacon for deployed tokens.
|
||||
address public tokenBeacon;
|
||||
|
||||
/// @notice The chainId mapped to a native token address which is then mapped to the bridged token address.
|
||||
mapping(uint256 chainId => mapping(address native => address bridged)) public nativeToBridgedToken;
|
||||
|
||||
/// @notice The bridged token address mapped to the native token address.
|
||||
mapping(address bridged => address native) public bridgedToNativeToken;
|
||||
|
||||
/// @notice The current layer's chainId from where the bridging is triggered.
|
||||
uint256 public sourceChainId;
|
||||
|
||||
/// @notice The targeted layer's chainId where the bridging is received.
|
||||
uint256 public targetChainId;
|
||||
|
||||
/// @dev Keep free storage slots for future implementation updates to avoid storage collision.
|
||||
uint256[50] private __gap;
|
||||
|
||||
/// @dev Ensures the token has not been bridged before.
|
||||
modifier isNewToken(address _token) {
|
||||
if (bridgedToNativeToken[_token] != EMPTY || nativeToBridgedToken[sourceChainId][_token] != EMPTY)
|
||||
revert AlreadyBridgedToken(_token);
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Ensures the address is not address(0).
|
||||
* @param _addr Address to check.
|
||||
*/
|
||||
modifier nonZeroAddress(address _addr) {
|
||||
if (_addr == EMPTY) revert ZeroAddressNotAllowed();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Ensures the amount is not 0.
|
||||
* @param _amount amount to check.
|
||||
*/
|
||||
modifier nonZeroAmount(uint256 _amount) {
|
||||
if (_amount == 0) revert ZeroAmountNotAllowed(_amount);
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Ensures the chainId is not 0.
|
||||
* @param _chainId chainId to check.
|
||||
*/
|
||||
modifier nonZeroChainId(uint256 _chainId) {
|
||||
if (_chainId == 0) revert ZeroChainIdNotAllowed();
|
||||
_;
|
||||
}
|
||||
|
||||
contract TokenBridge is TokenBridgeBase {
|
||||
/// @dev Disable constructor for safety
|
||||
/// @custom:oz-upgrades-unsafe-allow constructor
|
||||
constructor() {
|
||||
@@ -150,429 +31,6 @@ contract TokenBridge is
|
||||
nonZeroChainId(_initializationData.targetChainId)
|
||||
initializer
|
||||
{
|
||||
__ReentrancyGuard_init();
|
||||
__MessageServiceBase_init(_initializationData.messageService);
|
||||
__PauseManager_init(_initializationData.pauseTypeRoles, _initializationData.unpauseTypeRoles);
|
||||
|
||||
if (_initializationData.defaultAdmin == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev DEFAULT_ADMIN_ROLE is set for the security council explicitly,
|
||||
* as the permissions init purposefully does not allow DEFAULT_ADMIN_ROLE to be set.
|
||||
*/
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, _initializationData.defaultAdmin);
|
||||
|
||||
__Permissions_init(_initializationData.roleAddresses);
|
||||
|
||||
tokenBeacon = _initializationData.tokenBeacon;
|
||||
if (_initializationData.sourceChainId == _initializationData.targetChainId) revert SourceChainSameAsTargetChain();
|
||||
_setRemoteSender(_initializationData.remoteSender);
|
||||
sourceChainId = _initializationData.sourceChainId;
|
||||
targetChainId = _initializationData.targetChainId;
|
||||
|
||||
unchecked {
|
||||
for (uint256 i; i < _initializationData.reservedTokens.length; ) {
|
||||
if (_initializationData.reservedTokens[i] == EMPTY) revert ZeroAddressNotAllowed();
|
||||
nativeToBridgedToken[_initializationData.sourceChainId][
|
||||
_initializationData.reservedTokens[i]
|
||||
] = RESERVED_STATUS;
|
||||
emit TokenReserved(_initializationData.reservedTokens[i]);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice This function is the single entry point to bridge tokens to the
|
||||
* other chain, both for native and already bridged tokens. You can use it
|
||||
* to bridge any ERC-20. If the token is bridged for the first time an ERC-20
|
||||
* (BridgedToken.sol) will be automatically deployed on the target chain.
|
||||
* @dev User should first allow the bridge to transfer tokens on his behalf.
|
||||
* Alternatively, you can use BridgeTokenWithPermit to do so in a single
|
||||
* transaction. If you want the transfer to be automatically executed on the
|
||||
* destination chain. You should send enough ETH to pay the postman fees.
|
||||
* Note that Linea can reserve some tokens (which use a dedicated bridge).
|
||||
* In this case, the token cannot be bridged. Linea can only reserve tokens
|
||||
* that have not been bridged yet.
|
||||
* Linea can pause the bridge for security reason. In this case new bridge
|
||||
* transaction would revert.
|
||||
* @dev Note: If, when bridging an unbridged token and decimals are unknown,
|
||||
* the call will revert to prevent mismatched decimals. Only those ERC-20s,
|
||||
* with a decimals function are supported.
|
||||
* @param _token The address of the token to be bridged.
|
||||
* @param _amount The amount of the token to be bridged.
|
||||
* @param _recipient The address that will receive the tokens on the other chain.
|
||||
*/
|
||||
function bridgeToken(
|
||||
address _token,
|
||||
uint256 _amount,
|
||||
address _recipient
|
||||
) public payable nonZeroAddress(_token) nonZeroAddress(_recipient) nonZeroAmount(_amount) nonReentrant {
|
||||
_requireTypeAndGeneralNotPaused(PauseType.INITIATE_TOKEN_BRIDGING);
|
||||
uint256 sourceChainIdCache = sourceChainId;
|
||||
address nativeMappingValue = nativeToBridgedToken[sourceChainIdCache][_token];
|
||||
if (nativeMappingValue == RESERVED_STATUS) {
|
||||
// Token is reserved
|
||||
revert ReservedToken(_token);
|
||||
}
|
||||
|
||||
address nativeToken = bridgedToNativeToken[_token];
|
||||
uint256 chainId;
|
||||
bytes memory tokenMetadata;
|
||||
|
||||
if (nativeToken != EMPTY) {
|
||||
BridgedToken(_token).burn(msg.sender, _amount);
|
||||
chainId = targetChainId;
|
||||
} else {
|
||||
// Token is native
|
||||
|
||||
// For tokens with special fee logic, ensure that only the amount received
|
||||
// by the bridge will be minted on the target chain.
|
||||
uint256 balanceBefore = IERC20Upgradeable(_token).balanceOf(address(this));
|
||||
IERC20Upgradeable(_token).safeTransferFrom(msg.sender, address(this), _amount);
|
||||
_amount = IERC20Upgradeable(_token).balanceOf(address(this)) - balanceBefore;
|
||||
nativeToken = _token;
|
||||
|
||||
if (nativeMappingValue == EMPTY) {
|
||||
// New token
|
||||
nativeToBridgedToken[sourceChainIdCache][_token] = NATIVE_STATUS;
|
||||
emit NewToken(_token);
|
||||
}
|
||||
|
||||
// Send Metadata only when the token has not been deployed on the other chain yet
|
||||
if (nativeMappingValue != DEPLOYED_STATUS) {
|
||||
tokenMetadata = abi.encode(_safeName(_token), _safeSymbol(_token), _safeDecimals(_token));
|
||||
}
|
||||
chainId = sourceChainIdCache;
|
||||
}
|
||||
messageService.sendMessage{ value: msg.value }(
|
||||
remoteSender,
|
||||
msg.value, // fees
|
||||
abi.encodeCall(ITokenBridge.completeBridging, (nativeToken, _amount, _recipient, chainId, tokenMetadata))
|
||||
);
|
||||
emit BridgingInitiatedV2(msg.sender, _recipient, _token, _amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Similar to `bridgeToken` function but allows to pass additional
|
||||
* permit data to do the ERC-20 approval in a single transaction.
|
||||
* @notice _permit can fail silently, don't rely on this function passing as a form
|
||||
* of authentication
|
||||
* @dev There is no need for validation at this level as the validation on pausing,
|
||||
* and empty values exists on the "bridgeToken" call.
|
||||
* @param _token The address of the token to be bridged.
|
||||
* @param _amount The amount of the token to be bridged.
|
||||
* @param _recipient The address that will receive the tokens on the other chain.
|
||||
* @param _permitData The permit data for the token, if applicable.
|
||||
*/
|
||||
function bridgeTokenWithPermit(
|
||||
address _token,
|
||||
uint256 _amount,
|
||||
address _recipient,
|
||||
bytes calldata _permitData
|
||||
) external payable {
|
||||
if (_permitData.length != 0) {
|
||||
_permit(_token, _permitData);
|
||||
}
|
||||
bridgeToken(_token, _amount, _recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev It can only be called from the Message Service. To finalize the bridging
|
||||
* process, a user or postman needs to use the `claimMessage` function of the
|
||||
* Message Service to trigger the transaction.
|
||||
* @param _nativeToken The address of the token on its native chain.
|
||||
* @param _amount The amount of the token to be received.
|
||||
* @param _recipient The address that will receive the tokens.
|
||||
* @param _chainId The token's origin layer chainId
|
||||
* @param _tokenMetadata Additional data used to deploy the bridged token if it
|
||||
* doesn't exist already.
|
||||
*/
|
||||
function completeBridging(
|
||||
address _nativeToken,
|
||||
uint256 _amount,
|
||||
address _recipient,
|
||||
uint256 _chainId,
|
||||
bytes calldata _tokenMetadata
|
||||
)
|
||||
external
|
||||
nonReentrant
|
||||
onlyMessagingService
|
||||
onlyAuthorizedRemoteSender
|
||||
whenTypeAndGeneralNotPaused(PauseType.COMPLETE_TOKEN_BRIDGING)
|
||||
{
|
||||
address nativeMappingValue = nativeToBridgedToken[_chainId][_nativeToken];
|
||||
address bridgedToken;
|
||||
|
||||
if (nativeMappingValue == NATIVE_STATUS || nativeMappingValue == DEPLOYED_STATUS) {
|
||||
// Token is native on the local chain
|
||||
IERC20Upgradeable(_nativeToken).safeTransfer(_recipient, _amount);
|
||||
} else {
|
||||
bridgedToken = nativeMappingValue;
|
||||
if (nativeMappingValue == EMPTY) {
|
||||
// New token
|
||||
bridgedToken = deployBridgedToken(_nativeToken, _tokenMetadata, sourceChainId);
|
||||
bridgedToNativeToken[bridgedToken] = _nativeToken;
|
||||
nativeToBridgedToken[targetChainId][_nativeToken] = bridgedToken;
|
||||
}
|
||||
BridgedToken(bridgedToken).mint(_recipient, _amount);
|
||||
}
|
||||
emit BridgingFinalizedV2(_nativeToken, bridgedToken, _amount, _recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Change the address of the Message Service.
|
||||
* @dev SET_MESSAGE_SERVICE_ROLE is required to execute.
|
||||
* @param _messageService The address of the new Message Service.
|
||||
*/
|
||||
function setMessageService(
|
||||
address _messageService
|
||||
) external nonZeroAddress(_messageService) onlyRole(SET_MESSAGE_SERVICE_ROLE) {
|
||||
address oldMessageService = address(messageService);
|
||||
messageService = IMessageService(_messageService);
|
||||
emit MessageServiceUpdated(_messageService, oldMessageService, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Change the status to DEPLOYED to the tokens passed in parameter
|
||||
* Will call the method setDeployed on the other chain using the message Service
|
||||
* @param _tokens Array of bridged tokens that have been deployed.
|
||||
*/
|
||||
function confirmDeployment(address[] memory _tokens) external payable {
|
||||
uint256 tokensLength = _tokens.length;
|
||||
if (tokensLength == 0) {
|
||||
revert TokenListEmpty();
|
||||
}
|
||||
|
||||
// Check that the tokens have actually been deployed
|
||||
for (uint256 i; i < tokensLength; i++) {
|
||||
address nativeToken = bridgedToNativeToken[_tokens[i]];
|
||||
if (nativeToken == EMPTY) {
|
||||
revert TokenNotDeployed(_tokens[i]);
|
||||
}
|
||||
_tokens[i] = nativeToken;
|
||||
}
|
||||
|
||||
messageService.sendMessage{ value: msg.value }(
|
||||
remoteSender,
|
||||
msg.value, // fees
|
||||
abi.encodeCall(ITokenBridge.setDeployed, (_tokens))
|
||||
);
|
||||
|
||||
emit DeploymentConfirmed(_tokens, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Change the status of tokens to DEPLOYED. New bridge transaction will not
|
||||
* contain token metadata, which save gas.
|
||||
* Can only be called from the Message Service. A user or postman needs to use
|
||||
* the `claimMessage` function of the Message Service to trigger the transaction.
|
||||
* @param _nativeTokens Array of native tokens for which the DEPLOYED status must be set.
|
||||
*/
|
||||
function setDeployed(address[] calldata _nativeTokens) external onlyMessagingService onlyAuthorizedRemoteSender {
|
||||
unchecked {
|
||||
uint256 cachedSourceChainId = sourceChainId;
|
||||
for (uint256 i; i < _nativeTokens.length; ) {
|
||||
nativeToBridgedToken[cachedSourceChainId][_nativeTokens[i]] = DEPLOYED_STATUS;
|
||||
emit TokenDeployed(_nativeTokens[i]);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deploy a new EC20 contract for bridged token using a beacon proxy pattern.
|
||||
* To adapt to future requirements, Linea can update the implementation of
|
||||
* all (existing and future) contracts by updating the beacon. This update is
|
||||
* subject to a delay by a time lock.
|
||||
* Contracts are deployed using CREATE2 so deployment address is deterministic.
|
||||
* @param _nativeToken The address of the native token on the source chain.
|
||||
* @param _tokenMetadata The encoded metadata for the token.
|
||||
* @param _chainId The chain id on which the token will be deployed, used to calculate the salt
|
||||
* @return bridgedTokenAddress The address of the newly deployed BridgedToken contract.
|
||||
*/
|
||||
function deployBridgedToken(
|
||||
address _nativeToken,
|
||||
bytes calldata _tokenMetadata,
|
||||
uint256 _chainId
|
||||
) internal returns (address bridgedTokenAddress) {
|
||||
bridgedTokenAddress = address(
|
||||
new BeaconProxy{ salt: EfficientLeftRightKeccak._efficientKeccak(_chainId, _nativeToken) }(tokenBeacon, "")
|
||||
);
|
||||
|
||||
(string memory name, string memory symbol, uint8 decimals) = abi.decode(_tokenMetadata, (string, string, uint8));
|
||||
BridgedToken(bridgedTokenAddress).initialize(name, symbol, decimals);
|
||||
emit NewTokenDeployed(bridgedTokenAddress, _nativeToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Linea can reserve tokens. In this case, the token cannot be bridged.
|
||||
* Linea can only reserve tokens that have not been bridged before.
|
||||
* @dev SET_RESERVED_TOKEN_ROLE is required to execute.
|
||||
* @notice Make sure that _token is native to the current chain
|
||||
* where you are calling this function from
|
||||
* @param _token The address of the token to be set as reserved.
|
||||
*/
|
||||
function setReserved(
|
||||
address _token
|
||||
) external nonZeroAddress(_token) isNewToken(_token) onlyRole(SET_RESERVED_TOKEN_ROLE) {
|
||||
nativeToBridgedToken[sourceChainId][_token] = RESERVED_STATUS;
|
||||
emit TokenReserved(_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Removes a token from the reserved list.
|
||||
* @dev REMOVE_RESERVED_TOKEN_ROLE is required to execute.
|
||||
* @param _token The address of the token to be removed from the reserved list.
|
||||
*/
|
||||
function removeReserved(address _token) external nonZeroAddress(_token) onlyRole(REMOVE_RESERVED_TOKEN_ROLE) {
|
||||
uint256 cachedSourceChainId = sourceChainId;
|
||||
|
||||
if (nativeToBridgedToken[cachedSourceChainId][_token] != RESERVED_STATUS) revert NotReserved(_token);
|
||||
nativeToBridgedToken[cachedSourceChainId][_token] = EMPTY;
|
||||
|
||||
emit ReservationRemoved(_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Linea can set a custom ERC-20 contract for specific ERC-20.
|
||||
* For security purposes, Linea can only call this function if the token has
|
||||
* not been bridged yet.
|
||||
* @dev SET_CUSTOM_CONTRACT_ROLE is required to execute.
|
||||
* @param _nativeToken The address of the token on the source chain.
|
||||
* @param _targetContract The address of the custom contract.
|
||||
*/
|
||||
function setCustomContract(
|
||||
address _nativeToken,
|
||||
address _targetContract
|
||||
)
|
||||
external
|
||||
nonZeroAddress(_nativeToken)
|
||||
nonZeroAddress(_targetContract)
|
||||
onlyRole(SET_CUSTOM_CONTRACT_ROLE)
|
||||
isNewToken(_nativeToken)
|
||||
{
|
||||
if (bridgedToNativeToken[_targetContract] != EMPTY) {
|
||||
revert AlreadyBrigedToNativeTokenSet(_targetContract);
|
||||
}
|
||||
if (_targetContract == NATIVE_STATUS || _targetContract == DEPLOYED_STATUS || _targetContract == RESERVED_STATUS) {
|
||||
revert StatusAddressNotAllowed(_targetContract);
|
||||
}
|
||||
|
||||
uint256 cachedTargetChainId = targetChainId;
|
||||
|
||||
if (nativeToBridgedToken[cachedTargetChainId][_nativeToken] != EMPTY) {
|
||||
revert NativeToBridgedTokenAlreadySet(_nativeToken);
|
||||
}
|
||||
|
||||
nativeToBridgedToken[cachedTargetChainId][_nativeToken] = _targetContract;
|
||||
bridgedToNativeToken[_targetContract] = _nativeToken;
|
||||
emit CustomContractSet(_nativeToken, _targetContract, msg.sender);
|
||||
}
|
||||
|
||||
// Helpers to safely get the metadata from a token, inspired by
|
||||
// https://github.com/traderjoe-xyz/joe-core/blob/main/contracts/MasterChefJoeV3.sol#L55-L95
|
||||
|
||||
/**
|
||||
* @dev Provides a safe ERC-20.name version which returns 'NO_NAME' as fallback string.
|
||||
* @param _token The address of the ERC-20 token contract
|
||||
* @return tokenName Returns the string of the token name.
|
||||
*/
|
||||
function _safeName(address _token) internal view returns (string memory tokenName) {
|
||||
(bool success, bytes memory data) = _token.staticcall(METADATA_NAME);
|
||||
tokenName = success ? _returnDataToString(data) : "NO_NAME";
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Provides a safe ERC-20.symbol version which returns 'NO_SYMBOL' as fallback string
|
||||
* @param _token The address of the ERC-20 token contract
|
||||
* @return symbol Returns the string of the symbol.
|
||||
*/
|
||||
function _safeSymbol(address _token) internal view returns (string memory symbol) {
|
||||
(bool success, bytes memory data) = _token.staticcall(METADATA_SYMBOL);
|
||||
symbol = success ? _returnDataToString(data) : "NO_SYMBOL";
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Provides a safe ERC-20.decimals version which reverts when decimals are unknown
|
||||
* Note Tokens with (decimals > 255) are not supported
|
||||
* @param _token The address of the ERC-20 token contract
|
||||
* @return Returns the token's decimals value.
|
||||
*/
|
||||
function _safeDecimals(address _token) internal view returns (uint8) {
|
||||
(bool success, bytes memory data) = _token.staticcall(METADATA_DECIMALS);
|
||||
|
||||
if (success && data.length == VALID_DECIMALS_ENCODING_LENGTH) {
|
||||
return abi.decode(data, (uint8));
|
||||
}
|
||||
|
||||
revert DecimalsAreUnknown(_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Converts returned data to string. Returns 'NOT_VALID_ENCODING' as fallback value.
|
||||
* @param _data returned data.
|
||||
* @return decodedString The decoded string data.
|
||||
*/
|
||||
function _returnDataToString(bytes memory _data) internal pure returns (string memory decodedString) {
|
||||
if (_data.length >= MINIMUM_STRING_ABI_DECODE_LENGTH) {
|
||||
return abi.decode(_data, (string));
|
||||
} else if (_data.length != SHORT_STRING_ENCODING_LENGTH) {
|
||||
return "NOT_VALID_ENCODING";
|
||||
}
|
||||
|
||||
// Since the strings on bytes32 are encoded left-right, check the first zero in the data
|
||||
uint256 nonZeroBytes;
|
||||
unchecked {
|
||||
while (nonZeroBytes < SHORT_STRING_ENCODING_LENGTH && _data[nonZeroBytes] != 0) {
|
||||
nonZeroBytes++;
|
||||
}
|
||||
}
|
||||
|
||||
// If the first one is 0, we do not handle the encoding
|
||||
if (nonZeroBytes == 0) {
|
||||
return "NOT_VALID_ENCODING";
|
||||
}
|
||||
// Create a byte array with nonZeroBytes length
|
||||
bytes memory bytesArray = new bytes(nonZeroBytes);
|
||||
unchecked {
|
||||
for (uint256 i; i < nonZeroBytes; ) {
|
||||
bytesArray[i] = _data[i];
|
||||
++i;
|
||||
}
|
||||
}
|
||||
decodedString = string(bytesArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Call the token permit method of extended ERC-20
|
||||
* @notice Only support tokens implementing ERC-2612
|
||||
* @param _token ERC-20 token address
|
||||
* @param _permitData Raw data of the call `permit` of the token
|
||||
*/
|
||||
function _permit(address _token, bytes calldata _permitData) internal {
|
||||
if (bytes4(_permitData[:4]) != _PERMIT_SELECTOR)
|
||||
revert InvalidPermitData(bytes4(_permitData[:4]), _PERMIT_SELECTOR);
|
||||
// Decode the permit data
|
||||
// The parameters are:
|
||||
// 1. owner: The address of the wallet holding the tokens
|
||||
// 2. spender: The address of the entity permitted to spend the tokens
|
||||
// 3. value: The maximum amount of tokens the spender is allowed to spend
|
||||
// 4. deadline: The time until which the permit is valid
|
||||
// 5. v: Part of the signature (along with r and s), these three values form the signature of the permit
|
||||
// 6. r: Part of the signature
|
||||
// 7. s: Part of the signature
|
||||
(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) = abi.decode(
|
||||
_permitData[4:],
|
||||
(address, address, uint256, uint256, uint8, bytes32, bytes32)
|
||||
);
|
||||
if (owner != msg.sender) revert PermitNotFromSender(owner);
|
||||
if (spender != address(this)) revert PermitNotAllowingBridge(spender);
|
||||
|
||||
if (IERC20Upgradeable(_token).allowance(owner, spender) < amount) {
|
||||
IERC20PermitUpgradeable(_token).permit(msg.sender, address(this), amount, deadline, v, r, s);
|
||||
}
|
||||
__TokenBridge_init(_initializationData);
|
||||
}
|
||||
}
|
||||
|
||||
630
contracts/src/bridging/token/TokenBridgeBase.sol
Normal file
630
contracts/src/bridging/token/TokenBridgeBase.sol
Normal file
@@ -0,0 +1,630 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { ITokenBridge } from "./interfaces/ITokenBridge.sol";
|
||||
import { IMessageService } from "../../messaging/interfaces/IMessageService.sol";
|
||||
|
||||
import { IERC20PermitUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20PermitUpgradeable.sol";
|
||||
import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol";
|
||||
import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
|
||||
import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
|
||||
import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
|
||||
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
|
||||
|
||||
import { BridgedToken } from "./BridgedToken.sol";
|
||||
import { MessageServiceBase } from "../../messaging/MessageServiceBase.sol";
|
||||
|
||||
import { TokenBridgePauseManager } from "../../security/pausing/TokenBridgePauseManager.sol";
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { StorageFiller39 } from "./utils/StorageFiller39.sol";
|
||||
import { PermissionsManager } from "../../security/access/PermissionsManager.sol";
|
||||
|
||||
import { EfficientLeftRightKeccak } from "../../libraries/EfficientLeftRightKeccak.sol";
|
||||
/**
|
||||
* @title Linea Canonical Token Bridge
|
||||
* @notice Contract to manage cross-chain ERC-20 bridging.
|
||||
* @author ConsenSys Software Inc.
|
||||
* @custom:security-contact security-report@linea.build
|
||||
*/
|
||||
abstract contract TokenBridgeBase is
|
||||
ITokenBridge,
|
||||
ReentrancyGuardUpgradeable,
|
||||
AccessControlUpgradeable,
|
||||
MessageServiceBase,
|
||||
TokenBridgePauseManager,
|
||||
PermissionsManager,
|
||||
StorageFiller39
|
||||
{
|
||||
using EfficientLeftRightKeccak for *;
|
||||
using SafeERC20Upgradeable for IERC20Upgradeable;
|
||||
|
||||
/// @notice Role used for setting the message service address.
|
||||
bytes32 public constant SET_MESSAGE_SERVICE_ROLE = keccak256("SET_MESSAGE_SERVICE_ROLE");
|
||||
|
||||
/// @notice Role used for setting a reserved token address.
|
||||
bytes32 public constant SET_RESERVED_TOKEN_ROLE = keccak256("SET_RESERVED_TOKEN_ROLE");
|
||||
|
||||
/// @notice Role used for removing a reserved token address.
|
||||
bytes32 public constant REMOVE_RESERVED_TOKEN_ROLE = keccak256("REMOVE_RESERVED_TOKEN_ROLE");
|
||||
|
||||
/// @notice Role used for setting a custom token contract address.
|
||||
bytes32 public constant SET_CUSTOM_CONTRACT_ROLE = keccak256("SET_CUSTOM_CONTRACT_ROLE");
|
||||
|
||||
// Special addresses used in the mappings to mark specific states for tokens.
|
||||
/// @notice EMPTY means a token is not present in the mapping.
|
||||
address internal constant EMPTY = address(0x0);
|
||||
/// @notice RESERVED means a token is reserved and cannot be bridged.
|
||||
address internal constant RESERVED_STATUS = address(0x111);
|
||||
/// @notice NATIVE means a token is native to the current local chain.
|
||||
address internal constant NATIVE_STATUS = address(0x222);
|
||||
/// @notice DEPLOYED means the bridged token contract has been deployed on the remote chain.
|
||||
address internal constant DEPLOYED_STATUS = address(0x333);
|
||||
|
||||
// solhint-disable-next-line var-name-mixedcase
|
||||
/// @dev The permit selector to be used when decoding the permit.
|
||||
bytes4 internal constant _PERMIT_SELECTOR = IERC20PermitUpgradeable.permit.selector;
|
||||
|
||||
/// @dev This is the ABI version and not the reinitialize version.
|
||||
string private constant _CONTRACT_VERSION = "1.1";
|
||||
|
||||
/// @notice These 3 variables are used for the token metadata.
|
||||
bytes private constant METADATA_NAME = abi.encodeCall(IERC20MetadataUpgradeable.name, ());
|
||||
bytes private constant METADATA_SYMBOL = abi.encodeCall(IERC20MetadataUpgradeable.symbol, ());
|
||||
bytes private constant METADATA_DECIMALS = abi.encodeCall(IERC20MetadataUpgradeable.decimals, ());
|
||||
|
||||
/// @dev These 3 values are used when checking for token decimals and string values.
|
||||
uint256 private constant VALID_DECIMALS_ENCODING_LENGTH = 32;
|
||||
uint256 private constant SHORT_STRING_ENCODING_LENGTH = 32;
|
||||
uint256 private constant MINIMUM_STRING_ABI_DECODE_LENGTH = 64;
|
||||
|
||||
/// @notice The token beacon for deployed tokens.
|
||||
address public tokenBeacon;
|
||||
|
||||
/// @notice The chainId mapped to a native token address which is then mapped to the bridged token address.
|
||||
mapping(uint256 chainId => mapping(address native => address bridged)) public nativeToBridgedToken;
|
||||
|
||||
/// @notice The bridged token address mapped to the native token address.
|
||||
mapping(address bridged => address native) public bridgedToNativeToken;
|
||||
|
||||
/// @notice The current layer's chainId from where the bridging is triggered.
|
||||
uint256 public sourceChainId;
|
||||
|
||||
/// @notice The targeted layer's chainId where the bridging is received.
|
||||
uint256 public targetChainId;
|
||||
|
||||
/// @dev Keep free storage slots for future implementation updates to avoid storage collision.
|
||||
uint256[50] private __gap;
|
||||
|
||||
/// @dev Ensures the token has not been bridged before.
|
||||
modifier isNewToken(address _token) {
|
||||
if (bridgedToNativeToken[_token] != EMPTY || nativeToBridgedToken[sourceChainId][_token] != EMPTY)
|
||||
revert AlreadyBridgedToken(_token);
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Ensures the address is not address(0).
|
||||
* @param _addr Address to check.
|
||||
*/
|
||||
modifier nonZeroAddress(address _addr) {
|
||||
if (_addr == EMPTY) revert ZeroAddressNotAllowed();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Ensures the amount is not 0.
|
||||
* @param _amount amount to check.
|
||||
*/
|
||||
modifier nonZeroAmount(uint256 _amount) {
|
||||
if (_amount == 0) revert ZeroAmountNotAllowed(_amount);
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Ensures the chainId is not 0.
|
||||
* @param _chainId chainId to check.
|
||||
*/
|
||||
modifier nonZeroChainId(uint256 _chainId) {
|
||||
if (_chainId == 0) revert ZeroChainIdNotAllowed();
|
||||
_;
|
||||
}
|
||||
|
||||
/// @dev Disable constructor for safety
|
||||
/// @custom:oz-upgrades-unsafe-allow constructor
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes TokenBridge and underlying service dependencies - used for new networks only.
|
||||
* @param _initializationData The initial data used for initializing the TokenBridge contract.
|
||||
*/
|
||||
function __TokenBridge_init(InitializationData calldata _initializationData) internal virtual {
|
||||
__ReentrancyGuard_init();
|
||||
__MessageServiceBase_init(_initializationData.messageService);
|
||||
__PauseManager_init(_initializationData.pauseTypeRoles, _initializationData.unpauseTypeRoles);
|
||||
|
||||
if (_initializationData.defaultAdmin == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev DEFAULT_ADMIN_ROLE is set for the security council explicitly,
|
||||
* as the permissions init purposefully does not allow DEFAULT_ADMIN_ROLE to be set.
|
||||
*/
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, _initializationData.defaultAdmin);
|
||||
|
||||
__Permissions_init(_initializationData.roleAddresses);
|
||||
|
||||
tokenBeacon = _initializationData.tokenBeacon;
|
||||
if (_initializationData.sourceChainId == _initializationData.targetChainId) revert SourceChainSameAsTargetChain();
|
||||
_setRemoteSender(_initializationData.remoteSender);
|
||||
sourceChainId = _initializationData.sourceChainId;
|
||||
targetChainId = _initializationData.targetChainId;
|
||||
|
||||
unchecked {
|
||||
for (uint256 i; i < _initializationData.reservedTokens.length; ) {
|
||||
if (_initializationData.reservedTokens[i] == EMPTY) revert ZeroAddressNotAllowed();
|
||||
nativeToBridgedToken[_initializationData.sourceChainId][
|
||||
_initializationData.reservedTokens[i]
|
||||
] = RESERVED_STATUS;
|
||||
emit TokenReserved(_initializationData.reservedTokens[i]);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the ABI version and not the reinitialize version.
|
||||
* @return contractVersion The contract ABI version.
|
||||
*/
|
||||
function CONTRACT_VERSION() external view virtual returns (string memory contractVersion) {
|
||||
contractVersion = _CONTRACT_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice This function is the single entry point to bridge tokens to the
|
||||
* other chain, both for native and already bridged tokens. You can use it
|
||||
* to bridge any ERC-20. If the token is bridged for the first time an ERC-20
|
||||
* (BridgedToken.sol) will be automatically deployed on the target chain.
|
||||
* @dev User should first allow the bridge to transfer tokens on his behalf.
|
||||
* Alternatively, you can use BridgeTokenWithPermit to do so in a single
|
||||
* transaction. If you want the transfer to be automatically executed on the
|
||||
* destination chain. You should send enough ETH to pay the postman fees.
|
||||
* Note that Linea can reserve some tokens (which use a dedicated bridge).
|
||||
* In this case, the token cannot be bridged. Linea can only reserve tokens
|
||||
* that have not been bridged yet.
|
||||
* Linea can pause the bridge for security reason. In this case new bridge
|
||||
* transaction would revert.
|
||||
* @dev Note: If, when bridging an unbridged token and decimals are unknown,
|
||||
* the call will revert to prevent mismatched decimals. Only those ERC-20s,
|
||||
* with a decimals function are supported.
|
||||
* @param _token The address of the token to be bridged.
|
||||
* @param _amount The amount of the token to be bridged.
|
||||
* @param _recipient The address that will receive the tokens on the other chain.
|
||||
*/
|
||||
function bridgeToken(
|
||||
address _token,
|
||||
uint256 _amount,
|
||||
address _recipient
|
||||
) public payable virtual nonReentrant whenTypeAndGeneralNotPaused(PauseType.INITIATE_TOKEN_BRIDGING) {
|
||||
_bridgeToken(_token, _amount, _recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice This function is the internal implementation of bridgeToken.
|
||||
* @param _token The address of the token to be bridged.
|
||||
* @param _amount The amount of the token to be bridged.
|
||||
* @param _recipient The address that will receive the tokens on the other chain.
|
||||
*/
|
||||
function _bridgeToken(
|
||||
address _token,
|
||||
uint256 _amount,
|
||||
address _recipient
|
||||
) internal virtual nonZeroAddress(_token) nonZeroAddress(_recipient) nonZeroAmount(_amount) {
|
||||
uint256 sourceChainIdCache = sourceChainId;
|
||||
address nativeMappingValue = nativeToBridgedToken[sourceChainIdCache][_token];
|
||||
if (nativeMappingValue == RESERVED_STATUS) {
|
||||
revert ReservedToken(_token);
|
||||
}
|
||||
|
||||
address nativeToken = bridgedToNativeToken[_token];
|
||||
uint256 chainId;
|
||||
bytes memory tokenMetadata;
|
||||
|
||||
if (nativeToken != EMPTY) {
|
||||
BridgedToken(_token).burn(msg.sender, _amount);
|
||||
chainId = targetChainId;
|
||||
} else {
|
||||
// Token is native
|
||||
|
||||
_amount = _tranferTokensToEscrow(_token, _amount);
|
||||
|
||||
nativeToken = _token;
|
||||
|
||||
if (nativeMappingValue == EMPTY) {
|
||||
// New token
|
||||
nativeToBridgedToken[sourceChainIdCache][_token] = NATIVE_STATUS;
|
||||
emit NewToken(_token);
|
||||
}
|
||||
|
||||
// Send Metadata only when the token has not been deployed on the other chain yet
|
||||
if (nativeMappingValue != DEPLOYED_STATUS) {
|
||||
tokenMetadata = abi.encode(_safeName(_token), _safeSymbol(_token), _safeDecimals(_token));
|
||||
}
|
||||
chainId = sourceChainIdCache;
|
||||
}
|
||||
messageService.sendMessage{ value: msg.value }(
|
||||
remoteSender,
|
||||
msg.value, // fees
|
||||
abi.encodeCall(ITokenBridge.completeBridging, (nativeToken, _amount, _recipient, chainId, tokenMetadata))
|
||||
);
|
||||
emit BridgingInitiatedV2(msg.sender, _recipient, _token, _amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Transfers the token to the escrow address (default address(this)).
|
||||
* @param _token The address of the token to be bridged.
|
||||
* @param _amount The amount of the token to be bridged.
|
||||
* @return amount The real amount being transferred to the recipient.
|
||||
*/
|
||||
function _tranferTokensToEscrow(address _token, uint256 _amount) internal virtual returns (uint256 amount) {
|
||||
// For tokens with special fee logic, ensure that only the amount received by the bridge will be minted on the target chain.
|
||||
address escrowAddress = _getEscrowAddress(_token);
|
||||
uint256 balanceBefore = IERC20Upgradeable(_token).balanceOf(escrowAddress);
|
||||
IERC20Upgradeable(_token).safeTransferFrom(msg.sender, escrowAddress, _amount);
|
||||
amount = IERC20Upgradeable(_token).balanceOf(escrowAddress) - balanceBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Retrieves the escrow address (default address(this)).
|
||||
* @dev _token is only used in overrides allowing different escrow per token.
|
||||
* @param _token The address of the token to be bridged.
|
||||
* @return escrowAddress The address of where the bridged local token will be kept.
|
||||
*/
|
||||
function _getEscrowAddress(address _token) internal view virtual returns (address escrowAddress) {
|
||||
escrowAddress = address(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Similar to `bridgeToken` function but allows to pass additional
|
||||
* permit data to do the ERC-20 approval in a single transaction.
|
||||
* @notice _permit can fail silently, don't rely on this function passing as a form
|
||||
* of authentication
|
||||
* @dev There is no need for validation at this level as the validation on pausing,
|
||||
* and empty values exists on the "bridgeToken" call.
|
||||
* @param _token The address of the token to be bridged.
|
||||
* @param _amount The amount of the token to be bridged.
|
||||
* @param _recipient The address that will receive the tokens on the other chain.
|
||||
* @param _permitData The permit data for the token, if applicable.
|
||||
*/
|
||||
function bridgeTokenWithPermit(
|
||||
address _token,
|
||||
uint256 _amount,
|
||||
address _recipient,
|
||||
bytes calldata _permitData
|
||||
) external payable virtual nonReentrant whenTypeAndGeneralNotPaused(PauseType.INITIATE_TOKEN_BRIDGING) {
|
||||
if (_permitData.length != 0) {
|
||||
_permit(_token, _permitData);
|
||||
}
|
||||
_bridgeToken(_token, _amount, _recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Completes bridging deploying new tokens if needed.
|
||||
* @dev It can only be called from the Message Service. To finalize the bridging
|
||||
* process, a user or postman needs to use the `claimMessage` function of the
|
||||
* Message Service to trigger the transaction.
|
||||
* @param _nativeToken The address of the token on its native chain.
|
||||
* @param _amount The amount of the token to be received.
|
||||
* @param _recipient The address that will receive the tokens.
|
||||
* @param _chainId The token's origin layer chainId
|
||||
* @param _tokenMetadata Additional data used to deploy the bridged token if it
|
||||
* doesn't exist already.
|
||||
*/
|
||||
function completeBridging(
|
||||
address _nativeToken,
|
||||
uint256 _amount,
|
||||
address _recipient,
|
||||
uint256 _chainId,
|
||||
bytes calldata _tokenMetadata
|
||||
)
|
||||
external
|
||||
virtual
|
||||
nonReentrant
|
||||
onlyMessagingService
|
||||
onlyAuthorizedRemoteSender
|
||||
whenTypeAndGeneralNotPaused(PauseType.COMPLETE_TOKEN_BRIDGING)
|
||||
{
|
||||
_completeBridging(_nativeToken, _amount, _recipient, _chainId, _tokenMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Completes bridging.
|
||||
* @param _nativeToken The address of the token on its native chain.
|
||||
* @param _amount The amount of the token to be received.
|
||||
* @param _recipient The address that will receive the tokens.
|
||||
* @param _chainId The token's origin layer chainId
|
||||
* @param _tokenMetadata Additional data used to deploy the bridged token if it
|
||||
* doesn't exist already.
|
||||
*/
|
||||
function _completeBridging(
|
||||
address _nativeToken,
|
||||
uint256 _amount,
|
||||
address _recipient,
|
||||
uint256 _chainId,
|
||||
bytes calldata _tokenMetadata
|
||||
) internal virtual {
|
||||
address nativeMappingValue = nativeToBridgedToken[_chainId][_nativeToken];
|
||||
address bridgedToken;
|
||||
|
||||
if (nativeMappingValue == NATIVE_STATUS || nativeMappingValue == DEPLOYED_STATUS) {
|
||||
// Token is native on the local chain
|
||||
IERC20Upgradeable(_nativeToken).safeTransfer(_recipient, _amount);
|
||||
} else {
|
||||
bridgedToken = nativeMappingValue;
|
||||
if (nativeMappingValue == EMPTY) {
|
||||
// New token
|
||||
bridgedToken = deployBridgedToken(_nativeToken, _tokenMetadata, sourceChainId);
|
||||
bridgedToNativeToken[bridgedToken] = _nativeToken;
|
||||
nativeToBridgedToken[targetChainId][_nativeToken] = bridgedToken;
|
||||
}
|
||||
BridgedToken(bridgedToken).mint(_recipient, _amount);
|
||||
}
|
||||
emit BridgingFinalizedV2(_nativeToken, bridgedToken, _amount, _recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Change the address of the Message Service.
|
||||
* @dev SET_MESSAGE_SERVICE_ROLE is required to execute.
|
||||
* @param _messageService The address of the new Message Service.
|
||||
*/
|
||||
function setMessageService(
|
||||
address _messageService
|
||||
) external nonZeroAddress(_messageService) onlyRole(SET_MESSAGE_SERVICE_ROLE) {
|
||||
address oldMessageService = address(messageService);
|
||||
messageService = IMessageService(_messageService);
|
||||
emit MessageServiceUpdated(_messageService, oldMessageService, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Change the status to DEPLOYED to the tokens passed in parameter
|
||||
* Will call the method setDeployed on the other chain using the message Service
|
||||
* @param _tokens Array of bridged tokens that have been deployed.
|
||||
*/
|
||||
function confirmDeployment(address[] memory _tokens) external payable {
|
||||
uint256 tokensLength = _tokens.length;
|
||||
if (tokensLength == 0) {
|
||||
revert TokenListEmpty();
|
||||
}
|
||||
|
||||
// Check that the tokens have actually been deployed
|
||||
for (uint256 i; i < tokensLength; i++) {
|
||||
address nativeToken = bridgedToNativeToken[_tokens[i]];
|
||||
if (nativeToken == EMPTY) {
|
||||
revert TokenNotDeployed(_tokens[i]);
|
||||
}
|
||||
_tokens[i] = nativeToken;
|
||||
}
|
||||
|
||||
messageService.sendMessage{ value: msg.value }(
|
||||
remoteSender,
|
||||
msg.value, // fees
|
||||
abi.encodeCall(ITokenBridge.setDeployed, (_tokens))
|
||||
);
|
||||
|
||||
emit DeploymentConfirmed(_tokens, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Change the status of tokens to DEPLOYED. New bridge transaction will not
|
||||
* contain token metadata, which save gas.
|
||||
* Can only be called from the Message Service. A user or postman needs to use
|
||||
* the `claimMessage` function of the Message Service to trigger the transaction.
|
||||
* @param _nativeTokens Array of native tokens for which the DEPLOYED status must be set.
|
||||
*/
|
||||
function setDeployed(address[] calldata _nativeTokens) external onlyMessagingService onlyAuthorizedRemoteSender {
|
||||
unchecked {
|
||||
uint256 cachedSourceChainId = sourceChainId;
|
||||
for (uint256 i; i < _nativeTokens.length; ) {
|
||||
nativeToBridgedToken[cachedSourceChainId][_nativeTokens[i]] = DEPLOYED_STATUS;
|
||||
emit TokenDeployed(_nativeTokens[i]);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deploy a new EC20 contract for bridged token using a beacon proxy pattern.
|
||||
* To adapt to future requirements, Linea can update the implementation of
|
||||
* all (existing and future) contracts by updating the beacon. This update is
|
||||
* subject to a delay by a time lock.
|
||||
* Contracts are deployed using CREATE2 so deployment address is deterministic.
|
||||
* @param _nativeToken The address of the native token on the source chain.
|
||||
* @param _tokenMetadata The encoded metadata for the token.
|
||||
* @param _chainId The chain id on which the token will be deployed, used to calculate the salt
|
||||
* @return bridgedTokenAddress The address of the newly deployed BridgedToken contract.
|
||||
*/
|
||||
function deployBridgedToken(
|
||||
address _nativeToken,
|
||||
bytes calldata _tokenMetadata,
|
||||
uint256 _chainId
|
||||
) internal returns (address bridgedTokenAddress) {
|
||||
bridgedTokenAddress = address(
|
||||
new BeaconProxy{ salt: EfficientLeftRightKeccak._efficientKeccak(_chainId, _nativeToken) }(tokenBeacon, "")
|
||||
);
|
||||
|
||||
(string memory name, string memory symbol, uint8 decimals) = abi.decode(_tokenMetadata, (string, string, uint8));
|
||||
BridgedToken(bridgedTokenAddress).initialize(name, symbol, decimals);
|
||||
emit NewTokenDeployed(bridgedTokenAddress, _nativeToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Linea can reserve tokens. In this case, the token cannot be bridged.
|
||||
* Linea can only reserve tokens that have not been bridged before.
|
||||
* @dev SET_RESERVED_TOKEN_ROLE is required to execute.
|
||||
* @notice Make sure that _token is native to the current chain
|
||||
* where you are calling this function from
|
||||
* @param _token The address of the token to be set as reserved.
|
||||
*/
|
||||
function setReserved(
|
||||
address _token
|
||||
) external nonZeroAddress(_token) isNewToken(_token) onlyRole(SET_RESERVED_TOKEN_ROLE) {
|
||||
nativeToBridgedToken[sourceChainId][_token] = RESERVED_STATUS;
|
||||
emit TokenReserved(_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Removes a token from the reserved list.
|
||||
* @dev REMOVE_RESERVED_TOKEN_ROLE is required to execute.
|
||||
* @param _token The address of the token to be removed from the reserved list.
|
||||
*/
|
||||
function removeReserved(address _token) external nonZeroAddress(_token) onlyRole(REMOVE_RESERVED_TOKEN_ROLE) {
|
||||
uint256 cachedSourceChainId = sourceChainId;
|
||||
|
||||
if (nativeToBridgedToken[cachedSourceChainId][_token] != RESERVED_STATUS) revert NotReserved(_token);
|
||||
nativeToBridgedToken[cachedSourceChainId][_token] = EMPTY;
|
||||
|
||||
emit ReservationRemoved(_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Linea can set a custom ERC-20 contract for specific ERC-20.
|
||||
* For security purposes, Linea can only call this function if the token has
|
||||
* not been bridged yet.
|
||||
* @dev SET_CUSTOM_CONTRACT_ROLE is required to execute.
|
||||
* @param _nativeToken The address of the token on the source chain.
|
||||
* @param _targetContract The address of the custom contract.
|
||||
*/
|
||||
function setCustomContract(
|
||||
address _nativeToken,
|
||||
address _targetContract
|
||||
)
|
||||
external
|
||||
nonZeroAddress(_nativeToken)
|
||||
nonZeroAddress(_targetContract)
|
||||
onlyRole(SET_CUSTOM_CONTRACT_ROLE)
|
||||
isNewToken(_nativeToken)
|
||||
{
|
||||
if (bridgedToNativeToken[_targetContract] != EMPTY) {
|
||||
revert AlreadyBrigedToNativeTokenSet(_targetContract);
|
||||
}
|
||||
if (_targetContract == NATIVE_STATUS || _targetContract == DEPLOYED_STATUS || _targetContract == RESERVED_STATUS) {
|
||||
revert StatusAddressNotAllowed(_targetContract);
|
||||
}
|
||||
|
||||
uint256 cachedTargetChainId = targetChainId;
|
||||
|
||||
if (nativeToBridgedToken[cachedTargetChainId][_nativeToken] != EMPTY) {
|
||||
revert NativeToBridgedTokenAlreadySet(_nativeToken);
|
||||
}
|
||||
|
||||
nativeToBridgedToken[cachedTargetChainId][_nativeToken] = _targetContract;
|
||||
bridgedToNativeToken[_targetContract] = _nativeToken;
|
||||
emit CustomContractSet(_nativeToken, _targetContract, msg.sender);
|
||||
}
|
||||
|
||||
// Helpers to safely get the metadata from a token, inspired by
|
||||
// https://github.com/traderjoe-xyz/joe-core/blob/main/contracts/MasterChefJoeV3.sol#L55-L95
|
||||
|
||||
/**
|
||||
* @dev Provides a safe ERC-20.name version which returns 'NO_NAME' as fallback string.
|
||||
* @param _token The address of the ERC-20 token contract
|
||||
* @return tokenName Returns the string of the token name.
|
||||
*/
|
||||
function _safeName(address _token) internal view returns (string memory tokenName) {
|
||||
(bool success, bytes memory data) = _token.staticcall(METADATA_NAME);
|
||||
tokenName = success ? _returnDataToString(data) : "NO_NAME";
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Provides a safe ERC-20.symbol version which returns 'NO_SYMBOL' as fallback string
|
||||
* @param _token The address of the ERC-20 token contract
|
||||
* @return symbol Returns the string of the symbol.
|
||||
*/
|
||||
function _safeSymbol(address _token) internal view returns (string memory symbol) {
|
||||
(bool success, bytes memory data) = _token.staticcall(METADATA_SYMBOL);
|
||||
symbol = success ? _returnDataToString(data) : "NO_SYMBOL";
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Provides a safe ERC-20.decimals version which reverts when decimals are unknown
|
||||
* Note Tokens with (decimals > 255) are not supported
|
||||
* @param _token The address of the ERC-20 token contract
|
||||
* @return Returns the token's decimals value.
|
||||
*/
|
||||
function _safeDecimals(address _token) internal view returns (uint8) {
|
||||
(bool success, bytes memory data) = _token.staticcall(METADATA_DECIMALS);
|
||||
|
||||
if (success && data.length == VALID_DECIMALS_ENCODING_LENGTH) {
|
||||
return abi.decode(data, (uint8));
|
||||
}
|
||||
|
||||
revert DecimalsAreUnknown(_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Converts returned data to string. Returns 'NOT_VALID_ENCODING' as fallback value.
|
||||
* @param _data returned data.
|
||||
* @return decodedString The decoded string data.
|
||||
*/
|
||||
function _returnDataToString(bytes memory _data) internal pure returns (string memory decodedString) {
|
||||
if (_data.length >= MINIMUM_STRING_ABI_DECODE_LENGTH) {
|
||||
return abi.decode(_data, (string));
|
||||
} else if (_data.length != SHORT_STRING_ENCODING_LENGTH) {
|
||||
return "NOT_VALID_ENCODING";
|
||||
}
|
||||
|
||||
// Since the strings on bytes32 are encoded left-right, check the first zero in the data
|
||||
uint256 nonZeroBytes;
|
||||
unchecked {
|
||||
while (nonZeroBytes < SHORT_STRING_ENCODING_LENGTH && _data[nonZeroBytes] != 0) {
|
||||
nonZeroBytes++;
|
||||
}
|
||||
}
|
||||
|
||||
// If the first one is 0, we do not handle the encoding
|
||||
if (nonZeroBytes == 0) {
|
||||
return "NOT_VALID_ENCODING";
|
||||
}
|
||||
// Create a byte array with nonZeroBytes length
|
||||
bytes memory bytesArray = new bytes(nonZeroBytes);
|
||||
unchecked {
|
||||
for (uint256 i; i < nonZeroBytes; ) {
|
||||
bytesArray[i] = _data[i];
|
||||
++i;
|
||||
}
|
||||
}
|
||||
decodedString = string(bytesArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Call the token permit method of extended ERC-20
|
||||
* @notice Only support tokens implementing ERC-2612
|
||||
* @param _token ERC-20 token address
|
||||
* @param _permitData Raw data of the call `permit` of the token
|
||||
*/
|
||||
function _permit(address _token, bytes calldata _permitData) internal virtual {
|
||||
if (bytes4(_permitData[:4]) != _PERMIT_SELECTOR)
|
||||
revert InvalidPermitData(bytes4(_permitData[:4]), _PERMIT_SELECTOR);
|
||||
// Decode the permit data
|
||||
// The parameters are:
|
||||
// 1. owner: The address of the wallet holding the tokens
|
||||
// 2. spender: The address of the entity permitted to spend the tokens
|
||||
// 3. value: The maximum amount of tokens the spender is allowed to spend
|
||||
// 4. deadline: The time until which the permit is valid
|
||||
// 5. v: Part of the signature (along with r and s), these three values form the signature of the permit
|
||||
// 6. r: Part of the signature
|
||||
// 7. s: Part of the signature
|
||||
(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) = abi.decode(
|
||||
_permitData[4:],
|
||||
(address, address, uint256, uint256, uint8, bytes32, bytes32)
|
||||
);
|
||||
if (owner != msg.sender) revert PermitNotFromSender(owner);
|
||||
if (spender != address(this)) revert PermitNotAllowingBridge(spender);
|
||||
|
||||
if (IERC20Upgradeable(_token).allowance(owner, spender) < amount) {
|
||||
IERC20PermitUpgradeable(_token).permit(msg.sender, address(this), amount, deadline, v, r, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { IPauseManager } from "../../../security/pausing/interfaces/IPauseManager.sol";
|
||||
import { IPermissionsManager } from "../../../security/access/interfaces/IPermissionsManager.sol";
|
||||
@@ -226,6 +226,12 @@ interface ITokenBridge {
|
||||
*/
|
||||
error SourceChainSameAsTargetChain();
|
||||
|
||||
/**
|
||||
* @notice Returns the ABI version and not the reinitialize version.
|
||||
* @return contractVersion The contract ABI version.
|
||||
*/
|
||||
function CONTRACT_VERSION() external view returns (string memory contractVersion);
|
||||
|
||||
/**
|
||||
* @notice Similar to `bridgeToken` function but allows to pass additional
|
||||
* permit data to do the ERC-20 approval in a single transaction.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Contract to fill space in storage to maintain storage layout.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { TimelockController } from "@openzeppelin/contracts/governance/TimelockController.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Interface declaring generic errors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Contract to manage some efficient hashing functions.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
// Code generated by gnark DO NOT EDIT
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Library to perform MiMC hashing
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { Mimc } from "./Mimc.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import { IMessageService } from "./interfaces/IMessageService.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Interface declaring pre-existing cross-chain messaging functions, events and errors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { BitMaps } from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
|
||||
import { L1MessageManagerV1 } from "./v1/L1MessageManagerV1.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { L1MessageServiceV1 } from "./v1/L1MessageServiceV1.sol";
|
||||
@@ -57,7 +57,17 @@ abstract contract L1MessageService is
|
||||
address _to,
|
||||
uint256 _fee,
|
||||
bytes calldata _calldata
|
||||
) external payable whenTypeAndGeneralNotPaused(PauseType.L1_L2) {
|
||||
) external payable virtual whenTypeAndGeneralNotPaused(PauseType.L1_L2) {
|
||||
_sendMessage(_to, _fee, _calldata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Adds a message for sending cross-chain and emits MessageSent.
|
||||
* @param _to The address the message is intended for.
|
||||
* @param _fee The fee being paid for the message delivery.
|
||||
* @param _calldata The calldata to pass to the recipient.
|
||||
*/
|
||||
function _sendMessage(address _to, uint256 _fee, bytes calldata _calldata) internal virtual {
|
||||
if (_to == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
@@ -84,7 +94,15 @@ abstract contract L1MessageService is
|
||||
*/
|
||||
function claimMessageWithProof(
|
||||
ClaimMessageWithProofParams calldata _params
|
||||
) external nonReentrant distributeFees(_params.fee, _params.to, _params.data, _params.feeRecipient) {
|
||||
) external virtual nonReentrant distributeFees(_params.fee, _params.to, _params.data, _params.feeRecipient) {
|
||||
_claimMessageWithProof(_params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Claims and delivers a cross-chain message using a Merkle proof.
|
||||
* @param _params Collection of claim data with proof and supporting data.
|
||||
*/
|
||||
function _claimMessageWithProof(ClaimMessageWithProofParams calldata _params) internal virtual {
|
||||
_requireTypeAndGeneralNotPaused(PauseType.L2_L1);
|
||||
|
||||
uint256 merkleDepth = l2MerkleRootsDepths[_params.merkleRoot];
|
||||
@@ -144,7 +162,7 @@ abstract contract L1MessageService is
|
||||
* @dev The message sender address is set temporarily in the transient storage when claiming.
|
||||
* @return originalSender The message sender address that is stored temporarily in the transient storage when claiming.
|
||||
*/
|
||||
function sender() external view returns (address originalSender) {
|
||||
function sender() external view virtual returns (address originalSender) {
|
||||
originalSender = TRANSIENT_MESSAGE_SENDER;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title L1 Message manager interface for current functions, events and errors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title L1 Message Service interface for pre-existing functions, events, structs and errors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { IL1MessageManagerV1 } from "./interfaces/IL1MessageManagerV1.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { LineaRollupPauseManager } from "../../../security/pausing/LineaRollupPauseManager.sol";
|
||||
import { RateLimiter } from "../../../security/limiting/RateLimiter.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title L1 Message manager V1 interface for pre-existing errors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { L2MessageManagerV1 } from "./v1/L2MessageManagerV1.sol";
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { L2MessageServiceV1 } from "./v1/L2MessageServiceV1.sol";
|
||||
import { L2MessageManager } from "./L2MessageManager.sol";
|
||||
import { PermissionsManager } from "../../security/access/PermissionsManager.sol";
|
||||
|
||||
import { L2MessageServiceBase } from "./L2MessageServiceBase.sol";
|
||||
/**
|
||||
* @title Contract to manage cross-chain messaging on L2.
|
||||
* @author ConsenSys Software Inc.
|
||||
* @custom:security-contact security-report@linea.build
|
||||
*/
|
||||
contract L2MessageService is AccessControlUpgradeable, L2MessageServiceV1, L2MessageManager, PermissionsManager {
|
||||
/// @dev This is the ABI version and not the reinitialize version.
|
||||
string public constant CONTRACT_VERSION = "1.0";
|
||||
|
||||
contract L2MessageService is L2MessageServiceBase {
|
||||
/// @dev Total contract storage is 50 slots with the gap below.
|
||||
/// @dev Keep 50 free storage slots for future implementation updates to avoid storage collision.
|
||||
uint256[50] private __gap_L2MessageService;
|
||||
@@ -40,46 +32,14 @@ contract L2MessageService is AccessControlUpgradeable, L2MessageServiceV1, L2Mes
|
||||
RoleAddress[] calldata _roleAddresses,
|
||||
PauseTypeRole[] calldata _pauseTypeRoleAssignments,
|
||||
PauseTypeRole[] calldata _unpauseTypeRoleAssignments
|
||||
) external initializer {
|
||||
__ERC165_init();
|
||||
__Context_init();
|
||||
__AccessControl_init();
|
||||
__RateLimiter_init(_rateLimitPeriod, _rateLimitAmount);
|
||||
|
||||
__PauseManager_init(_pauseTypeRoleAssignments, _unpauseTypeRoleAssignments);
|
||||
__ReentrancyGuard_init();
|
||||
|
||||
if (_defaultAdmin == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev DEFAULT_ADMIN_ROLE is set for the security council explicitly,
|
||||
* as the permissions init purposefully does not allow DEFAULT_ADMIN_ROLE to be set.
|
||||
*/
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin);
|
||||
|
||||
__Permissions_init(_roleAddresses);
|
||||
|
||||
nextMessageNumber = 1;
|
||||
|
||||
_messageSender = DEFAULT_SENDER_ADDRESS;
|
||||
minimumFeeInWei = 0.0001 ether;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets permissions for a list of addresses and their roles as well as initialises the PauseManager pauseType:role mappings.
|
||||
* @dev This function is a reinitializer and can only be called once per version. Should be called using an upgradeAndCall transaction to the ProxyAdmin.
|
||||
* @param _roleAddresses The list of addresses and roles to assign permissions to.
|
||||
* @param _pauseTypeRoleAssignments The list of pause types to associate with roles.
|
||||
* @param _unpauseTypeRoleAssignments The list of unpause types to associate with roles.
|
||||
*/
|
||||
function reinitializePauseTypesAndPermissions(
|
||||
RoleAddress[] calldata _roleAddresses,
|
||||
PauseTypeRole[] calldata _pauseTypeRoleAssignments,
|
||||
PauseTypeRole[] calldata _unpauseTypeRoleAssignments
|
||||
) external reinitializer(2) {
|
||||
__Permissions_init(_roleAddresses);
|
||||
__PauseManager_init(_pauseTypeRoleAssignments, _unpauseTypeRoleAssignments);
|
||||
) external virtual initializer {
|
||||
__L2MessageService_init(
|
||||
_rateLimitPeriod,
|
||||
_rateLimitAmount,
|
||||
_defaultAdmin,
|
||||
_roleAddresses,
|
||||
_pauseTypeRoleAssignments,
|
||||
_unpauseTypeRoleAssignments
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
82
contracts/src/messaging/l2/L2MessageServiceBase.sol
Normal file
82
contracts/src/messaging/l2/L2MessageServiceBase.sol
Normal file
@@ -0,0 +1,82 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { L2MessageServiceV1 } from "./v1/L2MessageServiceV1.sol";
|
||||
import { L2MessageManager } from "./L2MessageManager.sol";
|
||||
import { PermissionsManager } from "../../security/access/PermissionsManager.sol";
|
||||
|
||||
/**
|
||||
* @title Contract to manage cross-chain messaging on L2.
|
||||
* @author ConsenSys Software Inc.
|
||||
* @custom:security-contact security-report@linea.build
|
||||
*/
|
||||
abstract contract L2MessageServiceBase is
|
||||
AccessControlUpgradeable,
|
||||
L2MessageServiceV1,
|
||||
L2MessageManager,
|
||||
PermissionsManager
|
||||
{
|
||||
/// @dev This is the ABI version and not the reinitialize version.
|
||||
string private constant _CONTRACT_VERSION = "1.0";
|
||||
|
||||
/// @dev Total contract storage is 50 slots with the gap below.
|
||||
/// @dev Keep 50 free storage slots for future implementation updates to avoid storage collision.
|
||||
uint256[50] private __gap_L2MessageService;
|
||||
|
||||
/// @custom:oz-upgrades-unsafe-allow constructor
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes underlying message service dependencies.
|
||||
* @param _rateLimitPeriod The period to rate limit against.
|
||||
* @param _rateLimitAmount The limit allowed for withdrawing the period.
|
||||
* @param _defaultAdmin The account to be given DEFAULT_ADMIN_ROLE on initialization.
|
||||
* @param _roleAddresses The list of addresses to grant roles to.
|
||||
* @param _pauseTypeRoleAssignments The list of pause type roles.
|
||||
* @param _unpauseTypeRoleAssignments The list of unpause type roles.
|
||||
*/
|
||||
function __L2MessageService_init(
|
||||
uint256 _rateLimitPeriod,
|
||||
uint256 _rateLimitAmount,
|
||||
address _defaultAdmin,
|
||||
RoleAddress[] calldata _roleAddresses,
|
||||
PauseTypeRole[] calldata _pauseTypeRoleAssignments,
|
||||
PauseTypeRole[] calldata _unpauseTypeRoleAssignments
|
||||
) internal virtual {
|
||||
__ERC165_init();
|
||||
__Context_init();
|
||||
__AccessControl_init();
|
||||
__RateLimiter_init(_rateLimitPeriod, _rateLimitAmount);
|
||||
|
||||
__PauseManager_init(_pauseTypeRoleAssignments, _unpauseTypeRoleAssignments);
|
||||
__ReentrancyGuard_init();
|
||||
|
||||
if (_defaultAdmin == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev DEFAULT_ADMIN_ROLE is set for the security council explicitly,
|
||||
* as the permissions init purposefully does not allow DEFAULT_ADMIN_ROLE to be set.
|
||||
*/
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin);
|
||||
|
||||
__Permissions_init(_roleAddresses);
|
||||
|
||||
nextMessageNumber = 1;
|
||||
|
||||
_messageSender = DEFAULT_SENDER_ADDRESS;
|
||||
minimumFeeInWei = 0.0001 ether;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the ABI version and not the reinitialize version.
|
||||
* @return contractVersion The contract ABI version.
|
||||
*/
|
||||
function CONTRACT_VERSION() external view virtual returns (string memory contractVersion) {
|
||||
contractVersion = _CONTRACT_VERSION;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Interface declaring cross-chain messaging on L2 functions, events and errors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import { IL2MessageManagerV1 } from "./interfaces/IL2MessageManagerV1.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
|
||||
import { IMessageService } from "../../interfaces/IMessageService.sol";
|
||||
@@ -29,7 +29,7 @@ abstract contract L2MessageServiceV1 is
|
||||
* NB: Take note that this is at the beginning of the file where other storage gaps,
|
||||
* are at the end of files. Be careful with how storage is adjusted on upgrades.
|
||||
*/
|
||||
uint256[50] private __gap_L2MessageService;
|
||||
uint256[50] private __gap_L2MessageServiceV1;
|
||||
|
||||
/// @notice The role required to set the minimum DDOS fee.
|
||||
bytes32 public constant MINIMUM_FEE_SETTER_ROLE = keccak256("MINIMUM_FEE_SETTER_ROLE");
|
||||
@@ -63,9 +63,21 @@ abstract contract L2MessageServiceV1 is
|
||||
* @param _fee The fee being paid for the message delivery.
|
||||
* @param _calldata The calldata to pass to the recipient.
|
||||
*/
|
||||
function sendMessage(address _to, uint256 _fee, bytes calldata _calldata) external payable {
|
||||
_requireTypeAndGeneralNotPaused(PauseType.L2_L1);
|
||||
function sendMessage(
|
||||
address _to,
|
||||
uint256 _fee,
|
||||
bytes calldata _calldata
|
||||
) external payable virtual whenTypeAndGeneralNotPaused(PauseType.L2_L1) {
|
||||
_sendMessage(_to, _fee, _calldata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Adds a message for sending cross-chain and emits a relevant event.
|
||||
* @param _to The address the message is intended for.
|
||||
* @param _fee The fee being paid for the message delivery.
|
||||
* @param _calldata The calldata to pass to the recipient.
|
||||
*/
|
||||
function _sendMessage(address _to, uint256 _fee, bytes calldata _calldata) internal virtual {
|
||||
if (_to == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
@@ -121,9 +133,33 @@ abstract contract L2MessageServiceV1 is
|
||||
address payable _feeRecipient,
|
||||
bytes calldata _calldata,
|
||||
uint256 _nonce
|
||||
) external nonReentrant distributeFees(_fee, _to, _calldata, _feeRecipient) {
|
||||
_requireTypeAndGeneralNotPaused(PauseType.L1_L2);
|
||||
)
|
||||
external
|
||||
virtual
|
||||
nonReentrant
|
||||
distributeFees(_fee, _to, _calldata, _feeRecipient)
|
||||
whenTypeAndGeneralNotPaused(PauseType.L1_L2)
|
||||
{
|
||||
_claimMessage(_from, _to, _fee, _value, _calldata, _nonce);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Claims and delivers a cross-chain message.
|
||||
* @param _from The address of the original sender.
|
||||
* @param _to The address the message is intended for.
|
||||
* @param _fee The fee being paid for the message delivery.
|
||||
* @param _value The value to be transferred to the destination address.
|
||||
* @param _calldata The calldata to pass to the recipient.
|
||||
* @param _nonce The unique auto generated message number used when sending the message.
|
||||
*/
|
||||
function _claimMessage(
|
||||
address _from,
|
||||
address _to,
|
||||
uint256 _fee,
|
||||
uint256 _value,
|
||||
bytes calldata _calldata,
|
||||
uint256 _nonce
|
||||
) internal virtual {
|
||||
bytes32 messageHash = MessageHashing._hashMessage(_from, _to, _fee, _value, _nonce, _calldata);
|
||||
|
||||
/// @dev Status check and revert is in the message manager.
|
||||
@@ -163,7 +199,7 @@ abstract contract L2MessageServiceV1 is
|
||||
* @dev The _messageSender address is set temporarily when claiming.
|
||||
* @return originalSender The original sender stored temporarily at the _messageSender address in storage.
|
||||
*/
|
||||
function sender() external view returns (address originalSender) {
|
||||
function sender() external view virtual returns (address originalSender) {
|
||||
originalSender = _messageSender;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Interface declaring pre-existing cross-chain messaging on L2 functions, events and errors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title L2 Message Service interface for pre-existing functions, events and errors.
|
||||
@@ -7,6 +7,12 @@ pragma solidity 0.8.30;
|
||||
* @custom:security-contact security-report@linea.build
|
||||
*/
|
||||
interface IL2MessageServiceV1 {
|
||||
/**
|
||||
* @notice Returns the ABI version and not the reinitialize version.
|
||||
* @return contractVersion The contract ABI version.
|
||||
*/
|
||||
function CONTRACT_VERSION() external view returns (string memory contractVersion);
|
||||
|
||||
/**
|
||||
* @notice The Fee Manager sets a minimum fee to address DOS protection.
|
||||
* @dev MINIMUM_FEE_SETTER_ROLE is required to set the minimum fee.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Library to hash cross-chain messages.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { EfficientLeftRightKeccak } from "../../libraries/EfficientLeftRightKeccak.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Contract to forward calls to an underlying contract.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { IGenericErrors } from "../interfaces/IGenericErrors.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Interface declaring IRecoverFunds errors and functions.
|
||||
|
||||
@@ -1,86 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { L1MessageService } from "../messaging/l1/L1MessageService.sol";
|
||||
import { ZkEvmV2 } from "./ZkEvmV2.sol";
|
||||
import { ILineaRollup } from "./interfaces/ILineaRollup.sol";
|
||||
import { PermissionsManager } from "../security/access/PermissionsManager.sol";
|
||||
|
||||
import { EfficientLeftRightKeccak } from "../libraries/EfficientLeftRightKeccak.sol";
|
||||
import { LineaRollupBase } from "./LineaRollupBase.sol";
|
||||
/**
|
||||
* @title Contract to manage cross-chain messaging on L1, L2 data submission, and rollup proof verification.
|
||||
* @author ConsenSys Software Inc.
|
||||
* @custom:security-contact security-report@linea.build
|
||||
*/
|
||||
contract LineaRollup is AccessControlUpgradeable, ZkEvmV2, L1MessageService, PermissionsManager, ILineaRollup {
|
||||
using EfficientLeftRightKeccak for *;
|
||||
|
||||
/// @notice This is the ABI version and not the reinitialize version.
|
||||
string public constant CONTRACT_VERSION = "7.0";
|
||||
|
||||
/// @notice The role required to set/add proof verifiers by type.
|
||||
bytes32 public constant VERIFIER_SETTER_ROLE = keccak256("VERIFIER_SETTER_ROLE");
|
||||
|
||||
/// @notice The role required to set/remove proof verifiers by type.
|
||||
bytes32 public constant VERIFIER_UNSETTER_ROLE = keccak256("VERIFIER_UNSETTER_ROLE");
|
||||
|
||||
/// @dev Value indicating a shnarf exists.
|
||||
uint256 internal constant SHNARF_EXISTS_DEFAULT_VALUE = 1;
|
||||
|
||||
/// @dev The default hash value.
|
||||
bytes32 internal constant EMPTY_HASH = 0x0;
|
||||
|
||||
/// @dev The BLS Curve modulus value used.
|
||||
uint256 internal constant BLS_CURVE_MODULUS =
|
||||
52435875175126190479447740508185965837690552500527637822603658699938581184513;
|
||||
|
||||
/// @dev The well-known precompile address for point evaluation.
|
||||
address internal constant POINT_EVALUATION_PRECOMPILE_ADDRESS = address(0x0a);
|
||||
|
||||
/// @dev The expected point evaluation return data length.
|
||||
uint256 internal constant POINT_EVALUATION_RETURN_DATA_LENGTH = 64;
|
||||
|
||||
/// @dev The expected point evaluation field element length returned.
|
||||
uint256 internal constant POINT_EVALUATION_FIELD_ELEMENTS_LENGTH = 4096;
|
||||
|
||||
/// @dev In practice, when used, this is expected to be a close approximation to 6 months, and is intentional.
|
||||
uint256 internal constant SIX_MONTHS_IN_SECONDS = (365 / 2) * 24 * 60 * 60;
|
||||
|
||||
/// @dev DEPRECATED in favor of the single blobShnarfExists mapping.
|
||||
mapping(bytes32 dataHash => bytes32 finalStateRootHash) private dataFinalStateRootHashes_DEPRECATED;
|
||||
/// @dev DEPRECATED in favor of the single blobShnarfExists mapping.
|
||||
mapping(bytes32 dataHash => bytes32 parentHash) private dataParents_DEPRECATED;
|
||||
/// @dev DEPRECATED in favor of the single blobShnarfExists mapping.
|
||||
mapping(bytes32 dataHash => bytes32 shnarfHash) private dataShnarfHashes_DEPRECATED;
|
||||
/// @dev DEPRECATED in favor of the single blobShnarfExists mapping.
|
||||
mapping(bytes32 dataHash => uint256 startingBlock) private dataStartingBlock_DEPRECATED;
|
||||
/// @dev DEPRECATED in favor of the single blobShnarfExists mapping.
|
||||
mapping(bytes32 dataHash => uint256 endingBlock) private dataEndingBlock_DEPRECATED;
|
||||
|
||||
/// @dev DEPRECATED in favor of currentFinalizedState hash.
|
||||
uint256 private currentL2StoredL1MessageNumber_DEPRECATED;
|
||||
/// @dev DEPRECATED in favor of currentFinalizedState hash.
|
||||
bytes32 private currentL2StoredL1RollingHash_DEPRECATED;
|
||||
|
||||
/// @notice Contains the most recent finalized shnarf.
|
||||
bytes32 public currentFinalizedShnarf;
|
||||
|
||||
/**
|
||||
* @dev NB: THIS IS THE ONLY MAPPING BEING USED FOR DATA SUBMISSION TRACKING.
|
||||
* @dev NB: This was shnarfFinalBlockNumbers and is replaced to indicate only that a shnarf exists with a value of 1.
|
||||
*/
|
||||
mapping(bytes32 shnarf => uint256 exists) public blobShnarfExists;
|
||||
|
||||
/// @notice Hash of the L2 computed L1 message number, rolling hash and finalized timestamp.
|
||||
bytes32 public currentFinalizedState;
|
||||
|
||||
/// @notice The address of the fallback operator.
|
||||
/// @dev This address is granted the OPERATOR_ROLE after six months of finalization inactivity by the current operators.
|
||||
address public fallbackOperator;
|
||||
|
||||
/// @dev Total contract storage is 11 slots.
|
||||
|
||||
contract LineaRollup is LineaRollupBase {
|
||||
/// @custom:oz-upgrades-unsafe-allow constructor
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
@@ -94,623 +21,6 @@ contract LineaRollup is AccessControlUpgradeable, ZkEvmV2, L1MessageService, Per
|
||||
* @param _initializationData The initial data used for proof verification.
|
||||
*/
|
||||
function initialize(InitializationData calldata _initializationData) external initializer {
|
||||
if (_initializationData.defaultVerifier == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
__PauseManager_init(_initializationData.pauseTypeRoles, _initializationData.unpauseTypeRoles);
|
||||
|
||||
__MessageService_init(_initializationData.rateLimitPeriodInSeconds, _initializationData.rateLimitAmountInWei);
|
||||
|
||||
if (_initializationData.defaultAdmin == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev DEFAULT_ADMIN_ROLE is set for the security council explicitly,
|
||||
* as the permissions init purposefully does not allow DEFAULT_ADMIN_ROLE to be set.
|
||||
*/
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, _initializationData.defaultAdmin);
|
||||
|
||||
__Permissions_init(_initializationData.roleAddresses);
|
||||
|
||||
verifiers[0] = _initializationData.defaultVerifier;
|
||||
|
||||
if (_initializationData.fallbackOperator == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
fallbackOperator = _initializationData.fallbackOperator;
|
||||
emit FallbackOperatorAddressSet(msg.sender, _initializationData.fallbackOperator);
|
||||
|
||||
currentL2BlockNumber = _initializationData.initialL2BlockNumber;
|
||||
stateRootHashes[_initializationData.initialL2BlockNumber] = _initializationData.initialStateRootHash;
|
||||
|
||||
bytes32 genesisShnarf = _computeShnarf(
|
||||
EMPTY_HASH,
|
||||
EMPTY_HASH,
|
||||
_initializationData.initialStateRootHash,
|
||||
EMPTY_HASH,
|
||||
EMPTY_HASH
|
||||
);
|
||||
|
||||
blobShnarfExists[genesisShnarf] = SHNARF_EXISTS_DEFAULT_VALUE;
|
||||
currentFinalizedShnarf = genesisShnarf;
|
||||
currentFinalizedState = _computeLastFinalizedState(0, EMPTY_HASH, _initializationData.genesisTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets permissions for a list of addresses and their roles as well as initialises the PauseManager pauseType:role mappings and fallback operator.
|
||||
* @dev This function is a reinitializer and can only be called once per version. Should be called using an upgradeAndCall transaction to the ProxyAdmin.
|
||||
* @param _roleAddresses The list of addresses and roles to assign permissions to.
|
||||
* @param _pauseTypeRoleAssignments The list of pause types to associate with roles.
|
||||
* @param _unpauseTypeRoleAssignments The list of unpause types to associate with roles.
|
||||
* @param _fallbackOperator The address of the fallback operator.
|
||||
*/
|
||||
function reinitializeLineaRollupV6(
|
||||
RoleAddress[] calldata _roleAddresses,
|
||||
PauseTypeRole[] calldata _pauseTypeRoleAssignments,
|
||||
PauseTypeRole[] calldata _unpauseTypeRoleAssignments,
|
||||
address _fallbackOperator
|
||||
) external reinitializer(6) {
|
||||
__Permissions_init(_roleAddresses);
|
||||
__PauseManager_init(_pauseTypeRoleAssignments, _unpauseTypeRoleAssignments);
|
||||
|
||||
if (_fallbackOperator == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
fallbackOperator = _fallbackOperator;
|
||||
emit FallbackOperatorAddressSet(msg.sender, _fallbackOperator);
|
||||
|
||||
/// @dev using the constants requires string memory and more complex code.
|
||||
emit LineaRollupVersionChanged(bytes8("5.0"), bytes8("6.0"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Revokes `role` from the calling account.
|
||||
* @dev Fallback operator cannot renounce role. Reverts with OnlyNonFallbackOperator.
|
||||
* @param _role The role to renounce.
|
||||
* @param _account The account to renounce - can only be the _msgSender().
|
||||
*/
|
||||
function renounceRole(bytes32 _role, address _account) public override {
|
||||
if (_account == fallbackOperator) {
|
||||
revert OnlyNonFallbackOperator();
|
||||
}
|
||||
|
||||
super.renounceRole(_role, _account);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Adds or updates the verifier contract address for a proof type.
|
||||
* @dev VERIFIER_SETTER_ROLE is required to execute.
|
||||
* @param _newVerifierAddress The address for the verifier contract.
|
||||
* @param _proofType The proof type being set/updated.
|
||||
*/
|
||||
function setVerifierAddress(address _newVerifierAddress, uint256 _proofType) external onlyRole(VERIFIER_SETTER_ROLE) {
|
||||
if (_newVerifierAddress == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
emit VerifierAddressChanged(_newVerifierAddress, _proofType, msg.sender, verifiers[_proofType]);
|
||||
|
||||
verifiers[_proofType] = _newVerifierAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets the fallback operator role to the specified address if six months have passed since the last finalization.
|
||||
* @dev Reverts if six months have not passed since the last finalization.
|
||||
* @param _messageNumber Last finalized L1 message number as part of the feedback loop.
|
||||
* @param _rollingHash Last finalized L1 rolling hash as part of the feedback loop.
|
||||
* @param _lastFinalizedTimestamp Last finalized L2 block timestamp.
|
||||
*/
|
||||
function setFallbackOperator(uint256 _messageNumber, bytes32 _rollingHash, uint256 _lastFinalizedTimestamp) external {
|
||||
if (block.timestamp < _lastFinalizedTimestamp + SIX_MONTHS_IN_SECONDS) {
|
||||
revert LastFinalizationTimeNotLapsed();
|
||||
}
|
||||
if (currentFinalizedState != _computeLastFinalizedState(_messageNumber, _rollingHash, _lastFinalizedTimestamp)) {
|
||||
revert FinalizationStateIncorrect(
|
||||
currentFinalizedState,
|
||||
_computeLastFinalizedState(_messageNumber, _rollingHash, _lastFinalizedTimestamp)
|
||||
);
|
||||
}
|
||||
|
||||
address fallbackOperatorAddress = fallbackOperator;
|
||||
|
||||
_grantRole(OPERATOR_ROLE, fallbackOperatorAddress);
|
||||
emit FallbackOperatorRoleGranted(msg.sender, fallbackOperatorAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unset the verifier contract address for a proof type.
|
||||
* @dev VERIFIER_UNSETTER_ROLE is required to execute.
|
||||
* @param _proofType The proof type being set/updated.
|
||||
*/
|
||||
function unsetVerifierAddress(uint256 _proofType) external onlyRole(VERIFIER_UNSETTER_ROLE) {
|
||||
emit VerifierAddressChanged(address(0), _proofType, msg.sender, verifiers[_proofType]);
|
||||
|
||||
delete verifiers[_proofType];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Submit one or more EIP-4844 blobs.
|
||||
* @dev OPERATOR_ROLE is required to execute.
|
||||
* @dev This should be a blob carrying transaction.
|
||||
* @param _blobSubmissions The data for blob submission including proofs and required polynomials.
|
||||
* @param _parentShnarf The parent shnarf used in continuity checks as it includes the parentStateRootHash in its computation.
|
||||
* @param _finalBlobShnarf The expected final shnarf post computation of all the blob shnarfs.
|
||||
*/
|
||||
function submitBlobs(
|
||||
BlobSubmission[] calldata _blobSubmissions,
|
||||
bytes32 _parentShnarf,
|
||||
bytes32 _finalBlobShnarf
|
||||
) external whenTypeAndGeneralNotPaused(PauseType.BLOB_SUBMISSION) onlyRole(OPERATOR_ROLE) {
|
||||
if (_blobSubmissions.length == 0) {
|
||||
revert BlobSubmissionDataIsMissing();
|
||||
}
|
||||
|
||||
if (blobhash(_blobSubmissions.length) != EMPTY_HASH) {
|
||||
revert BlobSubmissionDataEmpty(_blobSubmissions.length);
|
||||
}
|
||||
|
||||
if (blobShnarfExists[_parentShnarf] == 0) {
|
||||
revert ParentBlobNotSubmitted(_parentShnarf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev validate we haven't submitted the last shnarf. There is a final check at the end of the function verifying,
|
||||
* that _finalBlobShnarf was computed correctly.
|
||||
* Note: As only the last shnarf is stored, we don't need to validate shnarfs,
|
||||
* computed for any previous blobs in the submission (if multiple are submitted).
|
||||
*/
|
||||
if (blobShnarfExists[_finalBlobShnarf] != 0) {
|
||||
revert DataAlreadySubmitted(_finalBlobShnarf);
|
||||
}
|
||||
|
||||
bytes32 currentDataEvaluationPoint;
|
||||
bytes32 currentDataHash;
|
||||
|
||||
/// @dev Assigning in memory saves a lot of gas vs. calldata reading.
|
||||
BlobSubmission memory blobSubmission;
|
||||
|
||||
bytes32 computedShnarf = _parentShnarf;
|
||||
|
||||
for (uint256 i; i < _blobSubmissions.length; i++) {
|
||||
blobSubmission = _blobSubmissions[i];
|
||||
|
||||
currentDataHash = blobhash(i);
|
||||
|
||||
if (currentDataHash == EMPTY_HASH) {
|
||||
revert EmptyBlobDataAtIndex(i);
|
||||
}
|
||||
|
||||
bytes32 snarkHash = blobSubmission.snarkHash;
|
||||
|
||||
currentDataEvaluationPoint = EfficientLeftRightKeccak._efficientKeccak(snarkHash, currentDataHash);
|
||||
|
||||
_verifyPointEvaluation(
|
||||
currentDataHash,
|
||||
uint256(currentDataEvaluationPoint),
|
||||
blobSubmission.dataEvaluationClaim,
|
||||
blobSubmission.kzgCommitment,
|
||||
blobSubmission.kzgProof
|
||||
);
|
||||
|
||||
computedShnarf = _computeShnarf(
|
||||
computedShnarf,
|
||||
snarkHash,
|
||||
blobSubmission.finalStateRootHash,
|
||||
currentDataEvaluationPoint,
|
||||
bytes32(blobSubmission.dataEvaluationClaim)
|
||||
);
|
||||
}
|
||||
|
||||
if (_finalBlobShnarf != computedShnarf) {
|
||||
revert FinalShnarfWrong(_finalBlobShnarf, computedShnarf);
|
||||
}
|
||||
|
||||
/// @dev use the last shnarf as the submission to store as technically it becomes the next parent shnarf.
|
||||
blobShnarfExists[computedShnarf] = SHNARF_EXISTS_DEFAULT_VALUE;
|
||||
|
||||
emit DataSubmittedV3(_parentShnarf, computedShnarf, blobSubmission.finalStateRootHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Submit blobs using compressed data via calldata.
|
||||
* @dev OPERATOR_ROLE is required to execute.
|
||||
* @param _submission The supporting data for compressed data submission including compressed data.
|
||||
* @param _parentShnarf The parent shnarf used in continuity checks as it includes the parentStateRootHash in its computation.
|
||||
* @param _expectedShnarf The expected shnarf post computation of all the submission.
|
||||
*/
|
||||
function submitDataAsCalldata(
|
||||
CompressedCalldataSubmission calldata _submission,
|
||||
bytes32 _parentShnarf,
|
||||
bytes32 _expectedShnarf
|
||||
) external whenTypeAndGeneralNotPaused(PauseType.CALLDATA_SUBMISSION) onlyRole(OPERATOR_ROLE) {
|
||||
if (_submission.compressedData.length == 0) {
|
||||
revert EmptySubmissionData();
|
||||
}
|
||||
|
||||
if (blobShnarfExists[_expectedShnarf] != 0) {
|
||||
revert DataAlreadySubmitted(_expectedShnarf);
|
||||
}
|
||||
|
||||
if (blobShnarfExists[_parentShnarf] == 0) {
|
||||
revert ParentBlobNotSubmitted(_parentShnarf);
|
||||
}
|
||||
|
||||
bytes32 currentDataHash = keccak256(_submission.compressedData);
|
||||
|
||||
bytes32 dataEvaluationPoint = EfficientLeftRightKeccak._efficientKeccak(_submission.snarkHash, currentDataHash);
|
||||
|
||||
bytes32 computedShnarf = _computeShnarf(
|
||||
_parentShnarf,
|
||||
_submission.snarkHash,
|
||||
_submission.finalStateRootHash,
|
||||
dataEvaluationPoint,
|
||||
_calculateY(_submission.compressedData, dataEvaluationPoint)
|
||||
);
|
||||
|
||||
if (_expectedShnarf != computedShnarf) {
|
||||
revert FinalShnarfWrong(_expectedShnarf, computedShnarf);
|
||||
}
|
||||
|
||||
blobShnarfExists[computedShnarf] = SHNARF_EXISTS_DEFAULT_VALUE;
|
||||
|
||||
emit DataSubmittedV3(_parentShnarf, computedShnarf, _submission.finalStateRootHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal function to compute and save the finalization state.
|
||||
* @dev Using assembly this way is cheaper gas wise.
|
||||
* @param _messageNumber Is the last L2 computed L1 message number in the finalization.
|
||||
* @param _rollingHash Is the last L2 computed L1 rolling hash in the finalization.
|
||||
* @param _timestamp The final timestamp in the finalization.
|
||||
*/
|
||||
function _computeLastFinalizedState(
|
||||
uint256 _messageNumber,
|
||||
bytes32 _rollingHash,
|
||||
uint256 _timestamp
|
||||
) internal pure returns (bytes32 hashedFinalizationState) {
|
||||
assembly {
|
||||
let mPtr := mload(0x40)
|
||||
mstore(mPtr, _messageNumber)
|
||||
mstore(add(mPtr, 0x20), _rollingHash)
|
||||
mstore(add(mPtr, 0x40), _timestamp)
|
||||
hashedFinalizationState := keccak256(mPtr, 0x60)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal function to compute the shnarf more efficiently.
|
||||
* @dev Using assembly this way is cheaper gas wise.
|
||||
* @param _parentShnarf The shnarf of the parent data item.
|
||||
* @param _snarkHash Is the computed hash for compressed data (using a SNARK-friendly hash function) that aggregates per data submission to be used in public input.
|
||||
* @param _finalStateRootHash The final state root hash of the data being submitted.
|
||||
* @param _dataEvaluationPoint The data evaluation point.
|
||||
* @param _dataEvaluationClaim The data evaluation claim.
|
||||
*/
|
||||
function _computeShnarf(
|
||||
bytes32 _parentShnarf,
|
||||
bytes32 _snarkHash,
|
||||
bytes32 _finalStateRootHash,
|
||||
bytes32 _dataEvaluationPoint,
|
||||
bytes32 _dataEvaluationClaim
|
||||
) internal pure returns (bytes32 shnarf) {
|
||||
assembly {
|
||||
let mPtr := mload(0x40)
|
||||
mstore(mPtr, _parentShnarf)
|
||||
mstore(add(mPtr, 0x20), _snarkHash)
|
||||
mstore(add(mPtr, 0x40), _finalStateRootHash)
|
||||
mstore(add(mPtr, 0x60), _dataEvaluationPoint)
|
||||
mstore(add(mPtr, 0x80), _dataEvaluationClaim)
|
||||
shnarf := keccak256(mPtr, 0xA0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Performs point evaluation for the compressed blob.
|
||||
* @dev _dataEvaluationPoint is modular reduced to be lower than the BLS_CURVE_MODULUS for precompile checks.
|
||||
* @param _currentDataHash The current blob versioned hash.
|
||||
* @param _dataEvaluationPoint The data evaluation point.
|
||||
* @param _dataEvaluationClaim The data evaluation claim.
|
||||
* @param _kzgCommitment The blob KZG commitment.
|
||||
* @param _kzgProof The blob KZG point proof.
|
||||
*/
|
||||
function _verifyPointEvaluation(
|
||||
bytes32 _currentDataHash,
|
||||
uint256 _dataEvaluationPoint,
|
||||
uint256 _dataEvaluationClaim,
|
||||
bytes memory _kzgCommitment,
|
||||
bytes memory _kzgProof
|
||||
) internal view {
|
||||
assembly {
|
||||
_dataEvaluationPoint := mod(_dataEvaluationPoint, BLS_CURVE_MODULUS)
|
||||
}
|
||||
|
||||
(bool success, bytes memory returnData) = POINT_EVALUATION_PRECOMPILE_ADDRESS.staticcall(
|
||||
abi.encodePacked(_currentDataHash, _dataEvaluationPoint, _dataEvaluationClaim, _kzgCommitment, _kzgProof)
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
revert PointEvaluationFailed();
|
||||
}
|
||||
|
||||
if (returnData.length != POINT_EVALUATION_RETURN_DATA_LENGTH) {
|
||||
revert PrecompileReturnDataLengthWrong(POINT_EVALUATION_RETURN_DATA_LENGTH, returnData.length);
|
||||
}
|
||||
|
||||
uint256 fieldElements;
|
||||
uint256 blsCurveModulus;
|
||||
assembly {
|
||||
fieldElements := mload(add(returnData, 0x20))
|
||||
blsCurveModulus := mload(add(returnData, POINT_EVALUATION_RETURN_DATA_LENGTH))
|
||||
}
|
||||
if (fieldElements != POINT_EVALUATION_FIELD_ELEMENTS_LENGTH || blsCurveModulus != BLS_CURVE_MODULUS) {
|
||||
revert PointEvaluationResponseInvalid(fieldElements, blsCurveModulus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Finalize compressed blocks with proof.
|
||||
* @dev OPERATOR_ROLE is required to execute.
|
||||
* @param _aggregatedProof The aggregated proof.
|
||||
* @param _proofType The proof type.
|
||||
* @param _finalizationData The full finalization data.
|
||||
*/
|
||||
function finalizeBlocks(
|
||||
bytes calldata _aggregatedProof,
|
||||
uint256 _proofType,
|
||||
FinalizationDataV3 calldata _finalizationData
|
||||
) external whenTypeAndGeneralNotPaused(PauseType.FINALIZATION) onlyRole(OPERATOR_ROLE) {
|
||||
if (_aggregatedProof.length == 0) {
|
||||
revert ProofIsEmpty();
|
||||
}
|
||||
|
||||
uint256 lastFinalizedBlockNumber = currentL2BlockNumber;
|
||||
|
||||
if (stateRootHashes[lastFinalizedBlockNumber] != _finalizationData.parentStateRootHash) {
|
||||
revert StartingRootHashDoesNotMatch();
|
||||
}
|
||||
|
||||
/// @dev currentFinalizedShnarf is updated in _finalizeBlocks and lastFinalizedShnarf MUST be set beforehand for the transition.
|
||||
bytes32 lastFinalizedShnarf = currentFinalizedShnarf;
|
||||
|
||||
bytes32 finalShnarf = _finalizeBlocks(_finalizationData, lastFinalizedBlockNumber);
|
||||
|
||||
uint256 publicInput = _computePublicInput(
|
||||
_finalizationData,
|
||||
lastFinalizedShnarf,
|
||||
finalShnarf,
|
||||
lastFinalizedBlockNumber
|
||||
);
|
||||
|
||||
_verifyProof(publicInput, _proofType, _aggregatedProof);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal function to finalize compressed blocks.
|
||||
* @param _finalizationData The full finalization data.
|
||||
* @param _lastFinalizedBlock The last finalized block.
|
||||
* @return finalShnarf The final computed shnarf in finalizing.
|
||||
*/
|
||||
function _finalizeBlocks(
|
||||
FinalizationDataV3 calldata _finalizationData,
|
||||
uint256 _lastFinalizedBlock
|
||||
) internal returns (bytes32 finalShnarf) {
|
||||
_validateL2ComputedRollingHash(_finalizationData.l1RollingHashMessageNumber, _finalizationData.l1RollingHash);
|
||||
|
||||
if (
|
||||
_computeLastFinalizedState(
|
||||
_finalizationData.lastFinalizedL1RollingHashMessageNumber,
|
||||
_finalizationData.lastFinalizedL1RollingHash,
|
||||
_finalizationData.lastFinalizedTimestamp
|
||||
) != currentFinalizedState
|
||||
) {
|
||||
revert FinalizationStateIncorrect(
|
||||
_computeLastFinalizedState(
|
||||
_finalizationData.lastFinalizedL1RollingHashMessageNumber,
|
||||
_finalizationData.lastFinalizedL1RollingHash,
|
||||
_finalizationData.lastFinalizedTimestamp
|
||||
),
|
||||
currentFinalizedState
|
||||
);
|
||||
}
|
||||
|
||||
if (_finalizationData.finalTimestamp >= block.timestamp) {
|
||||
revert FinalizationInTheFuture(_finalizationData.finalTimestamp, block.timestamp);
|
||||
}
|
||||
|
||||
if (_finalizationData.shnarfData.finalStateRootHash == EMPTY_HASH) {
|
||||
revert FinalBlockStateEqualsZeroHash();
|
||||
}
|
||||
|
||||
finalShnarf = _computeShnarf(
|
||||
_finalizationData.shnarfData.parentShnarf,
|
||||
_finalizationData.shnarfData.snarkHash,
|
||||
_finalizationData.shnarfData.finalStateRootHash,
|
||||
_finalizationData.shnarfData.dataEvaluationPoint,
|
||||
_finalizationData.shnarfData.dataEvaluationClaim
|
||||
);
|
||||
|
||||
if (blobShnarfExists[finalShnarf] == 0) {
|
||||
revert FinalBlobNotSubmitted(finalShnarf);
|
||||
}
|
||||
|
||||
_addL2MerkleRoots(_finalizationData.l2MerkleRoots, _finalizationData.l2MerkleTreesDepth);
|
||||
_anchorL2MessagingBlocks(_finalizationData.l2MessagingBlocksOffsets, _lastFinalizedBlock);
|
||||
|
||||
stateRootHashes[_finalizationData.endBlockNumber] = _finalizationData.shnarfData.finalStateRootHash;
|
||||
|
||||
currentL2BlockNumber = _finalizationData.endBlockNumber;
|
||||
|
||||
currentFinalizedShnarf = finalShnarf;
|
||||
|
||||
currentFinalizedState = _computeLastFinalizedState(
|
||||
_finalizationData.l1RollingHashMessageNumber,
|
||||
_finalizationData.l1RollingHash,
|
||||
_finalizationData.finalTimestamp
|
||||
);
|
||||
|
||||
emit DataFinalizedV3(
|
||||
++_lastFinalizedBlock,
|
||||
_finalizationData.endBlockNumber,
|
||||
finalShnarf,
|
||||
_finalizationData.parentStateRootHash,
|
||||
_finalizationData.shnarfData.finalStateRootHash
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal function to validate l1 rolling hash.
|
||||
* @param _rollingHashMessageNumber Message number associated with the rolling hash as computed on L2.
|
||||
* @param _rollingHash L1 rolling hash as computed on L2.
|
||||
*/
|
||||
function _validateL2ComputedRollingHash(uint256 _rollingHashMessageNumber, bytes32 _rollingHash) internal view {
|
||||
if (_rollingHashMessageNumber == 0) {
|
||||
if (_rollingHash != EMPTY_HASH) {
|
||||
revert MissingMessageNumberForRollingHash(_rollingHash);
|
||||
}
|
||||
} else {
|
||||
if (_rollingHash == EMPTY_HASH) {
|
||||
revert MissingRollingHashForMessageNumber(_rollingHashMessageNumber);
|
||||
}
|
||||
if (rollingHashes[_rollingHashMessageNumber] != _rollingHash) {
|
||||
revert L1RollingHashDoesNotExistOnL1(_rollingHashMessageNumber, _rollingHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal function to calculate Y for public input generation.
|
||||
* @param _data Compressed data from submission data.
|
||||
* @param _dataEvaluationPoint The data evaluation point.
|
||||
* @dev Each chunk of 32 bytes must start with a 0 byte.
|
||||
* @dev The dataEvaluationPoint value is modulo-ed down during the computation and scalar field checking is not needed.
|
||||
* @dev There is a hard constraint in the circuit to enforce the polynomial degree limit (4096), which will also be enforced with EIP-4844.
|
||||
* @return compressedDataComputedY The Y calculated value using the Horner method.
|
||||
*/
|
||||
function _calculateY(
|
||||
bytes calldata _data,
|
||||
bytes32 _dataEvaluationPoint
|
||||
) internal pure returns (bytes32 compressedDataComputedY) {
|
||||
if (_data.length % 0x20 != 0) {
|
||||
revert BytesLengthNotMultipleOf32();
|
||||
}
|
||||
|
||||
bytes4 errorSelector = ILineaRollup.FirstByteIsNotZero.selector;
|
||||
assembly {
|
||||
for {
|
||||
let i := _data.length
|
||||
} gt(i, 0) {} {
|
||||
i := sub(i, 0x20)
|
||||
let chunk := calldataload(add(_data.offset, i))
|
||||
if iszero(iszero(and(chunk, 0xFF00000000000000000000000000000000000000000000000000000000000000))) {
|
||||
let ptr := mload(0x40)
|
||||
mstore(ptr, errorSelector)
|
||||
revert(ptr, 0x4)
|
||||
}
|
||||
compressedDataComputedY := addmod(
|
||||
mulmod(compressedDataComputedY, _dataEvaluationPoint, BLS_CURVE_MODULUS),
|
||||
chunk,
|
||||
BLS_CURVE_MODULUS
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Compute the public input.
|
||||
* @dev Using assembly this way is cheaper gas wise.
|
||||
* @dev NB: the dynamic sized fields are placed last in _finalizationData on purpose to optimise hashing ranges.
|
||||
* @dev Computing the public input as the following:
|
||||
* keccak256(
|
||||
* abi.encode(
|
||||
* _lastFinalizedShnarf,
|
||||
* _finalShnarf,
|
||||
* _finalizationData.lastFinalizedTimestamp,
|
||||
* _finalizationData.finalTimestamp,
|
||||
* _lastFinalizedBlockNumber,
|
||||
* _finalizationData.endBlockNumber,
|
||||
* _finalizationData.lastFinalizedL1RollingHash,
|
||||
* _finalizationData.l1RollingHash,
|
||||
* _finalizationData.lastFinalizedL1RollingHashMessageNumber,
|
||||
* _finalizationData.l1RollingHashMessageNumber,
|
||||
* _finalizationData.l2MerkleTreesDepth,
|
||||
* keccak256(
|
||||
* abi.encodePacked(_finalizationData.l2MerkleRoots)
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* Data is found at the following offsets:
|
||||
* 0x00 parentStateRootHash
|
||||
* 0x20 endBlockNumber
|
||||
* 0x40 shnarfData.parentShnarf
|
||||
* 0x60 shnarfData.snarkHash
|
||||
* 0x80 shnarfData.finalStateRootHash
|
||||
* 0xa0 shnarfData.dataEvaluationPoint
|
||||
* 0xc0 shnarfData.dataEvaluationClaim
|
||||
* 0xe0 lastFinalizedTimestamp
|
||||
* 0x100 finalTimestamp
|
||||
* 0x120 lastFinalizedL1RollingHash
|
||||
* 0x140 l1RollingHash
|
||||
* 0x160 lastFinalizedL1RollingHashMessageNumber
|
||||
* 0x180 l1RollingHashMessageNumber
|
||||
* 0x1a0 l2MerkleTreesDepth
|
||||
* 0x1c0 l2MerkleRootsLengthLocation
|
||||
* 0x1e0 l2MessagingBlocksOffsetsLengthLocation
|
||||
* Dynamic l2MerkleRootsLength
|
||||
* Dynamic l2MerkleRoots
|
||||
* Dynamic l2MessagingBlocksOffsetsLength (location depends on where l2MerkleRoots ends)
|
||||
* Dynamic l2MessagingBlocksOffsets (location depends on where l2MerkleRoots ends)
|
||||
* @param _finalizationData The full finalization data.
|
||||
* @param _finalShnarf The final shnarf in the finalization.
|
||||
* @param _lastFinalizedBlockNumber The last finalized block number.
|
||||
*/
|
||||
function _computePublicInput(
|
||||
FinalizationDataV3 calldata _finalizationData,
|
||||
bytes32 _lastFinalizedShnarf,
|
||||
bytes32 _finalShnarf,
|
||||
uint256 _lastFinalizedBlockNumber
|
||||
) private pure returns (uint256 publicInput) {
|
||||
assembly {
|
||||
let mPtr := mload(0x40)
|
||||
mstore(mPtr, _lastFinalizedShnarf)
|
||||
mstore(add(mPtr, 0x20), _finalShnarf)
|
||||
|
||||
/**
|
||||
* _finalizationData.lastFinalizedTimestamp
|
||||
* _finalizationData.finalTimestamp
|
||||
*/
|
||||
calldatacopy(add(mPtr, 0x40), add(_finalizationData, 0xe0), 0x40)
|
||||
|
||||
mstore(add(mPtr, 0x80), _lastFinalizedBlockNumber)
|
||||
|
||||
// _finalizationData.endBlockNumber
|
||||
calldatacopy(add(mPtr, 0xA0), add(_finalizationData, 0x20), 0x20)
|
||||
|
||||
/**
|
||||
* _finalizationData.lastFinalizedL1RollingHash
|
||||
* _finalizationData.l1RollingHash
|
||||
* _finalizationData.lastFinalizedL1RollingHashMessageNumber
|
||||
* _finalizationData.l1RollingHashMessageNumber
|
||||
* _finalizationData.l2MerkleTreesDepth
|
||||
*/
|
||||
calldatacopy(add(mPtr, 0xC0), add(_finalizationData, 0x120), 0xA0)
|
||||
|
||||
/**
|
||||
* @dev Note the following in hashing the _finalizationData.l2MerkleRoots array:
|
||||
* The second memory pointer and free pointer are offset by 0x20 to temporarily hash the array outside the scope of working memory,
|
||||
* as we need the space left for the array hash to be stored at 0x160.
|
||||
*/
|
||||
let mPtrMerkleRoot := add(mPtr, 0x180)
|
||||
let merkleRootsLengthLocation := add(_finalizationData, calldataload(add(_finalizationData, 0x1c0)))
|
||||
let merkleRootsLen := calldataload(merkleRootsLengthLocation)
|
||||
calldatacopy(mPtrMerkleRoot, add(merkleRootsLengthLocation, 0x20), mul(merkleRootsLen, 0x20))
|
||||
let l2MerkleRootsHash := keccak256(mPtrMerkleRoot, mul(merkleRootsLen, 0x20))
|
||||
mstore(add(mPtr, 0x160), l2MerkleRootsHash)
|
||||
|
||||
publicInput := mod(keccak256(mPtr, 0x180), MODULO_R)
|
||||
}
|
||||
__LineaRollup_init(_initializationData);
|
||||
}
|
||||
}
|
||||
|
||||
727
contracts/src/rollup/LineaRollupBase.sol
Normal file
727
contracts/src/rollup/LineaRollupBase.sol
Normal file
@@ -0,0 +1,727 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { L1MessageService } from "../messaging/l1/L1MessageService.sol";
|
||||
import { ZkEvmV2 } from "./ZkEvmV2.sol";
|
||||
import { ILineaRollup } from "./interfaces/ILineaRollup.sol";
|
||||
import { PermissionsManager } from "../security/access/PermissionsManager.sol";
|
||||
|
||||
import { EfficientLeftRightKeccak } from "../libraries/EfficientLeftRightKeccak.sol";
|
||||
/**
|
||||
* @title Contract to manage cross-chain messaging on L1, L2 data submission, and rollup proof verification.
|
||||
* @author ConsenSys Software Inc.
|
||||
* @custom:security-contact security-report@linea.build
|
||||
*/
|
||||
abstract contract LineaRollupBase is
|
||||
AccessControlUpgradeable,
|
||||
ZkEvmV2,
|
||||
L1MessageService,
|
||||
PermissionsManager,
|
||||
ILineaRollup
|
||||
{
|
||||
using EfficientLeftRightKeccak for *;
|
||||
|
||||
/// @notice The role required to set/add proof verifiers by type.
|
||||
bytes32 public constant VERIFIER_SETTER_ROLE = keccak256("VERIFIER_SETTER_ROLE");
|
||||
|
||||
/// @notice The role required to set/remove proof verifiers by type.
|
||||
bytes32 public constant VERIFIER_UNSETTER_ROLE = keccak256("VERIFIER_UNSETTER_ROLE");
|
||||
|
||||
/// @dev Value indicating a shnarf exists.
|
||||
uint256 internal constant SHNARF_EXISTS_DEFAULT_VALUE = 1;
|
||||
|
||||
/// @dev The default hash value.
|
||||
bytes32 internal constant EMPTY_HASH = 0x0;
|
||||
|
||||
/// @dev The BLS Curve modulus value used.
|
||||
uint256 internal constant BLS_CURVE_MODULUS =
|
||||
52435875175126190479447740508185965837690552500527637822603658699938581184513;
|
||||
|
||||
/// @dev The well-known precompile address for point evaluation.
|
||||
address internal constant POINT_EVALUATION_PRECOMPILE_ADDRESS = address(0x0a);
|
||||
|
||||
/// @dev The expected point evaluation return data length.
|
||||
uint256 internal constant POINT_EVALUATION_RETURN_DATA_LENGTH = 64;
|
||||
|
||||
/// @dev The expected point evaluation field element length returned.
|
||||
uint256 internal constant POINT_EVALUATION_FIELD_ELEMENTS_LENGTH = 4096;
|
||||
|
||||
/// @dev In practice, when used, this is expected to be a close approximation to 6 months, and is intentional.
|
||||
uint256 internal constant SIX_MONTHS_IN_SECONDS = (365 / 2) * 24 * 60 * 60;
|
||||
|
||||
/// @notice This is the ABI version and not the reinitialize version.
|
||||
string private constant _CONTRACT_VERSION = "7.0";
|
||||
|
||||
/// @dev DEPRECATED in favor of the single blobShnarfExists mapping.
|
||||
mapping(bytes32 dataHash => bytes32 finalStateRootHash) private dataFinalStateRootHashes_DEPRECATED;
|
||||
/// @dev DEPRECATED in favor of the single blobShnarfExists mapping.
|
||||
mapping(bytes32 dataHash => bytes32 parentHash) private dataParents_DEPRECATED;
|
||||
/// @dev DEPRECATED in favor of the single blobShnarfExists mapping.
|
||||
mapping(bytes32 dataHash => bytes32 shnarfHash) private dataShnarfHashes_DEPRECATED;
|
||||
/// @dev DEPRECATED in favor of the single blobShnarfExists mapping.
|
||||
mapping(bytes32 dataHash => uint256 startingBlock) private dataStartingBlock_DEPRECATED;
|
||||
/// @dev DEPRECATED in favor of the single blobShnarfExists mapping.
|
||||
mapping(bytes32 dataHash => uint256 endingBlock) private dataEndingBlock_DEPRECATED;
|
||||
|
||||
/// @dev DEPRECATED in favor of currentFinalizedState hash.
|
||||
uint256 private currentL2StoredL1MessageNumber_DEPRECATED;
|
||||
/// @dev DEPRECATED in favor of currentFinalizedState hash.
|
||||
bytes32 private currentL2StoredL1RollingHash_DEPRECATED;
|
||||
|
||||
/// @notice Contains the most recent finalized shnarf.
|
||||
bytes32 public currentFinalizedShnarf;
|
||||
|
||||
/**
|
||||
* @dev NB: THIS IS THE ONLY MAPPING BEING USED FOR DATA SUBMISSION TRACKING.
|
||||
* @dev NB: This was shnarfFinalBlockNumbers and is replaced to indicate only that a shnarf exists with a value of 1.
|
||||
*/
|
||||
mapping(bytes32 shnarf => uint256 exists) public blobShnarfExists;
|
||||
|
||||
/// @notice Hash of the L2 computed L1 message number, rolling hash and finalized timestamp.
|
||||
bytes32 public currentFinalizedState;
|
||||
|
||||
/// @notice The address of the fallback operator.
|
||||
/// @dev This address is granted the OPERATOR_ROLE after six months of finalization inactivity by the current operators.
|
||||
address public fallbackOperator;
|
||||
|
||||
/// @dev Keep 50 free storage slots for inheriting contracts.
|
||||
uint256[50] private __gap_LineaRollup;
|
||||
|
||||
/// @dev Total contract storage is 61 slots.
|
||||
|
||||
/// @custom:oz-upgrades-unsafe-allow constructor
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
function __LineaRollup_init(InitializationData calldata _initializationData) internal virtual {
|
||||
if (_initializationData.defaultVerifier == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
__PauseManager_init(_initializationData.pauseTypeRoles, _initializationData.unpauseTypeRoles);
|
||||
|
||||
__MessageService_init(_initializationData.rateLimitPeriodInSeconds, _initializationData.rateLimitAmountInWei);
|
||||
|
||||
if (_initializationData.defaultAdmin == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev DEFAULT_ADMIN_ROLE is set for the security council explicitly,
|
||||
* as the permissions init purposefully does not allow DEFAULT_ADMIN_ROLE to be set.
|
||||
*/
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, _initializationData.defaultAdmin);
|
||||
|
||||
__Permissions_init(_initializationData.roleAddresses);
|
||||
|
||||
verifiers[0] = _initializationData.defaultVerifier;
|
||||
|
||||
if (_initializationData.fallbackOperator == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
fallbackOperator = _initializationData.fallbackOperator;
|
||||
emit FallbackOperatorAddressSet(msg.sender, _initializationData.fallbackOperator);
|
||||
|
||||
currentL2BlockNumber = _initializationData.initialL2BlockNumber;
|
||||
stateRootHashes[_initializationData.initialL2BlockNumber] = _initializationData.initialStateRootHash;
|
||||
|
||||
bytes32 genesisShnarf = _computeShnarf(
|
||||
EMPTY_HASH,
|
||||
EMPTY_HASH,
|
||||
_initializationData.initialStateRootHash,
|
||||
EMPTY_HASH,
|
||||
EMPTY_HASH
|
||||
);
|
||||
|
||||
blobShnarfExists[genesisShnarf] = SHNARF_EXISTS_DEFAULT_VALUE;
|
||||
currentFinalizedShnarf = genesisShnarf;
|
||||
currentFinalizedState = _computeLastFinalizedState(0, EMPTY_HASH, _initializationData.genesisTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Revokes `role` from the calling account.
|
||||
* @dev Fallback operator cannot renounce role. Reverts with OnlyNonFallbackOperator.
|
||||
* @param _role The role to renounce.
|
||||
* @param _account The account to renounce - can only be the _msgSender().
|
||||
*/
|
||||
function renounceRole(bytes32 _role, address _account) public override {
|
||||
if (_account == fallbackOperator) {
|
||||
revert OnlyNonFallbackOperator();
|
||||
}
|
||||
|
||||
super.renounceRole(_role, _account);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the ABI version and not the reinitialize version.
|
||||
* @return contractVersion The contract ABI version.
|
||||
*/
|
||||
function CONTRACT_VERSION() external view virtual returns (string memory contractVersion) {
|
||||
contractVersion = _CONTRACT_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Adds or updates the verifier contract address for a proof type.
|
||||
* @dev VERIFIER_SETTER_ROLE is required to execute.
|
||||
* @param _newVerifierAddress The address for the verifier contract.
|
||||
* @param _proofType The proof type being set/updated.
|
||||
*/
|
||||
function setVerifierAddress(address _newVerifierAddress, uint256 _proofType) external onlyRole(VERIFIER_SETTER_ROLE) {
|
||||
if (_newVerifierAddress == address(0)) {
|
||||
revert ZeroAddressNotAllowed();
|
||||
}
|
||||
|
||||
emit VerifierAddressChanged(_newVerifierAddress, _proofType, msg.sender, verifiers[_proofType]);
|
||||
|
||||
verifiers[_proofType] = _newVerifierAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets the fallback operator role to the specified address if six months have passed since the last finalization.
|
||||
* @dev Reverts if six months have not passed since the last finalization.
|
||||
* @param _messageNumber Last finalized L1 message number as part of the feedback loop.
|
||||
* @param _rollingHash Last finalized L1 rolling hash as part of the feedback loop.
|
||||
* @param _lastFinalizedTimestamp Last finalized L2 block timestamp.
|
||||
*/
|
||||
function setFallbackOperator(uint256 _messageNumber, bytes32 _rollingHash, uint256 _lastFinalizedTimestamp) external {
|
||||
if (block.timestamp < _lastFinalizedTimestamp + SIX_MONTHS_IN_SECONDS) {
|
||||
revert LastFinalizationTimeNotLapsed();
|
||||
}
|
||||
if (currentFinalizedState != _computeLastFinalizedState(_messageNumber, _rollingHash, _lastFinalizedTimestamp)) {
|
||||
revert FinalizationStateIncorrect(
|
||||
currentFinalizedState,
|
||||
_computeLastFinalizedState(_messageNumber, _rollingHash, _lastFinalizedTimestamp)
|
||||
);
|
||||
}
|
||||
|
||||
address fallbackOperatorAddress = fallbackOperator;
|
||||
|
||||
_grantRole(OPERATOR_ROLE, fallbackOperatorAddress);
|
||||
emit FallbackOperatorRoleGranted(msg.sender, fallbackOperatorAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unset the verifier contract address for a proof type.
|
||||
* @dev VERIFIER_UNSETTER_ROLE is required to execute.
|
||||
* @param _proofType The proof type being set/updated.
|
||||
*/
|
||||
function unsetVerifierAddress(uint256 _proofType) external onlyRole(VERIFIER_UNSETTER_ROLE) {
|
||||
emit VerifierAddressChanged(address(0), _proofType, msg.sender, verifiers[_proofType]);
|
||||
|
||||
delete verifiers[_proofType];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Submit one or more EIP-4844 blobs.
|
||||
* @dev OPERATOR_ROLE is required to execute.
|
||||
* @dev This should be a blob carrying transaction.
|
||||
* @param _blobSubmissions The data for blob submission including proofs and required polynomials.
|
||||
* @param _parentShnarf The parent shnarf used in continuity checks as it includes the parentStateRootHash in its computation.
|
||||
* @param _finalBlobShnarf The expected final shnarf post computation of all the blob shnarfs.
|
||||
*/
|
||||
function submitBlobs(
|
||||
BlobSubmission[] calldata _blobSubmissions,
|
||||
bytes32 _parentShnarf,
|
||||
bytes32 _finalBlobShnarf
|
||||
) external virtual whenTypeAndGeneralNotPaused(PauseType.BLOB_SUBMISSION) onlyRole(OPERATOR_ROLE) {
|
||||
_submitBlobs(_blobSubmissions, _parentShnarf, _finalBlobShnarf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Submit one or more EIP-4844 blobs.
|
||||
* @param _blobSubmissions The data for blob submission including proofs and required polynomials.
|
||||
* @param _parentShnarf The parent shnarf used in continuity checks as it includes the parentStateRootHash in its computation.
|
||||
* @param _finalBlobShnarf The expected final shnarf post computation of all the blob shnarfs.
|
||||
*/
|
||||
function _submitBlobs(
|
||||
BlobSubmission[] calldata _blobSubmissions,
|
||||
bytes32 _parentShnarf,
|
||||
bytes32 _finalBlobShnarf
|
||||
) internal virtual {
|
||||
if (_blobSubmissions.length == 0) {
|
||||
revert BlobSubmissionDataIsMissing();
|
||||
}
|
||||
|
||||
if (blobhash(_blobSubmissions.length) != EMPTY_HASH) {
|
||||
revert BlobSubmissionDataEmpty(_blobSubmissions.length);
|
||||
}
|
||||
|
||||
if (blobShnarfExists[_parentShnarf] == 0) {
|
||||
revert ParentBlobNotSubmitted(_parentShnarf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev validate we haven't submitted the last shnarf. There is a final check at the end of the function verifying,
|
||||
* that _finalBlobShnarf was computed correctly.
|
||||
* Note: As only the last shnarf is stored, we don't need to validate shnarfs,
|
||||
* computed for any previous blobs in the submission (if multiple are submitted).
|
||||
*/
|
||||
if (blobShnarfExists[_finalBlobShnarf] != 0) {
|
||||
revert DataAlreadySubmitted(_finalBlobShnarf);
|
||||
}
|
||||
|
||||
bytes32 currentDataEvaluationPoint;
|
||||
bytes32 currentDataHash;
|
||||
|
||||
/// @dev Assigning in memory saves a lot of gas vs. calldata reading.
|
||||
BlobSubmission memory blobSubmission;
|
||||
|
||||
bytes32 computedShnarf = _parentShnarf;
|
||||
|
||||
for (uint256 i; i < _blobSubmissions.length; i++) {
|
||||
blobSubmission = _blobSubmissions[i];
|
||||
|
||||
currentDataHash = blobhash(i);
|
||||
|
||||
if (currentDataHash == EMPTY_HASH) {
|
||||
revert EmptyBlobDataAtIndex(i);
|
||||
}
|
||||
|
||||
bytes32 snarkHash = blobSubmission.snarkHash;
|
||||
|
||||
currentDataEvaluationPoint = EfficientLeftRightKeccak._efficientKeccak(snarkHash, currentDataHash);
|
||||
|
||||
_verifyPointEvaluation(
|
||||
currentDataHash,
|
||||
uint256(currentDataEvaluationPoint),
|
||||
blobSubmission.dataEvaluationClaim,
|
||||
blobSubmission.kzgCommitment,
|
||||
blobSubmission.kzgProof
|
||||
);
|
||||
|
||||
computedShnarf = _computeShnarf(
|
||||
computedShnarf,
|
||||
snarkHash,
|
||||
blobSubmission.finalStateRootHash,
|
||||
currentDataEvaluationPoint,
|
||||
bytes32(blobSubmission.dataEvaluationClaim)
|
||||
);
|
||||
}
|
||||
|
||||
if (_finalBlobShnarf != computedShnarf) {
|
||||
revert FinalShnarfWrong(_finalBlobShnarf, computedShnarf);
|
||||
}
|
||||
|
||||
/// @dev use the last shnarf as the submission to store as technically it becomes the next parent shnarf.
|
||||
blobShnarfExists[computedShnarf] = SHNARF_EXISTS_DEFAULT_VALUE;
|
||||
|
||||
emit DataSubmittedV3(_parentShnarf, computedShnarf, blobSubmission.finalStateRootHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Submit blobs using compressed data via calldata.
|
||||
* @dev OPERATOR_ROLE is required to execute.
|
||||
* @param _submission The supporting data for compressed data submission including compressed data.
|
||||
* @param _parentShnarf The parent shnarf used in continuity checks as it includes the parentStateRootHash in its computation.
|
||||
* @param _expectedShnarf The expected shnarf post computation of all the submission.
|
||||
*/
|
||||
function submitDataAsCalldata(
|
||||
CompressedCalldataSubmission calldata _submission,
|
||||
bytes32 _parentShnarf,
|
||||
bytes32 _expectedShnarf
|
||||
) external virtual whenTypeAndGeneralNotPaused(PauseType.CALLDATA_SUBMISSION) onlyRole(OPERATOR_ROLE) {
|
||||
_submitDataAsCalldata(_submission, _parentShnarf, _expectedShnarf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Submit blobs using compressed data via calldata.
|
||||
* @dev OPERATOR_ROLE is required to execute.
|
||||
* @param _submission The supporting data for compressed data submission including compressed data.
|
||||
* @param _parentShnarf The parent shnarf used in continuity checks as it includes the parentStateRootHash in its computation.
|
||||
* @param _expectedShnarf The expected shnarf post computation of all the submission.
|
||||
*/
|
||||
function _submitDataAsCalldata(
|
||||
CompressedCalldataSubmission calldata _submission,
|
||||
bytes32 _parentShnarf,
|
||||
bytes32 _expectedShnarf
|
||||
) internal virtual {
|
||||
if (_submission.compressedData.length == 0) {
|
||||
revert EmptySubmissionData();
|
||||
}
|
||||
|
||||
if (blobShnarfExists[_expectedShnarf] != 0) {
|
||||
revert DataAlreadySubmitted(_expectedShnarf);
|
||||
}
|
||||
|
||||
if (blobShnarfExists[_parentShnarf] == 0) {
|
||||
revert ParentBlobNotSubmitted(_parentShnarf);
|
||||
}
|
||||
|
||||
bytes32 currentDataHash = keccak256(_submission.compressedData);
|
||||
|
||||
bytes32 dataEvaluationPoint = EfficientLeftRightKeccak._efficientKeccak(_submission.snarkHash, currentDataHash);
|
||||
|
||||
bytes32 computedShnarf = _computeShnarf(
|
||||
_parentShnarf,
|
||||
_submission.snarkHash,
|
||||
_submission.finalStateRootHash,
|
||||
dataEvaluationPoint,
|
||||
_calculateY(_submission.compressedData, dataEvaluationPoint)
|
||||
);
|
||||
|
||||
if (_expectedShnarf != computedShnarf) {
|
||||
revert FinalShnarfWrong(_expectedShnarf, computedShnarf);
|
||||
}
|
||||
|
||||
blobShnarfExists[computedShnarf] = SHNARF_EXISTS_DEFAULT_VALUE;
|
||||
|
||||
emit DataSubmittedV3(_parentShnarf, computedShnarf, _submission.finalStateRootHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal function to compute and save the finalization state.
|
||||
* @dev Using assembly this way is cheaper gas wise.
|
||||
* @param _messageNumber Is the last L2 computed L1 message number in the finalization.
|
||||
* @param _rollingHash Is the last L2 computed L1 rolling hash in the finalization.
|
||||
* @param _timestamp The final timestamp in the finalization.
|
||||
*/
|
||||
function _computeLastFinalizedState(
|
||||
uint256 _messageNumber,
|
||||
bytes32 _rollingHash,
|
||||
uint256 _timestamp
|
||||
) internal pure returns (bytes32 hashedFinalizationState) {
|
||||
assembly {
|
||||
let mPtr := mload(0x40)
|
||||
mstore(mPtr, _messageNumber)
|
||||
mstore(add(mPtr, 0x20), _rollingHash)
|
||||
mstore(add(mPtr, 0x40), _timestamp)
|
||||
hashedFinalizationState := keccak256(mPtr, 0x60)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal function to compute the shnarf more efficiently.
|
||||
* @dev Using assembly this way is cheaper gas wise.
|
||||
* @param _parentShnarf The shnarf of the parent data item.
|
||||
* @param _snarkHash Is the computed hash for compressed data (using a SNARK-friendly hash function) that aggregates per data submission to be used in public input.
|
||||
* @param _finalStateRootHash The final state root hash of the data being submitted.
|
||||
* @param _dataEvaluationPoint The data evaluation point.
|
||||
* @param _dataEvaluationClaim The data evaluation claim.
|
||||
*/
|
||||
function _computeShnarf(
|
||||
bytes32 _parentShnarf,
|
||||
bytes32 _snarkHash,
|
||||
bytes32 _finalStateRootHash,
|
||||
bytes32 _dataEvaluationPoint,
|
||||
bytes32 _dataEvaluationClaim
|
||||
) internal pure returns (bytes32 shnarf) {
|
||||
assembly {
|
||||
let mPtr := mload(0x40)
|
||||
mstore(mPtr, _parentShnarf)
|
||||
mstore(add(mPtr, 0x20), _snarkHash)
|
||||
mstore(add(mPtr, 0x40), _finalStateRootHash)
|
||||
mstore(add(mPtr, 0x60), _dataEvaluationPoint)
|
||||
mstore(add(mPtr, 0x80), _dataEvaluationClaim)
|
||||
shnarf := keccak256(mPtr, 0xA0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Performs point evaluation for the compressed blob.
|
||||
* @dev _dataEvaluationPoint is modular reduced to be lower than the BLS_CURVE_MODULUS for precompile checks.
|
||||
* @param _currentDataHash The current blob versioned hash.
|
||||
* @param _dataEvaluationPoint The data evaluation point.
|
||||
* @param _dataEvaluationClaim The data evaluation claim.
|
||||
* @param _kzgCommitment The blob KZG commitment.
|
||||
* @param _kzgProof The blob KZG point proof.
|
||||
*/
|
||||
function _verifyPointEvaluation(
|
||||
bytes32 _currentDataHash,
|
||||
uint256 _dataEvaluationPoint,
|
||||
uint256 _dataEvaluationClaim,
|
||||
bytes memory _kzgCommitment,
|
||||
bytes memory _kzgProof
|
||||
) internal view {
|
||||
assembly {
|
||||
_dataEvaluationPoint := mod(_dataEvaluationPoint, BLS_CURVE_MODULUS)
|
||||
}
|
||||
|
||||
(bool success, bytes memory returnData) = POINT_EVALUATION_PRECOMPILE_ADDRESS.staticcall(
|
||||
abi.encodePacked(_currentDataHash, _dataEvaluationPoint, _dataEvaluationClaim, _kzgCommitment, _kzgProof)
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
revert PointEvaluationFailed();
|
||||
}
|
||||
|
||||
if (returnData.length != POINT_EVALUATION_RETURN_DATA_LENGTH) {
|
||||
revert PrecompileReturnDataLengthWrong(POINT_EVALUATION_RETURN_DATA_LENGTH, returnData.length);
|
||||
}
|
||||
|
||||
uint256 fieldElements;
|
||||
uint256 blsCurveModulus;
|
||||
assembly {
|
||||
fieldElements := mload(add(returnData, 0x20))
|
||||
blsCurveModulus := mload(add(returnData, POINT_EVALUATION_RETURN_DATA_LENGTH))
|
||||
}
|
||||
if (fieldElements != POINT_EVALUATION_FIELD_ELEMENTS_LENGTH || blsCurveModulus != BLS_CURVE_MODULUS) {
|
||||
revert PointEvaluationResponseInvalid(fieldElements, blsCurveModulus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Finalize compressed blocks with proof.
|
||||
* @dev OPERATOR_ROLE is required to execute.
|
||||
* @param _aggregatedProof The aggregated proof.
|
||||
* @param _proofType The proof type.
|
||||
* @param _finalizationData The full finalization data.
|
||||
*/
|
||||
function finalizeBlocks(
|
||||
bytes calldata _aggregatedProof,
|
||||
uint256 _proofType,
|
||||
FinalizationDataV3 calldata _finalizationData
|
||||
) external virtual whenTypeAndGeneralNotPaused(PauseType.FINALIZATION) onlyRole(OPERATOR_ROLE) {
|
||||
if (_aggregatedProof.length == 0) {
|
||||
revert ProofIsEmpty();
|
||||
}
|
||||
|
||||
uint256 lastFinalizedBlockNumber = currentL2BlockNumber;
|
||||
|
||||
if (stateRootHashes[lastFinalizedBlockNumber] != _finalizationData.parentStateRootHash) {
|
||||
revert StartingRootHashDoesNotMatch();
|
||||
}
|
||||
|
||||
/// @dev currentFinalizedShnarf is updated in _finalizeBlocks and lastFinalizedShnarf MUST be set beforehand for the transition.
|
||||
bytes32 lastFinalizedShnarf = currentFinalizedShnarf;
|
||||
|
||||
bytes32 finalShnarf = _finalizeBlocks(_finalizationData, lastFinalizedBlockNumber);
|
||||
|
||||
uint256 publicInput = _computePublicInput(
|
||||
_finalizationData,
|
||||
lastFinalizedShnarf,
|
||||
finalShnarf,
|
||||
lastFinalizedBlockNumber
|
||||
);
|
||||
|
||||
_verifyProof(publicInput, _proofType, _aggregatedProof);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal function to finalize compressed blocks.
|
||||
* @param _finalizationData The full finalization data.
|
||||
* @param _lastFinalizedBlock The last finalized block.
|
||||
* @return finalShnarf The final computed shnarf in finalizing.
|
||||
*/
|
||||
function _finalizeBlocks(
|
||||
FinalizationDataV3 calldata _finalizationData,
|
||||
uint256 _lastFinalizedBlock
|
||||
) internal virtual returns (bytes32 finalShnarf) {
|
||||
_validateL2ComputedRollingHash(_finalizationData.l1RollingHashMessageNumber, _finalizationData.l1RollingHash);
|
||||
|
||||
if (
|
||||
_computeLastFinalizedState(
|
||||
_finalizationData.lastFinalizedL1RollingHashMessageNumber,
|
||||
_finalizationData.lastFinalizedL1RollingHash,
|
||||
_finalizationData.lastFinalizedTimestamp
|
||||
) != currentFinalizedState
|
||||
) {
|
||||
revert FinalizationStateIncorrect(
|
||||
_computeLastFinalizedState(
|
||||
_finalizationData.lastFinalizedL1RollingHashMessageNumber,
|
||||
_finalizationData.lastFinalizedL1RollingHash,
|
||||
_finalizationData.lastFinalizedTimestamp
|
||||
),
|
||||
currentFinalizedState
|
||||
);
|
||||
}
|
||||
|
||||
if (_finalizationData.finalTimestamp >= block.timestamp) {
|
||||
revert FinalizationInTheFuture(_finalizationData.finalTimestamp, block.timestamp);
|
||||
}
|
||||
|
||||
if (_finalizationData.shnarfData.finalStateRootHash == EMPTY_HASH) {
|
||||
revert FinalBlockStateEqualsZeroHash();
|
||||
}
|
||||
|
||||
finalShnarf = _computeShnarf(
|
||||
_finalizationData.shnarfData.parentShnarf,
|
||||
_finalizationData.shnarfData.snarkHash,
|
||||
_finalizationData.shnarfData.finalStateRootHash,
|
||||
_finalizationData.shnarfData.dataEvaluationPoint,
|
||||
_finalizationData.shnarfData.dataEvaluationClaim
|
||||
);
|
||||
|
||||
if (blobShnarfExists[finalShnarf] == 0) {
|
||||
revert FinalBlobNotSubmitted(finalShnarf);
|
||||
}
|
||||
|
||||
_addL2MerkleRoots(_finalizationData.l2MerkleRoots, _finalizationData.l2MerkleTreesDepth);
|
||||
_anchorL2MessagingBlocks(_finalizationData.l2MessagingBlocksOffsets, _lastFinalizedBlock);
|
||||
|
||||
stateRootHashes[_finalizationData.endBlockNumber] = _finalizationData.shnarfData.finalStateRootHash;
|
||||
|
||||
currentL2BlockNumber = _finalizationData.endBlockNumber;
|
||||
|
||||
currentFinalizedShnarf = finalShnarf;
|
||||
|
||||
currentFinalizedState = _computeLastFinalizedState(
|
||||
_finalizationData.l1RollingHashMessageNumber,
|
||||
_finalizationData.l1RollingHash,
|
||||
_finalizationData.finalTimestamp
|
||||
);
|
||||
|
||||
emit DataFinalizedV3(
|
||||
++_lastFinalizedBlock,
|
||||
_finalizationData.endBlockNumber,
|
||||
finalShnarf,
|
||||
_finalizationData.parentStateRootHash,
|
||||
_finalizationData.shnarfData.finalStateRootHash
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal function to validate l1 rolling hash.
|
||||
* @param _rollingHashMessageNumber Message number associated with the rolling hash as computed on L2.
|
||||
* @param _rollingHash L1 rolling hash as computed on L2.
|
||||
*/
|
||||
function _validateL2ComputedRollingHash(uint256 _rollingHashMessageNumber, bytes32 _rollingHash) internal view {
|
||||
if (_rollingHashMessageNumber == 0) {
|
||||
if (_rollingHash != EMPTY_HASH) {
|
||||
revert MissingMessageNumberForRollingHash(_rollingHash);
|
||||
}
|
||||
} else {
|
||||
if (_rollingHash == EMPTY_HASH) {
|
||||
revert MissingRollingHashForMessageNumber(_rollingHashMessageNumber);
|
||||
}
|
||||
if (rollingHashes[_rollingHashMessageNumber] != _rollingHash) {
|
||||
revert L1RollingHashDoesNotExistOnL1(_rollingHashMessageNumber, _rollingHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Internal function to calculate Y for public input generation.
|
||||
* @param _data Compressed data from submission data.
|
||||
* @param _dataEvaluationPoint The data evaluation point.
|
||||
* @dev Each chunk of 32 bytes must start with a 0 byte.
|
||||
* @dev The dataEvaluationPoint value is modulo-ed down during the computation and scalar field checking is not needed.
|
||||
* @dev There is a hard constraint in the circuit to enforce the polynomial degree limit (4096), which will also be enforced with EIP-4844.
|
||||
* @return compressedDataComputedY The Y calculated value using the Horner method.
|
||||
*/
|
||||
function _calculateY(
|
||||
bytes calldata _data,
|
||||
bytes32 _dataEvaluationPoint
|
||||
) internal pure returns (bytes32 compressedDataComputedY) {
|
||||
if (_data.length % 0x20 != 0) {
|
||||
revert BytesLengthNotMultipleOf32();
|
||||
}
|
||||
|
||||
bytes4 errorSelector = ILineaRollup.FirstByteIsNotZero.selector;
|
||||
assembly {
|
||||
for {
|
||||
let i := _data.length
|
||||
} gt(i, 0) {} {
|
||||
i := sub(i, 0x20)
|
||||
let chunk := calldataload(add(_data.offset, i))
|
||||
if iszero(iszero(and(chunk, 0xFF00000000000000000000000000000000000000000000000000000000000000))) {
|
||||
let ptr := mload(0x40)
|
||||
mstore(ptr, errorSelector)
|
||||
revert(ptr, 0x4)
|
||||
}
|
||||
compressedDataComputedY := addmod(
|
||||
mulmod(compressedDataComputedY, _dataEvaluationPoint, BLS_CURVE_MODULUS),
|
||||
chunk,
|
||||
BLS_CURVE_MODULUS
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Compute the public input.
|
||||
* @dev Using assembly this way is cheaper gas wise.
|
||||
* @dev NB: the dynamic sized fields are placed last in _finalizationData on purpose to optimise hashing ranges.
|
||||
* @dev Computing the public input as the following:
|
||||
* keccak256(
|
||||
* abi.encode(
|
||||
* _lastFinalizedShnarf,
|
||||
* _finalShnarf,
|
||||
* _finalizationData.lastFinalizedTimestamp,
|
||||
* _finalizationData.finalTimestamp,
|
||||
* _lastFinalizedBlockNumber,
|
||||
* _finalizationData.endBlockNumber,
|
||||
* _finalizationData.lastFinalizedL1RollingHash,
|
||||
* _finalizationData.l1RollingHash,
|
||||
* _finalizationData.lastFinalizedL1RollingHashMessageNumber,
|
||||
* _finalizationData.l1RollingHashMessageNumber,
|
||||
* _finalizationData.l2MerkleTreesDepth,
|
||||
* keccak256(
|
||||
* abi.encodePacked(_finalizationData.l2MerkleRoots)
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* Data is found at the following offsets:
|
||||
* 0x00 parentStateRootHash
|
||||
* 0x20 endBlockNumber
|
||||
* 0x40 shnarfData.parentShnarf
|
||||
* 0x60 shnarfData.snarkHash
|
||||
* 0x80 shnarfData.finalStateRootHash
|
||||
* 0xa0 shnarfData.dataEvaluationPoint
|
||||
* 0xc0 shnarfData.dataEvaluationClaim
|
||||
* 0xe0 lastFinalizedTimestamp
|
||||
* 0x100 finalTimestamp
|
||||
* 0x120 lastFinalizedL1RollingHash
|
||||
* 0x140 l1RollingHash
|
||||
* 0x160 lastFinalizedL1RollingHashMessageNumber
|
||||
* 0x180 l1RollingHashMessageNumber
|
||||
* 0x1a0 l2MerkleTreesDepth
|
||||
* 0x1c0 l2MerkleRootsLengthLocation
|
||||
* 0x1e0 l2MessagingBlocksOffsetsLengthLocation
|
||||
* Dynamic l2MerkleRootsLength
|
||||
* Dynamic l2MerkleRoots
|
||||
* Dynamic l2MessagingBlocksOffsetsLength (location depends on where l2MerkleRoots ends)
|
||||
* Dynamic l2MessagingBlocksOffsets (location depends on where l2MerkleRoots ends)
|
||||
* @param _finalizationData The full finalization data.
|
||||
* @param _finalShnarf The final shnarf in the finalization.
|
||||
* @param _lastFinalizedBlockNumber The last finalized block number.
|
||||
*/
|
||||
function _computePublicInput(
|
||||
FinalizationDataV3 calldata _finalizationData,
|
||||
bytes32 _lastFinalizedShnarf,
|
||||
bytes32 _finalShnarf,
|
||||
uint256 _lastFinalizedBlockNumber
|
||||
) private pure returns (uint256 publicInput) {
|
||||
assembly {
|
||||
let mPtr := mload(0x40)
|
||||
mstore(mPtr, _lastFinalizedShnarf)
|
||||
mstore(add(mPtr, 0x20), _finalShnarf)
|
||||
|
||||
/**
|
||||
* _finalizationData.lastFinalizedTimestamp
|
||||
* _finalizationData.finalTimestamp
|
||||
*/
|
||||
calldatacopy(add(mPtr, 0x40), add(_finalizationData, 0xe0), 0x40)
|
||||
|
||||
mstore(add(mPtr, 0x80), _lastFinalizedBlockNumber)
|
||||
|
||||
// _finalizationData.endBlockNumber
|
||||
calldatacopy(add(mPtr, 0xA0), add(_finalizationData, 0x20), 0x20)
|
||||
|
||||
/**
|
||||
* _finalizationData.lastFinalizedL1RollingHash
|
||||
* _finalizationData.l1RollingHash
|
||||
* _finalizationData.lastFinalizedL1RollingHashMessageNumber
|
||||
* _finalizationData.l1RollingHashMessageNumber
|
||||
* _finalizationData.l2MerkleTreesDepth
|
||||
*/
|
||||
calldatacopy(add(mPtr, 0xC0), add(_finalizationData, 0x120), 0xA0)
|
||||
|
||||
/**
|
||||
* @dev Note the following in hashing the _finalizationData.l2MerkleRoots array:
|
||||
* The second memory pointer and free pointer are offset by 0x20 to temporarily hash the array outside the scope of working memory,
|
||||
* as we need the space left for the array hash to be stored at 0x160.
|
||||
*/
|
||||
let mPtrMerkleRoot := add(mPtr, 0x180)
|
||||
let merkleRootsLengthLocation := add(_finalizationData, calldataload(add(_finalizationData, 0x1c0)))
|
||||
let merkleRootsLen := calldataload(merkleRootsLengthLocation)
|
||||
calldatacopy(mPtrMerkleRoot, add(merkleRootsLengthLocation, 0x20), mul(merkleRootsLen, 0x20))
|
||||
let l2MerkleRootsHash := keccak256(mPtrMerkleRoot, mul(merkleRootsLen, 0x20))
|
||||
mstore(add(mPtr, 0x160), l2MerkleRootsHash)
|
||||
|
||||
publicInput := mod(keccak256(mPtr, 0x180), MODULO_R)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { L1MessageServiceV1 } from "../messaging/l1/v1/L1MessageServiceV1.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { IPauseManager } from "../../security/pausing/interfaces/IPauseManager.sol";
|
||||
import { IPermissionsManager } from "../../security/access/interfaces/IPermissionsManager.sol";
|
||||
@@ -283,6 +283,12 @@ interface ILineaRollup {
|
||||
*/
|
||||
error OnlyNonFallbackOperator();
|
||||
|
||||
/**
|
||||
* @notice Returns the ABI version and not the reinitialize version.
|
||||
* @return contractVersion The contract ABI version.
|
||||
*/
|
||||
function CONTRACT_VERSION() external view returns (string memory contractVersion);
|
||||
|
||||
/**
|
||||
* @notice Adds or updates the verifier contract address for a proof type.
|
||||
* @dev VERIFIER_SETTER_ROLE is required to execute.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title ZkEvm rollup interface for pre-existing functions, events and errors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { IGenericErrors } from "../../interfaces/IGenericErrors.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Interface declaring permissions manager related data types.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { IRateLimiter } from "./interfaces/IRateLimiter.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Interface declaring rate limiting messaging functions, events and errors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { PauseManager } from "./PauseManager.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { PauseManager } from "./PauseManager.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
import { IPauseManager } from "./interfaces/IPauseManager.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { PauseManager } from "./PauseManager.sol";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Interface declaring pre-existing pausing functions, events and errors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Contract that helps prevent reentrant calls.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
import { ITokenMinter } from "./interfaces/ITokenMinter.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Token Minter Interface.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Token Minting Rate Limiter Interface.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
/**
|
||||
* @title Interface declaring verifier functions.
|
||||
|
||||
@@ -323,6 +323,14 @@ describe("TokenBridge", function () {
|
||||
const encodedValidData = ethers.AbiCoder.defaultAbiCoder().encode(["string"], [validString]);
|
||||
expect(await l1TestTokenBridge.testReturnDataToString(encodedValidData)).to.equal(validString);
|
||||
});
|
||||
|
||||
it("Should have the correct contract version", async () => {
|
||||
const { l1TokenBridge, l2TokenBridge } = await loadFixture(deployContractsFixture);
|
||||
|
||||
expect(await l1TokenBridge.CONTRACT_VERSION()).to.equal("1.1");
|
||||
|
||||
expect(await l2TokenBridge.CONTRACT_VERSION()).to.equal("1.1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Permissions", function () {
|
||||
|
||||
@@ -119,6 +119,10 @@ describe("L2MessageService", () => {
|
||||
expect(await l2MessageService.limitInWei()).to.be.equal(INITIAL_WITHDRAW_LIMIT);
|
||||
});
|
||||
|
||||
it("Should have the correct contract version", async () => {
|
||||
expect(await l2MessageService.CONTRACT_VERSION()).to.equal("1.0");
|
||||
});
|
||||
|
||||
it("Should fail to deploy if default admin is address zero", async () => {
|
||||
const deployCall = deployUpgradableFromFactory("TestL2MessageService", [
|
||||
ONE_DAY_IN_SECONDS,
|
||||
|
||||
@@ -264,6 +264,11 @@ describe("Linea Rollup contract", () => {
|
||||
expect(await lineaRollup.hasRole(VERIFIER_SETTER_ROLE, operator.address)).to.be.true;
|
||||
});
|
||||
|
||||
it("Should have the correct contract version", async () => {
|
||||
({ verifier, lineaRollup } = await loadFixture(deployLineaRollupFixture));
|
||||
expect(await lineaRollup.CONTRACT_VERSION()).to.equal("7.0");
|
||||
});
|
||||
|
||||
it("Should revert if the initialize function is called a second time", async () => {
|
||||
({ verifier, lineaRollup } = await loadFixture(deployLineaRollupFixture));
|
||||
const initializeCall = lineaRollup.initialize({
|
||||
@@ -726,12 +731,7 @@ describe("Linea Rollup contract", () => {
|
||||
|
||||
const upgradedContract = await newLineaRollup.waitForDeployment();
|
||||
|
||||
await upgradedContract.reinitializeLineaRollupV6(
|
||||
[],
|
||||
LINEA_ROLLUP_PAUSE_TYPES_ROLES,
|
||||
LINEA_ROLLUP_UNPAUSE_TYPES_ROLES,
|
||||
forwardingProxyAddress,
|
||||
);
|
||||
await upgradedContract.setFallbackOperatorAddress(forwardingProxyAddress);
|
||||
|
||||
// Grants deployed callforwarding proxy as operator
|
||||
await networkTime.increase(SIX_MONTHS_IN_SECONDS);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0
|
||||
pragma solidity 0.8.30;
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
contract TestContract {
|
||||
event TestEvent(address indexed sender, uint256 value);
|
||||
|
||||
Reference in New Issue
Block a user