Era

ZKsync
FoundryLayer 2
500,000 USDC
View results
Submission Details
Severity: medium
Valid

Mismatch in Encoding Format for `assetId` When Setting and Reading `assetHandlerAddress`

Summary

A mismatch in encoding formats for assetId occurs when setting the assetHandlerAddress in L2AssetRouter::_setAssetHandlerAddressThisChain and reading it during withdrawal operations in L2AssetRouter. The inconsistency prevents successful lookups of the assetHandlerAddress mapping, causing withdrawals to revert and leading to poor user experience and potential financial losses.

Vulnerability Details

Setting the assetHandlerAddress

When L2AssetRouter::setAssetHandlerAddressThisChain is called, it updates the assetHandlerAddress mapping via the AssetRouterBase::_setAssetHandlerAddressThisChain function. The assetId is calculated using DataEncoding::encodeAssetId, which encodes the chain ID, sender address, and asset data as follows:

function encodeAssetId(uint256 _chainId, bytes32 _assetData, address _sender) internal pure returns (bytes32) {
return keccak256(abi.encode(_chainId, _sender, _assetData));
}

Code Reference

_setAssetHandlerAddressThisChain:

function _setAssetHandlerAddressThisChain(
address _nativeTokenVault,
bytes32 _assetRegistrationData,
address _assetHandlerAddress
) internal {
bool senderIsNTV = msg.sender == address(_nativeTokenVault);
address sender = senderIsNTV ? L2_NATIVE_TOKEN_VAULT_ADDR : msg.sender;
bytes32 assetId = DataEncoding.encodeAssetId(block.chainid, _assetRegistrationData, sender);
if (!senderIsNTV && msg.sender != assetDeploymentTracker[assetId]) {
revert Unauthorized(msg.sender);
}
assetHandlerAddress[assetId] = _assetHandlerAddress;
assetDeploymentTracker[assetId] = msg.sender;
emit AssetHandlerRegisteredInitial(assetId, _assetHandlerAddress, _assetRegistrationData, sender);
}

Note that, this is the primary point of update for assetHandlerAddress mapping in L2AssetRouter.
The other points of update are:

Reading the assetHandlerAddress During Withdrawals

When L2AssetRouter::withdrawToken or legacy withdrawal functions are invoked, a different encoding method is used to calculate the assetId. For example, the _withdrawLegacy function generates the assetId using DataEncoding::encodeNTVAssetId:

function encodeNTVAssetId(uint256 _chainId, bytes32 _assetData) internal pure returns (bytes32) {
return keccak256(abi.encode(_chainId, L2_NATIVE_TOKEN_VAULT_ADDR, _assetData));
}

Inconsistency in Encoding Formats

  • Setting: encodeAssetId includes the sender address in the encoding.

  • Reading: encodeNTVAssetId uses a hardcoded L2_NATIVE_TOKEN_VAULT_ADDR

The mismatch ensures that the assetId generated during withdrawal will never match the one used to set the assetHandlerAddress, leading to failed lookups.

Impact

The causes users withdrawal transactions to consistently revert until an admin intercedes to resolve the issue through setAssetHandlerAddress which is called from the L1. Not only is this a poor user experience, it's inefficient, and obviously unfeasable. Admins can't step in everytime a user wants to withdraw their tokens from the L2. This could also lead to financial loss for users.

Tools Used

Manual code review.

Recommendations

To address the issue, the same encoding function should be used across all operations involving assetId. This ensures consistent behavior when setting and reading the assetHandlerAddress.

Updates

Lead Judging Commences

inallhonesty Lead Judge
6 months ago
inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

krisrenzo Submitter
5 months ago
inallhonesty Lead Judge
5 months ago
inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`L2AssetRouter._ensureTokenRegisteredWithNTV` `assetId` return value is never assigned, which will cause `withdrawToken` to fail

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.