Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better governance protection (audit) #822

Merged
merged 16 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions l1-contracts/contracts/bridgehub/IBridgehub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,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 @@ -407,6 +407,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
owner_address = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"

[chain]
base_token_addr = "0x0000000000000000000000000000000000000001"
base_token_gas_price_multiplier_denominator = 1
base_token_gas_price_multiplier_nominator = 1
bridgehub_create_new_chain_salt = 10
chain_chain_id = 10
governance_min_delay = 0
governance_security_council_address = "0x0000000000000000000000000000000000000000"
validator_sender_operator_blobs_eth = "0x0000000000000000000000000000000000000001"
validator_sender_operator_commit_eth = "0x0000000000000000000000000000000000000000"
validium_mode = 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
owner_address = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"

[chain]
base_token_addr = "0x0000000000000000000000000000000000000001"
base_token_gas_price_multiplier_denominator = 1
base_token_gas_price_multiplier_nominator = 1
bridgehub_create_new_chain_salt = 11
chain_chain_id = 11
governance_min_delay = 0
governance_security_council_address = "0x0000000000000000000000000000000000000000"
validator_sender_operator_blobs_eth = "0x0000000000000000000000000000000000000001"
validator_sender_operator_commit_eth = "0x0000000000000000000000000000000000000000"
validium_mode = 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
owner_address = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"

[chain]
base_token_addr = "0x0000000000000000000000000000000000000001"
base_token_gas_price_multiplier_denominator = 1
base_token_gas_price_multiplier_nominator = 1
bridgehub_create_new_chain_salt = 9
chain_chain_id = 9
governance_min_delay = 0
governance_security_council_address = "0x0000000000000000000000000000000000000000"
validator_sender_operator_blobs_eth = "0x0000000000000000000000000000000000000001"
validator_sender_operator_commit_eth = "0x0000000000000000000000000000000000000000"
validium_mode = 0
Loading
Loading