Skip to content

Commit

Permalink
Merge 5c5c967 into d7ee927
Browse files Browse the repository at this point in the history
  • Loading branch information
StanislavBreadless authored Sep 26, 2024
2 parents d7ee927 + 5c5c967 commit ac32611
Show file tree
Hide file tree
Showing 8 changed files with 401 additions and 13 deletions.
4 changes: 4 additions & 0 deletions l1-contracts/contracts/bridgehub/IBridgehub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,8 @@ interface IBridgehub is IAssetHandler, IL1AssetHandler {
function registerAlreadyDeployedZKChain(uint256 _chainId, address _hyperchain) external;

function setLegacyChainAddress(uint256 _chainId) external;

/// @notice return the ZK chain contract for a chainId
/// @dev It is a legacy method. Do not use!
function getHyperchain(uint256 _chainId) external view returns (address);
}
6 changes: 6 additions & 0 deletions l1-contracts/contracts/common/L1ContractErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,12 @@ error IncorrectBatchBounds(
);
// 0x64107968
error AssetHandlerNotRegistered(bytes32 assetId);
// 0x10f30e75
error NotBridgehub(address addr);
// 0x2554babc
error InvalidAddress(address expected, address actual);
// 0xfa5cd00f
error NotAllowed(address addr);

enum SharedBridgeKey {
PostUpgradeFirstBatch,
Expand Down
3 changes: 3 additions & 0 deletions l1-contracts/contracts/governance/IPermanentRestriction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ interface IPermanentRestriction {

/// @notice Emitted when the selector is labeled as validated or not.
event SelectorValidationChanged(bytes4 indexed selector, bool isValidated);

/// @notice Emitted when the L2 admin is whitelisted or not.
event AllowL2Admin(address indexed adminAddress);
}
42 changes: 42 additions & 0 deletions l1-contracts/contracts/governance/L2AdminFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

import {ChainAdmin} from "./ChainAdmin.sol";

/// @author Matter Labs
/// @custom:security-contact [email protected]
/// @dev Contract used to deploy ChainAdmin contracts on L2.
/// @dev It can be used to ensure that certain L2 admins are deployed with
/// predefined restrictions. E.g. it can be used to deploy admins that ensure that
/// a chain is a permanent rollup.
/// @dev This contract is expected to be deployed in zkEVM (L2) environment.
/// @dev The contract is immutable, in case the restrictions need to be changed,
/// a new contract should be deployed.
contract L2AdminFactory {
event AdminDeployed(address admin);

/// @dev We use storage instead of immutable variables due to the
/// specifics of the zkEVM environment, where storage is actually cheaper.
address[] public requiredRestrictions;

constructor(address[] memory _requiredRestrictions) {
requiredRestrictions = _requiredRestrictions;
}

/// @notice Deploys a new L2 admin contract.
/// @return admin The address of the deployed admin contract.
function deployAdmin(address[] calldata _additionalRestrictions, bytes32 _salt) external returns (address admin) {
address[] memory restrictions = new address[](requiredRestrictions.length + _additionalRestrictions.length);
uint256 cachedRequired = requiredRestrictions.length;
for (uint256 i = 0; i < cachedRequired; ++i) {
restrictions[i] = requiredRestrictions[i];
}
uint256 cachedAdditional = _additionalRestrictions.length;
for (uint256 i = 0; i < cachedAdditional; ++i) {
restrictions[requiredRestrictions.length + i] = _additionalRestrictions[i];
}

admin = address(new ChainAdmin{salt: _salt}(restrictions));
}
}
137 changes: 131 additions & 6 deletions l1-contracts/contracts/governance/PermanentRestriction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,45 @@

pragma solidity 0.8.24;

import {CallNotAllowed, ChainZeroAddress, NotAHyperchain, NotAnAdmin, RemovingPermanentRestriction, ZeroAddress, UnallowedImplementation} from "../common/L1ContractErrors.sol";
import {UnsupportedEncodingVersion, CallNotAllowed, ChainZeroAddress, NotAHyperchain, NotAnAdmin, RemovingPermanentRestriction, ZeroAddress, UnallowedImplementation, AlreadyWhitelisted, NotAllowed, NotBridgehub, InvalidSelector, InvalidAddress, NotEnoughGas} from "../common/L1ContractErrors.sol";

import {Ownable2Step} from "@openzeppelin/contracts-v4/access/Ownable2Step.sol";
import {L2TransactionRequestTwoBridgesOuter, BridgehubBurnCTMAssetData} from "../bridgehub/IBridgehub.sol";
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable-v4/access/Ownable2StepUpgradeable.sol";
import {L2ContractHelper} from "../common/libraries/L2ContractHelper.sol";
import {NEW_ENCODING_VERSION} from "../bridge/asset-router/IAssetRouterBase.sol";

import {Call} from "./Common.sol";
import {IRestriction} from "./IRestriction.sol";
import {IChainAdmin} from "./IChainAdmin.sol";
import {IBridgehub} from "../bridgehub/IBridgehub.sol";
import {IZKChain} from "../state-transition/chain-interfaces/IZKChain.sol";
import {IGetters} from "../state-transition/chain-interfaces/IGetters.sol";
import {IAdmin} from "../state-transition/chain-interfaces/IAdmin.sol";

import {IPermanentRestriction} from "./IPermanentRestriction.sol";

/// @dev We use try-catch to test whether some of the conditions should be checked.
/// To avoid attacks based on the 63/64 gas limitations, we ensure that each such call
/// has at least this amount.
uint256 constant MIN_GAS_FOR_FALLABLE_CALL = 5_000_000;

/// @title PermanentRestriction contract
/// @author Matter Labs
/// @custom:security-contact [email protected]
/// @notice This contract should be used by chains that wish to guarantee that certain security
/// properties are preserved forever.
/// @dev To be deployed as a transparent upgradable proxy, owned by a trusted decentralized governance.
/// @dev Once of the instances of such contract is to ensure that a ZkSyncHyperchain is a rollup forever.
contract PermanentRestriction is IRestriction, IPermanentRestriction, Ownable2Step {
contract PermanentRestriction is IRestriction, IPermanentRestriction, Ownable2StepUpgradeable {
/// @notice The address of the Bridgehub contract.
IBridgehub public immutable BRIDGE_HUB;

/// @notice The address of the L2 admin factory that should be used to deploy the chain admins
/// for chains that migrated on top of an L2 settlement layer.
/// @dev If this contract is deployed on L2, this address is 0.
/// @dev This address is expected to be the same on all L2 chains.
address public immutable L2_ADMIN_FACTORY;

/// @notice The mapping of the allowed admin implementations.
mapping(bytes32 implementationCodeHash => bool isAllowed) public allowedAdminImplementations;

Expand All @@ -35,9 +50,15 @@ contract PermanentRestriction is IRestriction, IPermanentRestriction, Ownable2St
/// @notice The mapping of the validated selectors.
mapping(bytes4 selector => bool isValidated) public validatedSelectors;

constructor(address _initialOwner, IBridgehub _bridgehub) {
/// @notice The mapping of whitelisted L2 admins.
mapping(address adminAddress => bool isWhitelisted) public allowedL2Admins;

constructor(IBridgehub _bridgehub, address _l2AdminFactory) {
BRIDGE_HUB = _bridgehub;
L2_ADMIN_FACTORY = _l2AdminFactory;
}

function initialize(address _initialOwner) external initializer {
// solhint-disable-next-line gas-custom-errors, reason-string
if (_initialOwner == address(0)) {
revert ZeroAddress();
Expand Down Expand Up @@ -72,15 +93,53 @@ contract PermanentRestriction is IRestriction, IPermanentRestriction, Ownable2St
emit SelectorValidationChanged(_selector, _isValidated);
}

/// @notice Whitelists a certain L2 admin.
/// @param deploymentSalt The salt for the deployment.
/// @param l2BytecodeHash The hash of the L2 bytecode.
/// @param constructorInputHash The hash of the constructor data for the deployment.
function allowL2Admin(bytes32 deploymentSalt, bytes32 l2BytecodeHash, bytes32 constructorInputHash) external {
// We do not do any additional validations for constructor data or the bytecode,
// we expect that only admins of the allowed format are to be deployed.
address expectedAddress = L2ContractHelper.computeCreate2Address(
L2_ADMIN_FACTORY,
deploymentSalt,
l2BytecodeHash,
constructorInputHash
);

if (allowedL2Admins[expectedAddress]) {
revert AlreadyWhitelisted(expectedAddress);
}

allowedL2Admins[expectedAddress] = true;
emit AllowL2Admin(expectedAddress);
}

/// @inheritdoc IRestriction
function validateCall(
Call calldata _call,
address // _invoker
) external view override {
_validateAsChainAdmin(_call);
_validateMigrationToL2(_call);
_validateRemoveRestriction(_call);
}

/// @notice Validates the migration to an L2 settlement layer.
/// @param _call The call data.
/// @dev Note that we do not need to validate the migration to the L1 layer as the admin
/// is not changed in this case.
function _validateMigrationToL2(Call calldata _call) internal view {
_ensureEnoughGas();
try this.tryGetNewAdminFromMigration(_call) returns (address admin) {
if (!allowedL2Admins[admin]) {
revert NotAllowed(admin);
}
} catch {
// It was not the migration call, so we do nothing
}
}

/// @notice Validates the call as the chain admin
/// @param _call The call data.
function _validateAsChainAdmin(Call calldata _call) internal view {
Expand Down Expand Up @@ -153,6 +212,7 @@ contract PermanentRestriction is IRestriction, IPermanentRestriction, Ownable2St
/// @notice Checks if the `msg.sender` is an admin of a certain ZkSyncHyperchain.
/// @param _chain The address of the chain.
function _isAdminOfAChain(address _chain) internal view returns (bool) {
_ensureEnoughGas();
(bool success, ) = address(this).staticcall(abi.encodeCall(this.tryCompareAdminOfAChain, (_chain, msg.sender)));
return success;
}
Expand All @@ -172,8 +232,20 @@ contract PermanentRestriction is IRestriction, IPermanentRestriction, Ownable2St
// - Query it for `chainId`. If it reverts, it is not a ZkSyncHyperchain.
// - Query the Bridgehub for the Hyperchain with the given `chainId`.
// - We compare the corresponding addresses
uint256 chainId = IZKChain(_chain).getChainId();
if (BRIDGE_HUB.getZKChain(chainId) != _chain) {

// Note, that we do not use an explicit call here to ensure that the function does not panic in case of
// incorrect `_chain` address.
(bool success, bytes memory data) = _chain.staticcall(abi.encodeWithSelector(IGetters.getChainId.selector));
if (!success || data.length < 32) {
revert NotAHyperchain(_chain);
}

// Can not fail
uint256 chainId = abi.decode(data, (uint256));

// Note, that here it is important to use the legacy `getHyperchain` function, so that the contract
// is compatible with the legacy ones.
if (BRIDGE_HUB.getHyperchain(chainId) != _chain) {
revert NotAHyperchain(_chain);
}

Expand All @@ -183,4 +255,57 @@ contract PermanentRestriction is IRestriction, IPermanentRestriction, Ownable2St
revert NotAnAdmin(admin, _potentialAdmin);
}
}

/// @notice Tries to get the new admin from the migration.
/// @param _call The call data.
/// @dev This function reverts if the provided call was not a migration call.
function tryGetNewAdminFromMigration(Call calldata _call) external view returns (address) {
if (_call.target != address(BRIDGE_HUB)) {
revert NotBridgehub(_call.target);
}

if (bytes4(_call.data[:4]) != IBridgehub.requestL2TransactionTwoBridges.selector) {
revert InvalidSelector(bytes4(_call.data[:4]));
}

address sharedBridge = BRIDGE_HUB.sharedBridge();

L2TransactionRequestTwoBridgesOuter memory request = abi.decode(
_call.data[4:],
(L2TransactionRequestTwoBridgesOuter)
);

if (request.secondBridgeAddress != sharedBridge) {
revert InvalidAddress(sharedBridge, request.secondBridgeAddress);
}

bytes memory secondBridgeData = request.secondBridgeCalldata;
if (secondBridgeData[0] != NEW_ENCODING_VERSION) {
revert UnsupportedEncodingVersion();
}
bytes memory encodedData = new bytes(secondBridgeData.length - 1);
assembly {
mcopy(add(encodedData, 0x20), add(secondBridgeData, 0x21), mload(encodedData))
}

(bytes32 chainAssetId, bytes memory bridgehubData) = abi.decode(encodedData, (bytes32, bytes));
// We will just check that the chainAssetId is a valid chainAssetId.
// For now, for simplicity, we do not check that the admin is exactly the admin
// of this chain.
address ctmAddress = BRIDGE_HUB.ctmAssetIdToAddress(chainAssetId);
if (ctmAddress == address(0)) {
revert ZeroAddress();
}

BridgehubBurnCTMAssetData memory burnData = abi.decode(bridgehubData, (BridgehubBurnCTMAssetData));
(address l2Admin, ) = abi.decode(burnData.ctmData, (address, bytes));

return l2Admin;
}

function _ensureEnoughGas() internal view {
if (gasleft() < MIN_GAS_FOR_FALLABLE_CALL) {
revert NotEnoughGas();
}
}
}
2 changes: 1 addition & 1 deletion l1-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"test:foundry": "forge test --ffi --match-path 'test/foundry/l1/*'",
"test:zkfoundry": "forge test --zksync --match-path 'test/foundry/l2/*'",
"test:fork": "TEST_CONTRACTS_FORK=1 yarn run hardhat test test/unit_tests/*.fork.ts --network hardhat",
"coverage:foundry": "forge coverage --ffi --match-path 'test/foundry/l1/*' --no-match-coverage 'contracts/bridge/.*L2.*.sol'",
"coverage:foundry": "forge coverage --ffi --match-path 'test/foundry/l1/*' --no-match-coverage 'contracts/(bridge/.*L2.*\\.sol|governance/L2AdminFactory\\.sol)'",
"deploy-no-build": "ts-node scripts/deploy.ts",
"register-zk-chain": "ts-node scripts/register-zk-chain.ts",
"deploy-weth-bridges": "ts-node scripts/deploy-weth-bridges.ts",
Expand Down
Loading

0 comments on commit ac32611

Please sign in to comment.