Era

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

L2 AssetRouter passes an invalid assetId to _withdrawSender causing failed withdrawals

Summary

In the L2 implementation of _ensureTokenRegisteredWithNTV, the function fails to return a valid
assetId after attempting to register a token. This oversight leads the withdrawToken function to receive an invalid assetId (bytes32(0)), disrupting the withdrawal flow. As withdrawToken calls withdrawSender with this invalid assetId, the lack of a valid identifier results in misrouted or incomplete transactions, failed interactions within _withdrawSender, and mismanaged token burns in the bridgeBurn and _bridgeBurnBridgedToken flows.

Vulnerability Details

The issue arises because the L2 version of _ensureTokenRegisteredWithNTV does not return an assetId,
contrary to the L1 function, which correctly retrieves and returns this value. This discrepancy means that
when withdrawToken calls _ensureTokenRegisteredWithNTV, it receives bytes32(0) as the assetId.
Consequently, downstream functions in the withdrawal process are fed an invalid assetId, disrupting the withdrawal and
bridging functions.

Correct L1 Function (Benchmark)
The L1 implementation of _ensureTokenRegisteredWithNTV ensures that a valid assetId is returned, as seen below:

function _ensureTokenRegisteredWithNTV(address _token) internal override returns (bytes32 assetId) {
assetId = nativeTokenVault.assetId(_token);
if (assetId != bytes32(0)) {
return assetId;
}
nativeTokenVault.ensureTokenIsRegistered(_token);
assetId = nativeTokenVault.assetId(_token);
}

L2 Function with the Vulnerability
In contrast, the L2 version of _ensureTokenRegisteredWithNTV does not return any assetId,
causing withdrawToken to receive an invalid identifier (bytes32(0)), which propagates through the withdrawal flow:

function _ensureTokenRegisteredWithNTV(address _token) internal override returns (bytes32 assetId) {
IL2NativeTokenVault nativeTokenVault = IL2NativeTokenVault(L2_NATIVE_TOKEN_VAULT_ADDR);
nativeTokenVault.ensureTokenIsRegistered(_token); // Registers token but does not return assetId
}

Impact

When _withdrawSender is called with an invalid assetId (bytes32(0)), it attempts to retrieve the asset handler
address using assetHandlerAddress[_assetId], which is likely uninitialized or incorrect due to the invalid
identifier. This results in potentially null interactions, failed transactions, or unintended contract actions,
as _withdrawSender interacts with incorrect or undefined addresses.

function _withdrawSender(
bytes32 _assetId,
bytes memory _assetData,
address _sender,
bool _alwaysNewMessageFormat
) internal returns (bytes32 txHash) {
address assetHandler = assetHandlerAddress[_assetId];// @audit assetId will be zero.
bytes memory _l1bridgeMintData = IAssetHandler(assetHandler).bridgeBurn({
_chainId: L1_CHAIN_ID,
_msgValue: 0,
_assetId: _assetId,
_originalCaller: _sender,
_data: _assetData
});
bytes memory message;
if (_alwaysNewMessageFormat || L2_LEGACY_SHARED_BRIDGE == address(0)) {
message = _getAssetRouterWithdrawMessage(_assetId, _l1bridgeMintData);
// slither-disable-next-line unused-return
txHash = L2ContractHelper.sendMessageToL1(message);
} else {
address l1Token = IBridgedStandardToken(
IL2NativeTokenVault(L2_NATIVE_TOKEN_VAULT_ADDR).tokenAddress(_assetId)
).originToken();
if (l1Token == address(0)) {
revert AssetIdNotSupported(_assetId);
}
(uint256 amount, address l1Receiver) = abi.decode(_assetData, (uint256, address));
message = _getSharedBridgeWithdrawMessage(l1Receiver, l1Token, amount);
txHash = IL2SharedBridgeLegacy(L2_LEGACY_SHARED_BRIDGE).sendMessageToL1(message);
}
emit WithdrawalInitiatedAssetRouter(L1_CHAIN_ID, _sender, _assetId, _assetData);
}

The bridgeBurn function, which is called within _withdrawSender, expects a valid assetId to differentiate
between native and bridged token burning. Since the originChainId[_assetId] will not match the current chain ID
due to the invalid assetId, bridgeBurn proceeds with _bridgeBurnBridgedToken.

In _bridgeBurnBridgedToken, the following issues arise:

The tokenAddress[_assetId] mapping fetches an incorrect or null address for bridgedToken due to the
invalid assetId.
Consequently, when IBridgedStandardToken(bridgedToken).bridgeBurn(_originalCaller, _amount) is called, it could
attempt to burn tokens at an incorrect address, leading to token mismanagement or reverts.

The function continues to rely on a valid assetId to set originChainId and retrieve metadata, both of which would
fail or produce incorrect results, causing incomplete or incorrect token handling.
If the _assetId used is invalid, _bridgeBurnBridgedToken emits an originChainId[_assetId] value of 0, leading to
a ZeroAddress revert, ultimately blocking the withdrawal process for users.

As _withdrawSender continues with erroneous data, the entire withdrawal attempt can revert,
leaving users unable to access their funds on L1. This results in funds being effectively locked on L2,
preventing users from completing their withdrawal due to the invalid assetId.

In summary, the lack of a valid assetId in the L2 _ensureTokenRegisteredWithNTV function leads to failed
contract interactions, mismanaged token burns, and incomplete withdrawals, directly impacting users
attempting to bridge tokens to L1.

Tools Used

Manual Review

Recommendations

To ensure withdrawToken and _withdrawSender operate with the correct assetId, the L2 _ensureTokenRegisteredWithNTV
function should be updated to retrieve and return the assetId as in the L1 implementation.

function _ensureTokenRegisteredWithNTV(address _token) internal override returns (bytes32 assetId) {
IL2NativeTokenVault nativeTokenVault = IL2NativeTokenVault(L2_NATIVE_TOKEN_VAULT_ADDR);
assetId = nativeTokenVault.assetId(_token); // Retrieve assetId if it exists
if (assetId == bytes32(0)) {
nativeTokenVault.ensureTokenIsRegistered(_token); // Register if not registered
assetId = nativeTokenVault.assetId(_token); // Retrieve new assetId after registration
}
return assetId; // Return valid assetId
}

With this correction, withdrawToken will consistently receive a valid assetId, ensuring reliable token
handling within _withdrawSender and complete, accurate execution of user withdrawals.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

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

Appeal created

inallhonesty Lead Judge
6 months ago
inallhonesty Lead Judge 6 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.