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

Merge dev into kl sync layer reorg #777

Merged
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
1 change: 1 addition & 0 deletions .solhintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ l1-contracts/lib
l1-contracts/node_modules
l1-contracts/contracts/dev-contracts
l1-contracts/test
l1-contracts/deploy-scripts

# l1-contracts-foundry
l1-contracts-foundry/cache
Expand Down
22 changes: 22 additions & 0 deletions l1-contracts/contracts/common/L1ContractErrors.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

// 0x5ecf2d7a
error AccessToFallbackDenied(address target, address invoker);
// 0x3995f750
error AccessToFunctionDenied(address target, bytes4 selector, address invoker);
// 0x6c167909
error OnlySelfAllowed();
// 0x52e22c98
error RestrictionWasNotPresent(address restriction);
// 0xf126e113
error RestrictionWasAlreadyPresent(address restriction);
// 0x3331e9c0
error CallNotAllowed(bytes call);
// 0x59e1b0d2
error ChainZeroAddress();
// 0xff4bbdf1
error NotAHyperchain(address chainAddress);
// 0xa3decdf3
error NotAnAdmin(address expected, address actual);
// 0xf6fd7071
error RemovingPermanentRestriction();
// 0xfcb9b2e1
error UnallowedImplementation(bytes32 implementationHash);
// 0x1ff9d522
error AddressAlreadyUsed(address addr);
// 0x86bb51b8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pragma solidity 0.8.24;

import {IGovernance} from "../../governance/IGovernance.sol";
import {Call} from "../../governance/Common.sol";

contract ReenterGovernance {
// add this to be excluded from coverage report
Expand All @@ -12,7 +13,7 @@ contract ReenterGovernance {

// Store call, predecessor and salt separately,
// because Operation struct can't be stored on storage.
IGovernance.Call call;
Call call;
bytes32 predecessor;
bytes32 salt;

Expand Down Expand Up @@ -45,7 +46,7 @@ contract ReenterGovernance {
fallback() external payable {
if (!alreadyReentered) {
alreadyReentered = true;
IGovernance.Call[] memory calls = new IGovernance.Call[](1);
Call[] memory calls = new Call[](1);
calls[0] = call;
IGovernance.Operation memory op = IGovernance.Operation({
calls: calls,
Expand Down
72 changes: 72 additions & 0 deletions l1-contracts/contracts/governance/AccessControlRestriction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

import {AccessToFallbackDenied, AccessToFunctionDenied} from "../common/L1ContractErrors.sol";
import {IAccessControlRestriction} from "./IAccessControlRestriction.sol";
import {AccessControlDefaultAdminRules} from "@openzeppelin/contracts-v4/access/AccessControlDefaultAdminRules.sol";
import {IRestriction} from "./IRestriction.sol";
import {Call} from "./Common.sol";

/// @author Matter Labs
/// @custom:security-contact [email protected]
/// @notice The Restriction that is designed to provide the access control logic for the `ChainAdmin` contract.
/// @dev It inherits from `AccessControlDefaultAdminRules` without overriding `_setRoleAdmin` functionaity. In other
/// words, the `DEFAULT_ADMIN_ROLE` is the only role that can manage roles. This is done for simplicity.
/// @dev An instance of this restriction should be deployed separately for each `ChainAdmin` contract.
/// @dev IMPORTANT: this function does not validate the ability of the invoker to use `msg.value`. Thus,
/// either all callers with access to functions should be trusted to not steal ETH from the `ChainAdmin` account
/// or not ETH should be passively stored in `ChainAdmin` account.
contract AccessControlRestriction is IRestriction, IAccessControlRestriction, AccessControlDefaultAdminRules {
/// @notice Required roles to call a specific functions.
/// @dev Note, that the role 0 means the `DEFAULT_ADMIN_ROLE` from the `AccessControlDefaultAdminRules` contract.
mapping(address target => mapping(bytes4 selector => bytes32 requiredRole)) public requiredRoles;

/// @notice Required roles to call a fallback function.
mapping(address target => bytes32 requiredRole) public requiredRolesForFallback;

constructor(
uint48 initialDelay,
address initialDefaultAdmin
) AccessControlDefaultAdminRules(initialDelay, initialDefaultAdmin) {}

/// @notice Sets the required role for a specific function call.
/// @param _target The address of the contract.
/// @param _selector The selector of the function.
/// @param _requiredRole The required role.
function setRequiredRoleForCall(
address _target,
bytes4 _selector,
bytes32 _requiredRole
) external onlyRole(DEFAULT_ADMIN_ROLE) {
requiredRoles[_target][_selector] = _requiredRole;

emit RoleSet(_target, _selector, _requiredRole);
}

/// @notice Sets the required role for a fallback function call.
/// @param _target The address of the contract.
/// @param _requiredRole The required role.
function setRequiredRoleForFallback(address _target, bytes32 _requiredRole) external onlyRole(DEFAULT_ADMIN_ROLE) {
requiredRolesForFallback[_target] = _requiredRole;

emit FallbackRoleSet(_target, _requiredRole);
}

/// @inheritdoc IRestriction
function validateCall(Call calldata _call, address _invoker) external view {
// Note, that since `DEFAULT_ADMIN_ROLE` is 0 and the default storage value for the
// `requiredRoles` and `requiredRolesForFallback` is 0, the default admin is by default a required
// role for all the functions.
if (_call.data.length < 4) {
if (!hasRole(requiredRolesForFallback[_call.target], _invoker)) {
revert AccessToFallbackDenied(_call.target, _invoker);
}
} else {
bytes4 selector = bytes4(_call.data[:4]);
if (!hasRole(requiredRoles[_call.target][selector], _invoker)) {
revert AccessToFunctionDenied(_call.target, selector, _invoker);
}
}
}
}
114 changes: 78 additions & 36 deletions l1-contracts/contracts/governance/ChainAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,76 @@

pragma solidity 0.8.24;

import {Ownable2Step} from "@openzeppelin/contracts-v4/access/Ownable2Step.sol";
// solhint-disable gas-length-in-loops

import {NoCallsProvided, OnlySelfAllowed, RestrictionWasNotPresent, RestrictionWasAlreadyPresent} from "../common/L1ContractErrors.sol";
import {IChainAdmin} from "./IChainAdmin.sol";
import {IAdmin} from "../state-transition/chain-interfaces/IAdmin.sol";
import {NoCallsProvided, Unauthorized, ZeroAddress} from "../common/L1ContractErrors.sol";
import {IRestriction} from "./IRestriction.sol";
import {Call} from "./Common.sol";

import {EnumerableSet} from "@openzeppelin/contracts-v4/utils/structs/EnumerableSet.sol";
import {ReentrancyGuard} from "../common/ReentrancyGuard.sol";

/// @author Matter Labs
/// @custom:security-contact [email protected]
/// @notice The contract is designed to hold the `admin` role in ZKSync Chain (State Transition) contracts.
/// The owner of the contract can perform any external calls and also save the information needed for
/// the blockchain node to accept the protocol upgrade. Another role - `tokenMultiplierSetter` can be used in the contract
/// to change the base token gas price in the Chain contract.
contract ChainAdmin is IChainAdmin, Ownable2Step {
/// the blockchain node to accept the protocol upgrade.
contract ChainAdmin is IChainAdmin, ReentrancyGuard {
using EnumerableSet for EnumerableSet.AddressSet;

/// @notice Ensures that only the `ChainAdmin` contract itself can call the function.
/// @dev All functions that require access-control should use `onlySelf` modifier, while the access control logic
/// should be implemented in the restriction contracts.
modifier onlySelf() {
if (msg.sender != address(this)) {
revert OnlySelfAllowed();
}
_;
}

constructor(address[] memory _initialRestrictions) reentrancyGuardInitializer {
unchecked {
for (uint256 i = 0; i < _initialRestrictions.length; ++i) {
_addRestriction(_initialRestrictions[i]);
}
}
}

/// @notice Mapping of protocol versions to their expected upgrade timestamps.
/// @dev Needed for the offchain node administration to know when to start building batches with the new protocol version.
mapping(uint256 protocolVersion => uint256 upgradeTimestamp) public protocolVersionToUpgradeTimestamp;

/// @notice The address which can call `setTokenMultiplier` function to change the base token gas price in the Chain contract.
/// @dev The token base price can be changed quite often, so the private key for this role is supposed to be stored in the node
/// and used by the automated service in a way similar to the sequencer workflow.
address public tokenMultiplierSetter;
/// @notice The set of active restrictions.
EnumerableSet.AddressSet internal activeRestrictions;

constructor(address _initialOwner, address _initialTokenMultiplierSetter) {
if (_initialOwner == address(0)) {
revert ZeroAddress();
}
_transferOwnership(_initialOwner);
// Can be zero if no one has this permission.
tokenMultiplierSetter = _initialTokenMultiplierSetter;
emit NewTokenMultiplierSetter(address(0), _initialTokenMultiplierSetter);
/// @notice Returns the list of active restrictions.
function getRestrictions() public view returns (address[] memory) {
return activeRestrictions.values();
}

/// @inheritdoc IChainAdmin
function isRestrictionActive(address _restriction) external view returns (bool) {
return activeRestrictions.contains(_restriction);
}

/// @notice Updates the address responsible for setting token multipliers on the Chain contract .
/// @param _tokenMultiplierSetter The new address to be set as the token multiplier setter.
function setTokenMultiplierSetter(address _tokenMultiplierSetter) external onlyOwner {
emit NewTokenMultiplierSetter(tokenMultiplierSetter, _tokenMultiplierSetter);
tokenMultiplierSetter = _tokenMultiplierSetter;
/// @inheritdoc IChainAdmin
function addRestriction(address _restriction) external onlySelf {
_addRestriction(_restriction);
}

/// @inheritdoc IChainAdmin
function removeRestriction(address _restriction) external onlySelf {
if (!activeRestrictions.remove(_restriction)) {
revert RestrictionWasNotPresent(_restriction);
}
emit RestrictionRemoved(_restriction);
}

/// @notice Set the expected upgrade timestamp for a specific protocol version.
/// @param _protocolVersion The ZKsync chain protocol version.
/// @param _upgradeTimestamp The timestamp at which the chain node should expect the upgrade to happen.
function setUpgradeTimestamp(uint256 _protocolVersion, uint256 _upgradeTimestamp) external onlyOwner {
function setUpgradeTimestamp(uint256 _protocolVersion, uint256 _upgradeTimestamp) external onlySelf {
protocolVersionToUpgradeTimestamp[_protocolVersion] = _upgradeTimestamp;
emit UpdateUpgradeTimestamp(_protocolVersion, _upgradeTimestamp);
}
Expand All @@ -52,12 +80,16 @@ contract ChainAdmin is IChainAdmin, Ownable2Step {
/// @param _calls Array of Call structures defining target, value, and data for each call.
/// @param _requireSuccess If true, reverts transaction on any call failure.
/// @dev Intended for batch processing of contract interactions, managing gas efficiency and atomicity of operations.
function multicall(Call[] calldata _calls, bool _requireSuccess) external payable onlyOwner {
/// @dev Note, that this function lacks access control. It is expected that the access control is implemented in a separate restriction contract.
/// @dev Even though all the validation from external modules is executed via `staticcall`, the function
/// is marked as `nonReentrant` to prevent reentrancy attacks in case the staticcall restriction is lifted in the future.
function multicall(Call[] calldata _calls, bool _requireSuccess) external payable nonReentrant {
if (_calls.length == 0) {
revert NoCallsProvided();
}
// solhint-disable-next-line gas-length-in-loops
for (uint256 i = 0; i < _calls.length; ++i) {
_validateCall(_calls[i]);

// slither-disable-next-line arbitrary-send-eth
(bool success, bytes memory returnData) = _calls[i].target.call{value: _calls[i].value}(_calls[i].data);
if (_requireSuccess && !success) {
Expand All @@ -70,17 +102,27 @@ contract ChainAdmin is IChainAdmin, Ownable2Step {
}
}

/// @notice Sets the token multiplier in the specified Chain contract.
/// @param _chainContract The chain contract address where the token multiplier will be set.
/// @param _nominator The numerator part of the token multiplier.
/// @param _denominator The denominator part of the token multiplier.
function setTokenMultiplier(IAdmin _chainContract, uint128 _nominator, uint128 _denominator) external {
if (msg.sender != tokenMultiplierSetter) {
revert Unauthorized(msg.sender);
/// @dev Contract might receive/hold ETH as part of the maintenance process.
receive() external payable {}

/// @notice Function that returns the current admin can perform the call.
/// @dev By default it always returns true, but can be overridden in derived contracts.
function _validateCall(Call calldata _call) internal view {
address[] memory restrictions = getRestrictions();

unchecked {
for (uint256 i = 0; i < restrictions.length; ++i) {
IRestriction(restrictions[i]).validateCall(_call, msg.sender);
}
}
_chainContract.setTokenMultiplier(_nominator, _denominator);
}

/// @dev Contract might receive/hold ETH as part of the maintenance process.
receive() external payable {}
/// @notice Adds a new restriction to the active restrictions set.
/// @param _restriction The address of the restriction contract to be added.
function _addRestriction(address _restriction) internal {
if (!activeRestrictions.add(_restriction)) {
revert RestrictionWasAlreadyPresent(_restriction);
}
emit RestrictionAdded(_restriction);
}
}
13 changes: 13 additions & 0 deletions l1-contracts/contracts/governance/Common.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

/// @dev Represents a call to be made during multicall.
/// @param target The address to which the call will be made.
/// @param value The amount of Ether (in wei) to be sent along with the call.
/// @param data The calldata to be executed on the `target` address.
struct Call {
address target;
uint256 value;
bytes data;
}
1 change: 1 addition & 0 deletions l1-contracts/contracts/governance/Governance.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.24;

import {Ownable2Step} from "@openzeppelin/contracts-v4/access/Ownable2Step.sol";
import {IGovernance} from "./IGovernance.sol";
import {Call} from "./Common.sol";
import {ZeroAddress, Unauthorized, OperationMustBeReady, OperationMustBePending, OperationExists, InvalidDelay, PreviousOperationNotExecuted} from "../common/L1ContractErrors.sol";

/// @author Matter Labs
Expand Down
14 changes: 14 additions & 0 deletions l1-contracts/contracts/governance/IAccessControlRestriction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

/// @title AccessControlRestriction contract interface
/// @author Matter Labs
/// @custom:security-contact [email protected]
interface IAccessControlRestriction {
/// @notice Emitted when the required role for a specific function is set.
event RoleSet(address indexed target, bytes4 indexed selector, bytes32 requiredRole);

/// @notice Emitted when the required role for a fallback function is set.
event FallbackRoleSet(address indexed target, bytes32 requiredRole);
}
39 changes: 20 additions & 19 deletions l1-contracts/contracts/governance/IChainAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,37 @@

pragma solidity 0.8.24;

import {IAdmin} from "../state-transition/chain-interfaces/IAdmin.sol";
import {Call} from "./Common.sol";

/// @title ChainAdmin contract interface
/// @author Matter Labs
/// @custom:security-contact [email protected]
interface IChainAdmin {
/// @dev Represents a call to be made during multicall.
/// @param target The address to which the call will be made.
/// @param value The amount of Ether (in wei) to be sent along with the call.
/// @param data The calldata to be executed on the `target` address.
struct Call {
address target;
uint256 value;
bytes data;
}

/// @notice Emitted when the expected upgrade timestamp for a specific protocol version is set.
event UpdateUpgradeTimestamp(uint256 indexed _protocolVersion, uint256 _upgradeTimestamp);
event UpdateUpgradeTimestamp(uint256 indexed protocolVersion, uint256 upgradeTimestamp);

/// @notice Emitted when the call is executed from the contract.
event CallExecuted(Call _call, bool _success, bytes _returnData);
event CallExecuted(Call call, bool success, bytes returnData);

/// @notice Emitted when a new restriction is added.
event RestrictionAdded(address indexed restriction);

/// @notice Emitted when the new token multiplier address is set.
event NewTokenMultiplierSetter(address _oldTokenMultiplierSetter, address _newTokenMultiplierSetter);
/// @notice Emitted when a restriction is removed.
event RestrictionRemoved(address indexed restriction);

function setTokenMultiplierSetter(address _tokenMultiplierSetter) external;
/// @notice Returns the list of active restrictions.
function getRestrictions() external view returns (address[] memory);

function setUpgradeTimestamp(uint256 _protocolVersion, uint256 _upgradeTimestamp) external;
/// @notice Checks if the restriction is active.
/// @param _restriction The address of the restriction contract.
function isRestrictionActive(address _restriction) external view returns (bool);

function multicall(Call[] calldata _calls, bool _requireSuccess) external payable;
/// @notice Adds a new restriction to the active restrictions set.
/// @param _restriction The address of the restriction contract.
function addRestriction(address _restriction) external;

function setTokenMultiplier(IAdmin _chainContract, uint128 _nominator, uint128 _denominator) external;
/// @notice Removes a restriction from the active restrictions set.
/// @param _restriction The address of the restriction contract.
/// @dev Sometimes restrictions might need to enforce their permanence (e.g. if a chain should be a rollup forever).
function removeRestriction(address _restriction) external;
}
Loading
Loading