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 permanent restriction protection #820

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
94b18c1
just merge
StanislavBreadless Sep 10, 2024
9615d90
resolve most of the conflicts
StanislavBreadless Sep 10, 2024
1673186
sync head with base
StanislavBreadless Sep 11, 2024
eb3583a
delete unneeded files
StanislavBreadless Sep 11, 2024
e5727a0
Sync audit head with base (#797)
StanislavBreadless Sep 11, 2024
ef318e2
Merge pull request #798 from matter-labs/sb-sync-head-base
StanislavBreadless Sep 11, 2024
6a8dc33
better governance protection
StanislavBreadless Sep 25, 2024
82a2639
fix old tests
StanislavBreadless Sep 25, 2024
54f3c8b
fix lint
StanislavBreadless Sep 25, 2024
c2c3dbe
tests wip
StanislavBreadless Sep 25, 2024
6ea2aed
min gas for fallable call
StanislavBreadless Sep 25, 2024
4be4c29
full test cover for tryGetNewAdminFromMigration
StanislavBreadless Sep 25, 2024
c77fd57
some rename + better coverage
StanislavBreadless Sep 25, 2024
950862e
lint fix
StanislavBreadless Sep 25, 2024
16a26bc
comment
StanislavBreadless Sep 25, 2024
30f1e54
add tests for l2 admin factory
StanislavBreadless Sep 25, 2024
2c6d116
fix lint
StanislavBreadless Sep 25, 2024
5bd14e1
exclude l2adminfactory as it is covered by l2 tests
StanislavBreadless Sep 25, 2024
61e8cda
use legacy functions
StanislavBreadless Sep 25, 2024
a27fdea
ensure that the function never panics
StanislavBreadless Sep 25, 2024
8e4c1fc
fix things
StanislavBreadless Sep 25, 2024
b18032a
Merge branch 'oz-audit-sep-head' into sb-better-governance-protection
vladbochok Sep 26, 2024
5c5c967
Merge branch 'kl/sync-layer-reorg' into sb-better-governance-protection
StanislavBreadless Sep 26, 2024
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/common/L1ContractErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,10 @@ error IncorrectBatchBounds(
);
// 0x64107968
error AssetHandlerNotRegistered(bytes32 assetId);
// 0x10f30e75
error NotBridgehub(address addr);
// 0x2554babc
error InvalidAddress(address expected, address actual);

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 WhitelistL2Admin(address indexed adminAddress, bool isWhitelisted);
}
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));
}
}
107 changes: 103 additions & 4 deletions l1-contracts/contracts/governance/PermanentRestriction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

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, NotWhitelisted, NotBridgehub, InvalidSelector, InvalidAddress} 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";
Expand All @@ -22,10 +25,16 @@ import {IPermanentRestriction} from "./IPermanentRestriction.sol";
/// 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 +44,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 whitelistedL2Admins;

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 +87,52 @@ 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 whitelistL2Admin(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 (whitelistedL2Admins[expectedAddress]) {
revert AlreadyWhitelisted(expectedAddress);
}

whitelistedL2Admins[expectedAddress] = true;
emit WhitelistL2Admin(expectedAddress, true);
}

/// @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 {
try this.tryGetNewAdminFromMigration(_call) returns (address admin) {
StanislavBreadless marked this conversation as resolved.
Show resolved Hide resolved
if (!whitelistedL2Admins[admin]) {
revert NotWhitelisted(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 @@ -183,4 +235,51 @@ 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)) {
Copy link
Member

Choose a reason for hiding this comment

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

Right now we use ChainAdmin contract as an admin in BridgeHub, SharedBridge, and other contracts. The logic is fine, just need to remember that ChainAdmin can be used only for Chains adminship

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think we will need to deploy a different chainadmin contract for those ones

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(without the permanent rollup restriction)

revert NotBridgehub(_call.target);
}

if (bytes4(_call.data[:4]) != IBridgehub.requestL2TransactionTwoBridges.selector) {
StanislavBreadless marked this conversation as resolved.
Show resolved Hide resolved
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(request.secondBridgeAddress, sharedBridge);
}

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;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pragma solidity 0.8.24;

import "@openzeppelin/contracts-v4/utils/Strings.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts-v4/proxy/transparent/TransparentUpgradeableProxy.sol";
import {Bridgehub} from "contracts/bridgehub/Bridgehub.sol";
import {Diamond} from "contracts/state-transition/libraries/Diamond.sol";
import {ChainTypeManager} from "contracts/state-transition/ChainTypeManager.sol";
Expand All @@ -22,12 +23,15 @@ import {IMessageRoot} from "contracts/bridgehub/IMessageRoot.sol";
import {MessageRoot} from "contracts/bridgehub/MessageRoot.sol";
import {IL1AssetRouter} from "contracts/bridge/asset-router/IL1AssetRouter.sol";
import {IL1Nullifier} from "contracts/bridge/interfaces/IL1Nullifier.sol";
import {IBridgehub} from "contracts/bridgehub/IBridgehub.sol";

contract PermanentRestrictionTest is ChainTypeManagerTest {
ChainAdmin internal chainAdmin;
AccessControlRestriction internal restriction;
PermanentRestriction internal permRestriction;

address constant L2_FACTORY_ADDR = address(0);

address internal owner;
address internal hyperchain;

Expand All @@ -40,16 +44,36 @@ contract PermanentRestrictionTest is ChainTypeManagerTest {

owner = makeAddr("owner");
hyperchain = chainContractAddress.getHyperchain(chainId);
permRestriction = new PermanentRestriction(owner, bridgehub);
(permRestriction, ) = _deployPermRestriction(bridgehub, L2_FACTORY_ADDR, owner);
restriction = new AccessControlRestriction(0, owner);
address[] memory restrictions = new address[](1);
restrictions[0] = address(restriction);
chainAdmin = new ChainAdmin(restrictions);
}

function _deployPermRestriction(
IBridgehub _bridgehub,
address _l2AdminFactory,
address _owner
) internal returns (PermanentRestriction proxy, PermanentRestriction impl) {
impl = new PermanentRestriction(_bridgehub, _l2AdminFactory);
TransparentUpgradeableProxy tup = new TransparentUpgradeableProxy(
address(impl),
address(uint160(1)),
abi.encodeCall(PermanentRestriction.initialize, (_owner))
);

proxy = PermanentRestriction(address(tup));
}

function test_ownerAsAddressZero() public {
PermanentRestriction impl = new PermanentRestriction(bridgehub, L2_FACTORY_ADDR);
vm.expectRevert(ZeroAddress.selector);
permRestriction = new PermanentRestriction(address(0), bridgehub);
new TransparentUpgradeableProxy(
address(impl),
address(uint160(1)),
abi.encodeCall(PermanentRestriction.initialize, (address(0)))
);
}

function test_allowAdminImplementation(bytes32 implementationHash) public {
Expand Down
Loading