zkSync v2 contest - Tomo's results

Rely on math, not validators.

General Information

Platform: Code4rena

Start Date: 28/10/2022

Pot Size: $165,500 USDC

Total HM: 2

Participants: 24

Period: 12 days

Judge: GalloDaSballo

Total Solo HM: 1

Id: 177

League: ETH

zkSync

Findings Distribution

Researcher Performance

Rank: 8/24

Findings: 1

Award: $2,102.32

QA:
grade-a

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: HE1M

Also found by: 0xSmartContract, Rolezn, Tomo, brgltd, cccz, chaduke, ctf_sec, datapunk, jayjonah8, ladboy233, pashov, rbserver

Labels

bug
QA (Quality Assurance)
grade-a
Q-05

Awards

2102.3184 USDC - $2,102.32

External Links

QA Report

L-1: should check the address(0) L1ERC20Bridge#deposit

In the zkSync, the address of ETH is set like this.

And L1EthBridge#deposit checks the tokenAddress is CONVENTIONAL_ETH_ADDRESS

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1EthBridge.sol#L32-L33

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1EthBridge.sol#L88-L93

// L1EthBridge.sol

/// @dev Ether native coin has no real address on L1, so a conventional zero address is used.
address constant CONVENTIONAL_ETH_ADDRESS = address(0);

