NFTBridge
60,000 USDC
View results
Submission Details
Severity: high
Valid

Collection address generated for L2 native token in Bridge.sol is not synced with bridge.cairo

Summary

When we bridge an L2 native token to L1 for the first time, collectionL1 would be address(0x0), and a new address will be created by _deployERC721Bridgeable() for that. However, bridge.cairo is not synced with L1 regarding this new generated address. As a result, when the same collection is bridged back from L1 to L2, verify_collection_address() fails, causing tokens to become non-transferable to L2.

Vulnerability Details

For bridging a native L2 token from L2 to L1, we need to call deposit_tokens() function in bridge.cairo. If this is the first time that we are doing so, the self.l2_to_l1_addresses.read(collection_l2) returns zero for the given collection_l2 address.

let collection_l1 = self.l2_to_l1_addresses.read(collection_l2);

After sending the request to L1, we need to withdraw it at L1 by calling withdrawTokens() in Bridge.sol. withdrawTokens() calls _verifyRequestAddresses() with the provided collectionL1 and collectionL2 addresses as arguments. Since L2 address is present in the request and L1 address is not, the following code will be executed:

// L2 address is present in the request and L1 address is not.
if (l2Req > 0 && l1Req == address(0)) {
if (l1Mapping == address(0)) {
// It's the first token of the collection to be bridged.
return address(0);

As a result, a new address will be generated for the collection at L1 by executing the following code at withdrawTokens() function:

if (collectionL1 == address(0x0)) {
if (ctype == CollectionType.ERC721) {
collectionL1 = _deployERC721Bridgeable(
req.name,
req.symbol,
req.collectionL2,
req.hash
);
// update whitelist if needed
_whiteListCollection(collectionL1, true);

The new generated address will be mapped to L2 address in _l1ToL2Addresses and _l2ToL1Addresses:

address proxy = Deployer.deployERC721Bridgeable(name, symbol);
_l1ToL2Addresses[proxy] = collectionL2;
_l2ToL1Addresses[collectionL2] = proxy;

At the end, tokens will be minted to ownerL1.

The next time, that we want to bridge back the same collection from L1 to L2, we need to call depositTokens() in Bridge.sol. Since we already mapped two collectionL1 and collectionL2 addresses in _l1ToL2Addresses, we can send the request to L2 with correct addresses.

For receving tokens at L2 side, the function withdraw_auto_from_l1() will be called. withdraw_auto_from_l1() will call ensure_erc721_deployment() and then verify_collection_address() will be called. L1 and L2 addresses both are present in the request, but since we have not updated l2_to_l1_addresses and l1_to_l2_addresses at L2 side with the new generated address for the collection at L1, the following code will be executed and panics because of l2_bridge != l2_req:

else {
// L1 address is present, and L2 address too.
if l2_bridge != l2_req {
panic!("Invalid collection L2 address");
}

Impact

It is not possible to bridge back a native L2 token when it is bridged to L1.

Tools Used

Manual review.

Recommendations

Contract states at L1 and L2 sides need to be synced (e.g., with a message from L1 to L2 regarding new generated collection address at L1).

Updates

Lead Judging Commences

n0kto Lead Judge 12 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-first-bridgeof-a-collection-L1<->L2-do-not-sync-addresses

Likelyhood: High, any collections bridged, without bridge owner action, will be unable to bridge back. Impact: High, L2 -> L1 tokens will be stuck in the bridge. L1 -> L2 will need to ask for a cancellation.

Support

FAQs

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