Platform: Code4rena
Start Date: 07/03/2024
Pot Size: $250,000 USDC
Total HM: 5
Participants: 24
Period: 21 days
Judge: 0xsomeone
Total Solo HM: 3
Id: 347
League: ETH
Rank: 7/24
Findings: 2
Award: $975.51
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: 0x11singh99
Also found by: Bauchibred, Dup1337, Topmark, XDZIBECX, bctester, bin2chen, erebus, forgebyola, oakcobalt, rvierdiiev, yashar, zhanmingjing
734.7056 USDC - $734.71
StateTransitionManager::unfreezeChain
function is meant for unfreeze the freezed chain of passed _chainId
param. While freezeChain
function is meant for freeze the chain according to passed _chainId. But freezeChain
and unfreezeChain
both functions are calling same function freezeDiamond
by same line IZkSyncStateTransition(stateTransition[_chainId]).freezeDiamond()
by mistake. So both these function will only freeze the chain.
Also there is no other function inside StateTransitionManager.sol
contract which is calling unfreezeDiamond
. unfreezeDiamond
is function defined in Admin.sol
where the call is going since IZkSyncStateTransition
also inherits IAdmin which have freezeDiamond and unfreezeDiamond both functions. But unfreezeDiamond
is not called from unfreezeChain
function. So freezed chain will never be unfreeze.
unfreezeChain
also have wrong comment instead of writing unfreezes it writes freezes.
It seems like dev just copy pasted without doing required changes.
code/contracts/ethereum/contracts/state-transition/StateTransitionManager.sol#L165-L167
159: /// @dev freezes the specified chain 160: function freezeChain(uint256 _chainId) external onlyOwner { 161: IZkSyncStateTransition(stateTransition[_chainId]).freezeDiamond(); } 164: /// @dev freezes the specified chain 165: function unfreezeChain(uint256 _chainId) external onlyOwner { 166: IZkSyncStateTransition(stateTransition[_chainId]).freezeDiamond();//@audit `freezeDiamond` called instead of `unfreezeDiamond` }
IZkSyncStateTransition is inheriting IAdmin and by IZkSyncStateTransition wrapping instance is prepared to call freezeDiamond.
15: interface IZkSyncStateTransition is IAdmin, IExecutor, IGetters, IMailbox {
IAdmin interfaces have both functions
13: interface IAdmin is IZkSyncStateTransitionBase { .... 52: /// @notice Instantly pause the functionality of all freezable facets & their selectors /// @dev Only the governance mechanism may freeze Diamond Proxy 54: function freezeDiamond() external; /// @notice Unpause the functionality of all freezable facets & their selectors /// @dev Both the admin and the STM can unfreeze Diamond Proxy 58: function unfreezeDiamond() external;
It shows that by mistake unfreezeChain is calling freezeDiamond instaed of unfreezeDiamond which should be used to unfreeze th chain.
freezed chain will never be unfreeze.
Since freezeChain
and unfreezeChain
both functions are calling same function freezeDiamond
which is used to freeze the chain. And unfreezeDiamond
no where called which should is made for unfreeze the freezed chain.
Manual Review
In StateTransitionManager::unfreezeChain
function call unfreezeDiamond
instead of freezeDiamond
on IZkSyncStateTransition(stateTransition[_chainId])
instance.
File: code/contracts/ethereum/contracts/state-transition/StateTransitionManager.sol - 164: /// @dev freezes the specified chain + 164: /// @dev unfreezes the specified chain 165: function unfreezeChain(uint256 _chainId) external onlyOwner { - 166: IZkSyncStateTransition(stateTransition[_chainId]).freezeDiamond(); + 166: IZkSyncStateTransition(stateTransition[_chainId]).unfreezeDiamond(); }
Other
#0 - c4-judge
2024-04-02T17:01:47Z
alex-ppg marked the issue as primary issue
#1 - c4-judge
2024-04-02T17:03:34Z
alex-ppg changed the severity to 3 (High Risk)
#2 - c4-sponsor
2024-04-04T10:31:25Z
saxenism (sponsor) confirmed
#3 - c4-sponsor
2024-04-04T10:31:32Z
saxenism marked the issue as disagree with severity
#4 - saxenism
2024-04-04T10:32:24Z
This is a good finding, but we consider this a medium
severity issue because in the current codebase admin
could also unfreeze (so no permanent freeze & so not high), but in the future we might wanna change this mechanism
#5 - c4-judge
2024-04-29T13:51:26Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2024-04-29T13:51:29Z
alex-ppg marked the issue as selected for report
#7 - alex-ppg
2024-04-29T13:51:47Z
The submission and its relevant duplicates have identified a mistype in the codebase that causes certain functionality that is expected to be accessible to behave oppositely.
The exhibit represents an actual error in the code resulting in functionality missing, however, the functionality does contain an alternative access path per the Sponsor's statement and as such I consider this exhibit to be of medium risk.
#8 - c4-judge
2024-04-29T13:51:55Z
alex-ppg changed the severity to 2 (Med Risk)
🌟 Selected for report: Bauchibred
Also found by: 0x11singh99, ChaseTheLight, Dup1337, hihen, lsaudit, oakcobalt
240.8022 USDC - $240.80
BaseZkSyncUpgradeGenesis::_setNewProtocolVersion
unchecked
blockrenounceOwnership
functionaddress(0x0)
in the constructor
l2Bridge
, l2TokenBeacon
, and l2TokenProxyBytecodeHash
are not explicitly initializedTotal Low Risk Issues | 9 |
---|
Event
missing NatSpec @dev
tagupgrade
function declares a return
type but does not explicitly return
any value@return
tag@author
tags@dev
tags@dev
tag@param
tagTotal Non-Critical Issues | 9 |
---|
BaseZkSyncUpgradeGenesis::_setNewProtocolVersion
The _setNewProtocolVersion
function, which is called internally to change the protocol version, already includes a requirement that the new protocol version must be greater than or equal to the previous one.
Additionally, it checks if the difference between the new and previous protocol versions does not exceed a certain threshold (MAX_ALLOWED_PROTOCOL_VERSION_DELTA
). The existing checks in _setNewProtocolVersion
already ensure that the upgrade process adheres to sensible constraints, preventing sudden jumps in protocol versions that could potentially disrupt the system.
File : contracts/ethereum/contracts/upgrades/BaseZkSyncUpgradeGenesis.sol 20: function _setNewProtocolVersion(uint256 _newProtocolVersion) internal override { uint256 previousProtocolVersion = s.protocolVersion; require( // Genesis Upgrade difference: Note this is the only thing change > to >= _newProtocolVersion >= previousProtocolVersion, "New protocol version is not greater than the current one" ); require( 28: _newProtocolVersion - previousProtocolVersion <= MAX_ALLOWED_PROTOCOL_VERSION_DELTA, "Too big protocol version difference" ); File : contracts/ethereum/contracts/state-transition/StateTransitionManager.sol 142: function setNewVersionUpgrade( Diamond.DiamondCutData calldata _cutData, uint256 _oldProtocolVersion, uint256 _newProtocolVersion ) external onlyOwner { upgradeCutHash[_oldProtocolVersion] = keccak256(abi.encode(_cutData)); 148: protocolVersion = _newProtocolVersion; }
The _getERC20Getters
function should check the success status of each staticcall
using the return value and revert the transaction if any call fails. Adding error handling logic to check the success status of staticcall
operations and revert the transaction if any call fails.
File : contracts/ethereum/contracts/bridge/L1SharedBridge.sol 254: function _getERC20Getters(address _token) internal view returns (bytes memory) { if (_token == ETH_TOKEN_ADDRESS) { bytes memory name = bytes("Ether"); bytes memory symbol = bytes("ETH"); bytes memory decimals = abi.encode(uint8(18)); return abi.encode(name, symbol, decimals); // when depositing eth to a non-eth based chain it is an ERC20 } (, bytes memory data1) = _token.staticcall(abi.encodeCall(IERC20Metadata.name, ())); (, bytes memory data2) = _token.staticcall(abi.encodeCall(IERC20Metadata.symbol, ())); (, bytes memory data3) = _token.staticcall(abi.encodeCall(IERC20Metadata.decimals, ())); return abi.encode(data1, data2, data3);
Emitting events before external calls in Solidity enhances transaction atomicity, provides comprehensive event logging, helps protect against front-running attacks, improves user experience, and enables better gas optimization. It's a best practice that contributes to the security, transparency, and efficiency of smart contract development.
File : contracts/ethereum/contracts/bridgehub/Bridgehub.sol 114: function createNewChain( ... 137: IStateTransitionManager(_stateTransitionManager).createNewChain( 138: _chainId, 139: _baseToken, 140: address(sharedBridge), 141: _admin, 142: _initData 143: ); 144: 145: emit NewChain(_chainId, _stateTransitionManager, _admin);
unchecked
blockIt mentions that the offset value used in the addition operation is significant, implying that it might be large enough to potentially cause overflow issues. Removing the unchecked block ensures that arithmetic operations are checked for overflow and underflow, reducing the likelihood of vulnerabilities and promoting code safety and reliability.
Remove unchecked
block from applyL1ToL2Alias
and undoL1ToL2Alias
functions
unchecked
addition
may cause unwanted overflow
since offsets also a big valueFile : contracts/ethereum/contracts/vendor/AddressAliasHelper.sol 28: function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { 29: unchecked { 30: l2Address = address(uint160(l1Address) + offset); 31: }
unchecked
subtraction
may cause unwanted underflow
since offsets also a big valueFile : contracts/ethereum/contracts/vendor/AddressAliasHelper.sol 38: function undoL1ToL2Alias(address l2Address) internal pure returns (address l1Address) { 39: unchecked { 40: l1Address = address(uint160(l2Address) - offset); 41: }
The event NewAdmin
emits the address(0) as the pendingAdmin
, which might be misleading since the pendingAdmin
is deleted. Instead of emitting address(0) as the pendingAdmin
, it would be clearer to emit the currentPendingAdmin
address, as it represents the newAdmin
address after the pendingAdmin
is set as the newAdmin
.
File : contracts/ethereum/contracts/bridgehub/Bridgehub.sol 65: admin = currentPendingAdmin; 66: delete pendingAdmin; 67: 68: emit NewPendingAdmin(currentPendingAdmin, address(0)); 69: emit NewAdmin(previousAdmin, pendingAdmin);
renounceOwnership
functionThe override of the renounceOwnership
function inherited from the OpenZeppelin
Ownable
contract. It recommends overriding the renounceOwnership
function to revert if called, preventing accidental ownership renunciation. The rationale behind this is to add an extra layer of protection against accidental or malicious removal of ownership.
File : contracts/ethereum/contracts/governance/Governance.sol 129: function scheduleTransparent(Operation calldata _operation, uint256 _delay) external onlyOwner { 130: bytes32 id = hashOperation(_operation); 131: _schedule(id, _delay); 132: emit TransparentOperationScheduled(id, _delay, _operation); 133: }
address(0x0)
in the constructor
_admin
and _securityCouncil
File : contracts/ethereum/contracts/governance/Governance.sol 41: constructor(address _admin, address _securityCouncil, uint256 _minDelay) { require(_admin != address(0), "Admin should be non zero address"); _transferOwnership(_admin); 46: securityCouncil = _securityCouncil; emit ChangeSecurityCouncil(address(0), _securityCouncil); 49: minDelay = _minDelay; emit ChangeMinDelay(0, _minDelay); }
_l1WethAddress
, _bridgehub
and _legacyBridge
File : contracts/ethereum/contracts/bridge/L1SharedBridge.sol 90: constructor( 91: address _l1WethAddress, 92: IBridgehub _bridgehub, 93: IL1ERC20Bridge _legacyBridge ) reentrancyGuardInitializer { _disableInitializers(); 96: l1WethAddress = _l1WethAddress; 97: bridgehub = _bridgehub; 98: legacyBridge = _legacyBridge; }
Adding a zero address check for each address type parameter can prevent errors.
_owner
for address zeroSince _transferOwnership
don't have address zero check
File : contracts/ethereum/contracts/bridgehub/Bridgehub.sol 41: function initialize(address _owner) external reentrancyGuardInitializer { 42: _transferOwnership(_owner); 43: }
_stateTransitionManager
for address zeroFile : contracts/ethereum/contracts/bridgehub/Bridgehub.sol 92: function removeStateTransitionManager(address _stateTransitionManager) external onlyOwner { 93: require( 94: stateTransitionManagerIsRegistered[_stateTransitionManager], 95: "Bridgehub: state transition not registered yet" 96: ); 97: stateTransitionManagerIsRegistered[_stateTransitionManager] = false;
_sharedBridge
for address zeroFile : contracts/ethereum/contracts/bridgehub/Bridgehub.sol 108: function setSharedBridge(address _sharedBridge) external onlyOwner { 109: sharedBridge = IL1SharedBridge(_sharedBridge);
l2Bridge
, l2TokenBeacon
, and l2TokenProxyBytecodeHash
are not explicitly initializedIn L1ERC20Bridge
contract , If these variables are not initialized explicitly, they will indeed take on the default values for their respective types. You may want to initialize these variables in the constructor or at some point during the contract execution to ensure they hold meaningful values.
Otherwise they will always gives 0 value.
File : contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol 36: address public l2Bridge; /// @dev The address that is used as a beacon for L2 tokens in zkSync Era. 39: address public l2TokenBeacon; /// @dev Stores the hash of the L2 token proxy contract's bytecode on zkSync Era. 42: bytes32 public l2TokenProxyBytecodeHash;
Event
missing NatSpec @dev
tagFile : contracts/ethereum/contracts/upgrades/BaseZkSyncUpgrade.sol /// @notice Changes the protocol version 46: event NewProtocolVersion(uint256 indexed previousProtocolVersion, uint256 indexed newProtocolVersion); /// @notice Сhanges to the bytecode that is used in L2 as a bootloader (start program) 49: event NewL2BootloaderBytecodeHash(bytes32 indexed previousBytecodeHash, bytes32 indexed newBytecodeHash); /// @notice Сhanges to the bytecode that is used in L2 as a default account 52: event NewL2DefaultAccountBytecodeHash(bytes32 indexed previousBytecodeHash, bytes32 indexed newBytecodeHash); /// @notice Verifier address changed 55: event NewVerifier(address indexed oldVerifier, address indexed newVerifier); /// @notice Verifier parameters changed 58: event NewVerifierParams(VerifierParams oldVerifierParams, VerifierParams newVerifierParams); /// @notice Notifies about complete upgrade 61: event UpgradeComplete(uint256 indexed newProtocolVersion, bytes32 indexed l2UpgradeTxHash, ProposedUpgrade upgrade);
upgrade
function declares a return
type but does not explicitly return
any valueWithin the upgrade
function implementation, there is no explicit return statement that provides a bytes32 value. The function executes several upgrade steps and emits an event UpgradeComplete
but does not explicitly return any value.
File : contracts/ethereum/contracts/upgrades/BaseZkSyncUpgradeGenesis.sol 47: function upgrade(ProposedUpgrade calldata _proposedUpgrade) public virtual override returns (bytes32) { // Note that due to commitment delay, the timestamp of the L2 upgrade batch may be earlier than the timestamp // of the L1 block at which the upgrade occurred. This means that using timestamp as a signifier of "upgraded" // on the L2 side would be inaccurate. The effects of this "back-dating" of L2 upgrade batches will be reduced // as the permitted delay window is reduced in the future. require(block.timestamp >= _proposedUpgrade.upgradeTimestamp, "Upgrade is not ready yet"); _setNewProtocolVersion(_proposedUpgrade.newProtocolVersion); _upgradeL1Contract(_proposedUpgrade.l1ContractsUpgradeCalldata); _upgradeVerifier(_proposedUpgrade.verifier, _proposedUpgrade.verifierParams); _setBaseSystemContracts(_proposedUpgrade.bootloaderHash, _proposedUpgrade.defaultAccountHash); bytes32 txHash = _setL2SystemContractUpgrade( _proposedUpgrade.l2ProtocolUpgradeTx, _proposedUpgrade.factoryDeps, _proposedUpgrade.newProtocolVersion ); _postUpgrade(_proposedUpgrade.postUpgradeCalldata); 67: emit UpgradeComplete(_proposedUpgrade.newProtocolVersion, txHash, _proposedUpgrade); }
@return
tagFile : contracts/ethereum/contracts/upgrades/DefaultUpgrade.sol 11: /// @notice The main function that will be called by the upgrade proxy. 12: /// @param _proposedUpgrade The upgrade to be executed. 13: function upgrade(ProposedUpgrade calldata _proposedUpgrade) public override returns (bytes32) {
File : contracts/ethereum/contracts/upgrades/GenesisUpgrade.sol 12: /// @notice The main function that will be called by the upgrade proxy. 13: /// @param _proposedUpgrade The upgrade to be executed. 14: function upgrade(ProposedUpgrade calldata _proposedUpgrade) public override returns (bytes32) {
File : contracts/ethereum/contracts/bridgehub/Bridgehub.sol 111: 112: /// @notice register new chain 113: /// @notice for Eth the baseToken address is 1 114: function createNewChain( 150: 151: /// @notice forwards function call to Mailbox based on ChainId 152: function proveL2MessageInclusion( 162: 163: /// @notice forwards function call to Mailbox based on ChainId 164: function proveL2LogInclusion(
@author
tagsFile : contracts/ethereum/contracts/vendor/AddressAliasHelper.sol 20: 21: library AddressAliasHelper {
@dev
tagsFile : contracts/ethereum/contracts/vendor/AddressAliasHelper.sol 20: 21: library AddressAliasHelper {
@dev
tagFile : contracts/ethereum/contracts/bridgehub/Bridgehub.sol 44: 45: modifier onlyOwnerOrAdmin() {
File : contracts/ethereum/contracts/governance/Governance.sol 56: 57: /// @notice Checks that the message sender is contract itself. 58: modifier onlySelf() { 62: 63: /// @notice Checks that the message sender is an active security council. 64: modifier onlySecurityCouncil() { 68: 69: /// @notice Checks that the message sender is an active owner or an active security council. 70: modifier onlyOwnerOrSecurityCouncil() {
@param
tagFile : contracts/ethereum/contracts/bridgehub/Bridgehub.sol 44: 45: modifier onlyOwnerOrAdmin() {
File : contracts/ethereum/contracts/bridgehub/Bridgehub.sol 114: function createNewChain( uint256 _chainId, address _stateTransitionManager, address _baseToken, uint256, //_salt address _admin, bytes calldata _initData 121: ) external onlyOwnerOrAdmin nonReentrant returns (uint256 chainId) { require(_chainId != 0, "Bridgehub: chainId cannot be 0"); require(_chainId <= type(uint48).max, "Bridgehub: chainId too large"); require( stateTransitionManagerIsRegistered[_stateTransitionManager], "Bridgehub: state transition not registered" ); require(tokenIsRegistered[_baseToken], "Bridgehub: token not registered"); require(address(sharedBridge) != address(0), "Bridgehub: weth bridge not set"); require(stateTransitionManager[_chainId] == address(0), "Bridgehub: chainId already registered"); stateTransitionManager[_chainId] = _stateTransitionManager; baseToken[_chainId] = _baseToken; IStateTransitionManager(_stateTransitionManager).createNewChain( _chainId, _baseToken, address(sharedBridge), _admin, _initData ); emit NewChain(_chainId, _stateTransitionManager, _admin); 146: return _chainId; }
See the order of functions section of the Solidity Style Guide
File : 84: function isOperation(bytes32 _id) public view returns (bool) { 89: function isOperationPending(bytes32 _id) public view returns (bool) { 95: function isOperationReady(bytes32 _id) public view returns (bool) { 100: function isOperationDone(bytes32 _id) public view returns (bool) { 105: function getOperationState(bytes32 _id) public view returns (OperationState) { 129: function scheduleTransparent(Operation calldata _operation, uint256 _delay) external onlyOwner { 142: function scheduleShadow(bytes32 _id, uint256 _delay) external onlyOwner { 154: function cancel(bytes32 _id) external onlyOwner { 167: function execute(Operation calldata _operation) external payable onlyOwnerOrSecurityCouncil { 186 function executeInstant(Operation calldata _operation) external payable onlySecurityCouncil { 204: function hashOperation(Operation calldata _operation) public pure returns (bytes32) { 215: function _schedule(bytes32 _id, uint256 _delay) internal { 224: function _execute(Call[] calldata _calls) internal { 239: function _checkPredecessorDone(bytes32 _predecessorId) internal view { 249: function updateDelay(uint256 _newDelay) external onlySelf { 256: function updateSecurityCouncil(address _newSecurityCouncil) external onlySelf { 262: receive() external payable {}
84, 89, 95, 100, 105, 129, 142, 154, 167, 186, 204, 215, 224, 239, 249, 256, 262
#0 - c4-judge
2024-04-29T13:39:03Z
alex-ppg marked the issue as grade-b
#1 - thenua3bhai
2024-04-30T16:35:30Z
Hi @alex-ppg Thanks for judging
[L-09] in this QA report is a duplicate of #77 and can be upgraded to medium. This is the similar issue of state var. not set during initialization but in different file.
issue 77 states that l1LegacyBridge
is not set in L2SharedBridge
While [L-09] in this QA report states that l2Bridge
and other two state variables are not set in L1ERC20Bridge.sol initialize function. Also they are never initialized inside contract using any function, only used directly as their by default value 0.
Thanks
#2 - alex-ppg
2024-05-02T12:05:22Z
Hey @thenua3bhai, thanks for contributing to the PJQA process. L-09 is invalid because the referenced variables are meant to indicate storage slots that are used by the previous L1ERC20Bridge
implementation, as the L1ERC20Bridge
represents an upgrade.