function deposit(
        address _l2Receiver,
        address _l1Token,//address(0)
        uint256 _amount
    ) external payable nonReentrant senderCanCallFunction(allowList) returns (bytes32 txHash) {
        require(_l1Token == CONVENTIONAL_ETH_ADDRESS, "bx");
				/* ~~~ */

The L1ERC20Bridge#deposit has no checks the tokenAddress is not CONVENTIONAL_ETH_ADDRESS https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1ERC20Bridge.sol#L111-L117

// L1ERC20Bridge.sol
function deposit(
      address _l2Receiver,
      address _l1Token,
      uint256 _amount
  ) external payable nonReentrant senderCanCallFunction(allowList) returns (bytes32 txHash) {
			// @audit-info _depositFunds has safeTransferFrom() by OpenZppelin
      uint256 amount = _depositFunds(msg.sender, IERC20(_l1Token), _amount);
      require(amount > 0, "1T"); // empty deposit amount

The safeTransferFrom() by OpenZeppelin will return revert in this case. However, you should prevent this issue like this.

// L1ERC20Bridge.sol
function deposit(
      address _l2Receiver,
      address _l1Token,
      uint256 _amount
  ) external payable nonReentrant senderCanCallFunction(allowList) returns (bytes32 txHash) {
      require(_l1Token != CONVENTIONAL_ETH_ADDRESS, "bx");
      uint256 amount = _depositFunds(msg.sender, IERC20(_l1Token), _amount);
      require(amount > 0, "1T"); // empty deposit amount

L-2: Can be overflow

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/common/libraries/UncheckedMath.sol

library UncheckedMath {
    function uncheckedInc(uint256 _number) internal pure returns (uint256) {
        unchecked {
            return _number + 1;
        }
    }

    function uncheckedAdd(uint256 _lhs, uint256 _rhs) internal pure returns (uint256) {
        unchecked {
            return _lhs + _rhs;
        }
    }
}

The unchecked keyword can set the default overflow and underflow checks to off. This is used to optimize the gas cost but there are no overflow checks so, it happens to overflow silently.

Therefore, you should add checking to prevent overflow or add a comment.

e.g) β€œBe sure to check overflow before using this library”

L-3: require() should be used instead of assert()

πŸ“ Description

Prior to solidity version 0.8.0, hitting an assert consumes the remainder of the transaction's available gas rather than returning it, as require()/revert() do. assert() should be avoided even past solidity version 0.8.0 as its documentation states that The assert function creates an error of type Panic(uint256). ... Properly functioning code should never create a Panic, not even on invalid external input. If this happens, then there is a bug in your contract which you should fix

πŸ’‘ Recommendation

You should change from assert() to require()

πŸ” Findings:

2022-10-zksync/blob/main/ethereum/contracts/zksync/facets/DiamondCut.sol#L16 assert(SECURITY_COUNCIL_APPROVALS_FOR_EMERGENCY_UPGRADE > 0);

L-4: should add checking return value by readAddress()

L1EthBridge.sol

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1EthBridge.sol#L214-L228

L1ERC20Bridge.sol

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1ERC20Bridge.sol#L225-L257

function _parseL2WithdrawalMessage(bytes memory _message)
    internal
    pure
    returns (address l1Receiver, uint256 amount)
{
		/* ~~~ */
    (l1Receiver, offset) = UnsafeBytes.readAddress(_message, offset);
    (amount, offset) = UnsafeBytes.readUint256(_message, offset);
}

L1EthBridge.sol

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1EthBridge.sol#L182-L211

L1ERC20Bridge.sol

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/bridge/L1ERC20Bridge.sol#L260-L279

function finalizeWithdrawal(
        uint256 _l2BlockNumber,
        uint256 _l2MessageIndex,
        uint16 _l2TxNumberInBlock,
        bytes calldata _message,
        bytes32[] calldata _merkleProof
    ) external override nonReentrant senderCanCallFunction(allowList) {
				/* ~~~ */
        (address l1Receiver, uint256 amount) = _parseL2WithdrawalMessage(_message);
				/* ~~~ */
        _withdrawFunds(l1Receiver, amount);

        emit WithdrawalFinalized(l1Receiver, CONVENTIONAL_ETH_ADDRESS, amount);
    }

πŸ“ Description

If the return value of _parseL2WithdrawalMessage() is as follows, the amount can’t withdraw forever

(address l1Receiver, uint256 amount) = (address(0), 100000)

Therefore, you should add checking to prevent this case

πŸ’‘ Recommendation

L1EthBridge.sol

(address l1Receiver, uint256 amount) = _parseL2WithdrawalMessage(_message);
require(l1Receiver != address(0) && amount != 0);

L1ERC20Bridge.sol

(address l1Receiver, address l1Token, uint256 amount) = _parseL2WithdrawalMessage(l2ToL1Message.data);
require(l1Receiver != address(0) && ll1Token != address(0) && amount != 0);

N-1: Open Todos

πŸ“ Description

Code architecture, incentives, and error handling/reporting questions/issues should be resolved before deployment

πŸ’‘ Recommendation

Delete TODO keyword

πŸ” Findings:

2022-10-zksync/blob/main/ethereum/contracts/zksync/Config.sol#L28 // TODO: change constant to the real root hash of empty Merkle tree (SMA-184)

2022-10-zksync/blob/main/ethereum/contracts/zksync/Plonk4VerifierWithAccessToDNext.sol#L43 PairingsBn254.Fr[1] state_polys_openings_at_z_omega; // TODO: not use array while there is only D_next

2022-10-zksync/blob/main/ethereum/contracts/zksync/Plonk4VerifierWithAccessToDNext.sol#L224 require(proof.state_polys_openings_at_z_omega.length == 1); // TODO

2022-10-zksync/blob/main/ethereum/contracts/zksync/Plonk4VerifierWithAccessToDNext.sol#L304 // require(vk.num_inputs > 0); // TODO

2022-10-zksync/blob/main/ethereum/contracts/zksync/Plonk4VerifierWithAccessToDNext.sol#L308 // TODO we may use batched lagrange compputation

2022-10-zksync/blob/main/ethereum/contracts/zksync/Plonk4VerifierWithAccessToDNext.sol#L454 t.mul_assign(vk.non_residues[i - 1]); // TODO add one into non-residues during codegen?

2022-10-zksync/blob/main/ethereum/contracts/zksync/Plonk4VerifierWithAccessToDNext.sol#L485 // TODO

2022-10-zksync/blob/main/ethereum/contracts/zksync/Verifier.sol#L132 // require(serialized_proof.length == 44); TODO

2022-10-zksync/blob/main/ethereum/contracts/zksync/facets/Mailbox.sol#L94 // TODO: estimate gas for L1 execute

2022-10-zksync/blob/main/ethereum/contracts/zksync/facets/Mailbox.sol#L127 // TODO: Restore after stable priority op fee modeling. (SMA-1230)

2022-10-zksync/blob/main/ethereum/contracts/zksync/facets/Mailbox.sol#L169 layer2Tip: uint192(0) // TODO: Restore after fee modeling will be stable. (SMA-1230)

2022-10-zksync/blob/main/ethereum/contracts/zksync/interfaces/IExecutor.sol#L56 /// TODO: The verifier integration is not finished yet, change the structure for compatibility later

2022-10-zksync/blob/main/ethereum/contracts/zksync/libraries/PairingsBn254.sol#L208 // TODO

N-2: No use of two-phase ownership transfers

πŸ“ Description

Consider adding a two-phase transfer, where the current owner nominates the next owner, and the next owner has to call accept*() to become the new owner. This prevents passing the ownership to an account that is unable to use it.

πŸ’‘ Recommendation

Consider implementing a two step process where the admin nominates an account and the nominated account needs to call an acceptOwnership() function for the transfer of admin to fully succeed. This ensures the nominated EOA account is a valid and active account.

πŸ” Findings:

2022-10-zksync/blob/main/ethereum/contracts/common/AllowList.sol#L34 owner = _owner;

2022-10-zksync/blob/main/ethereum/contracts/common/AllowList.sol#L158 owner = newOwner;

N-3: Use string.concat() orbytes.concat()

πŸ“ Description

Solidity version 0.8.4 introduces bytes.concat() (vs abi.encodePacked(<bytes>,<bytes>))Solidity version 0.8.12 introduces string.concat()(vs abi.encodePacked(<str>,<str>))

πŸ’‘ Recommendation

Use concat instead of abi.encodePacked

πŸ” Findings:

2022-10-zksync/blob/main/ethereum/contracts/common/libraries/UnsafeBytes.sol#L7 * @dev The library provides a set of functions that help read data from an "abi.encodePacked" byte array.

2022-10-zksync/blob/main/ethereum/contracts/zksync/facets/Executor.sol#L283 abi.encodePacked(

2022-10-zksync/blob/main/ethereum/contracts/zksync/facets/Executor.sol#L364 abi.encodePacked(

2022-10-zksync/blob/main/ethereum/contracts/zksync/facets/Executor.sol#L373 return abi.encodePacked(s.zkPorterIsAvailable, s.l2BootloaderBytecodeHash, s.l2DefaultAccountBytecodeHash);

2022-10-zksync/blob/main/ethereum/contracts/zksync/facets/Mailbox.sol#L59 abi.encodePacked(_log.l2ShardId, _log.isService, _log.txNumberInBlock, _log.sender, _log.key, _log.value)

2022-10-zksync/blob/main/ethereum/contracts/zksync/libraries/TranscriptLib.sol#L29 self.state_0 = keccak256(abi.encodePacked(DST_0, old_state_0, self.state_1, value));

2022-10-zksync/blob/main/ethereum/contracts/zksync/libraries/TranscriptLib.sol#L30 self.state_1 = keccak256(abi.encodePacked(DST_1, old_state_0, self.state_1, value));

2022-10-zksync/blob/main/ethereum/contracts/zksync/libraries/TranscriptLib.sol#L43 bytes32 query = keccak256(abi.encodePacked(DST_CHALLENGE, self.state_0, self.state_1, self.challenge_counter));

2022-10-zksync/blob/main/zksync/contracts/bridge/L2ERC20Bridge.sol#L109 return abi.encodePacked(IL1Bridge.finalizeWithdrawal.selector, _to, _l1Token, _amount);

2022-10-zksync/blob/main/zksync/contracts/bridge/L2ETHBridge.sol#L75 return abi.encodePacked(IL1Bridge.finalizeWithdrawal.selector, _to, _amount);

N-4: Empty Revert

If returns revert, you should add comment to makes failed points clear

https://github.com/code-423n4/2022-10-zksync/blob/main/zksync/contracts/bridge/L2StandardERC20.sol#L116

https://github.com/code-423n4/2022-10-zksync/blob/main/zksync/contracts/bridge/L2StandardERC20.sol#L122

https://github.com/code-423n4/2022-10-zksync/blob/main/zksync/contracts/bridge/L2StandardERC20.sol#L128

N-5: 404 Links

https://github.com/code-423n4/2022-10-zksync/blob/main/ethereum/contracts/common/ReentrancyGuard.sol#L19 This is invalid link https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]

This is valid link https://blog.openzeppelin.com/reentrancy-after-istanbul/

You should change as follows

// before
// * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].

// after
// * https://blog.openzeppelin.com/reentrancy-after-istanbul/  [Reentrancy After Istanbul].

#0 - miladpiri

2022-11-23T14:31:50Z

IMO, should be considered as high quality report!

#1 - GalloDaSballo

2022-11-24T01:44:38Z

L-1: should check the address(0) L1ERC20Bridge#deposit

L

##Β L-2: Can be overflow L

L-3: require() should be used instead of assert()

Disputing as I think it's ok to check that the developer set the value and it will never run in prod

L-4: should add checking return value by readAddress()

Check for address(0) already awarded in L-1

N-1: Open Todos

NC

##Β N-2: No use of two-phase ownership transfers NC

N-3: Use string.concat() orbytes.concat()

NC

##Β N-4: Empty Revert NC

N-5: 404 Links

NC

2L 5NC

#2 - c4-judge

2022-12-03T19:08:42Z

GalloDaSballo marked the issue as grade-a

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter