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:
The Dark Jester
2025-06-17 18:26:24 +02:00
committed by GitHub
parent 490ad2af70
commit e66abc64fd
97 changed files with 2157 additions and 1385 deletions

View File

@@ -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";

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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 {}

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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;

View File

@@ -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.
}
}

View 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 {}
}

View 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.
}
}

View File

@@ -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";

View File

@@ -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);
}
}

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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";

View 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);
}
}

View File

@@ -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);
}

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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";

View File

@@ -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";

View File

@@ -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);
}
}

View 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);
}
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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";

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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";

View File

@@ -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";

View File

@@ -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.

View File

@@ -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";

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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";

View File

@@ -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";

View File

@@ -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.

View File

@@ -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";

View File

@@ -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
);
}
}

View 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;
}
}

View File

@@ -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.

View File

@@ -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";

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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";

View File

@@ -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.

View File

@@ -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";

View File

@@ -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.

View File

@@ -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);
}
}

View 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)
}
}
}

View File

@@ -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";

View File

@@ -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.

View File

@@ -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.

View File

@@ -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";

View File

@@ -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.

View File

@@ -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";

View File

@@ -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.

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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.

View File

@@ -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.

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.30;
pragma solidity ^0.8.30;
/**
* @title Token Minter Interface.

View File

@@ -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.

View File

@@ -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.