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

Asymmetric Mapping Updates Prevent NFT Bridging Back to Original chain

Summary

The vulnerability arise for both bridging ways, but for simplicity only the L1 -> L2 will be explain in this report.

When a new NFT collection is bridged from Ethereum (L1) to Starknet (L2), the L2 contract correctly updates its address mappings, but the L1 contract fails to do so. This asymmetry in mapping updates creates a critical issue that prevents users from bridging their NFTs back to Ethereum, effectively locking assets on the L2 side.

Vulnerability Details

The core of this vulnerability lies in the asymmetric update of collection address mappings between L1 and L2. Let's examine the process step by step:

  1. When a new collection is bridged from Ethereum to Starknet, and no corresponding L2 collection exists, the Starknet bridge contract deploys a new collection and updates its mappings:

// In bridge.cairo
fn ensure_erc721_deployment(ref self: ContractState, req: @Request) -> ContractAddress {
// ... (deployment logic)
self.l1_to_l2_addresses.write(l1_req, l2_addr_from_deploy);
self.l2_to_l1_addresses.write(l2_addr_from_deploy, l1_req);
// ...
}
  1. However, there's no corresponding update on the Ethereum side. The L1 contract's mappings remain unchanged. the _l2ToL1Addresses is never updated and will return address(0x00).

function depositTokens(
uint256 salt,
address collectionL1,
snaddress ownerL2,
uint256[] calldata ids,
bool useAutoBurn
)
external
payable
{
...
// several tx may have the exact same block.
req.collectionL1 = collectionL1;
req.collectionL2 = _l1ToL2Addresses[collectionL1];
BUG --> // the _l1ToL2Addresses is never updated for newly created collection on L2
...
}
  1. When a user attempts to bridge an NFT back to Ethereum, the _verifyRequestAddresses function in CollectionManager.sol performs a check that will fail due to the mismatched mappings.

The l1Req will be > address(0), but the l1Mapping = _l2ToL1Addresses[collectionL2Req]; will return 0, thus it will revert : if (l1Mapping != l1Req) {revert InvalidCollectionL1Address();}

// In CollectionManager.sol
function _verifyRequestAddresses(
address collectionL1Req,
snaddress collectionL2Req
)
internal
view
returns (address)
{
address l1Req = collectionL1Req;
uint256 l2Req = snaddress.unwrap(collectionL2Req);
address l1Mapping = _l2ToL1Addresses[collectionL2Req];
uint256 l2Mapping = snaddress.unwrap(_l1ToL2Addresses[l1Req]);
// L2 address is present in the request and L1 address is not.
if (l2Req > 0 && l1Req == address(0)) {
...
}
// L2 address is present, and L1 address too.
if (l2Req > 0 && l1Req > address(0)) {
BUG --> if (l1Mapping != l1Req) {
revert InvalidCollectionL1Address();
}
...
}
...
}

Impact

The impact of this vulnerability is severe:

  1. Asset Lock: Users who bridge their NFTs from a newly deployed collection on L2 to L1 will find their assets locked on the L2 side, unable to complete the bridging process.

  2. Loss of Functionality: Users lose the ability to utilize their NFTs on the original Ethereum chain, potentially missing out on important ecosystem functionalities or economic opportunities.

  3. Trust Issues: This problem could lead to a loss of trust in the bridge system, potentially affecting the adoption and usage of the entire bridging ecosystem.

Root Cause

The root cause of this issue is the lack of a mechanism to update the L1 contract's mappings when a new collection is deployed on L2. The bridge protocol assumes symmetrical information on both sides, but fails to ensure this symmetry is maintained throughout the bridging process.

Recommendations

  1. Implement L2 to L1 Mapping Update:
    Develop a mechanism to update the L1 contract's mappings when a new collection is deployed on L2. This could be achieved through a message from L2 to L1 after successful deployment.

    // In bridge.cairo, after deploying new collection
    let message = array![
    MAPPING_UPDATE_SELECTOR,
    l1_collection_address,
    l2_collection_address
    ];
    starknet::send_message_to_l1_syscall(l1_bridge_address, message.span());

    On the L1 side, implement a function to handle this message and update the mappings:

    // In Bridge.sol
    function updateL2CollectionMapping(address l1Collection, uint256 l2Collection) external {
    require(msg.sender == address(this), "Only self-call allowed");
    _l1ToL2Addresses[l1Collection] = snaddress.wrap(l2Collection);
    _l2ToL1Addresses[snaddress.wrap(l2Collection)] = l1Collection;
    }

By implementing these recommendations, the bridge protocol can ensure consistent mappings across both chains, allowing for seamless bidirectional NFT transfers and maintaining the integrity of the bridging ecosystem.

Updates

Lead Judging Commences

n0kto Lead Judge 10 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.