If a user tries to bridge a L2-native NFT to L1 and it does not use a baseURI
on L1, the NFT's metadata is not reflected on L1 when minting causing the NFT's metadata to be missing/lost.
Currently when bridgeing from L1 to L2 there are two options, either the NFT is minted on L2 or it gets transferred from the bridge (if it has already been bridged L2->L1 before).
Now if the NFT gets minted and it has a token_uri
associated with it, that data gets properly transmitted from L1 to L2, causing the NFT to be minted with mint_from_bridge_uri
, ensuring that the NFT's uri
gets preserved.
This is important as the uri
contains information about a specific NFT like for example the image associated with it.
The problem is, that there is no such mechanism when bridging from L2 to L1. If an NFT gets minted by the bridge on L1, it gets minted with the function mintFromBridge
which does not set a uri
.
Now there is a TODO vaguely pointing out that this might be needed in the future, but it is a definite issue right now.
This causes NFTs bridged from L2 to L1 to be minted without metadata-uri. Since the metadata contains crucial information about an NFT, this is a major problem for users.
Local testnet setup needed for PoC:
The following script adds all needed files for my PoC execution on a local testnet.
NOTE: this only needs to be executed ONCE for all my testnet-related PoCs to work! Also please only execute fullSetup.sh
from the contest-directory apps/blockchain/
!
Go to apps/blockchain/
Go to ethereum
and execute forge test
once
Go to ../starknet
and execute snforge test
once
Go back to ../
(should be back in apps/blockchain/
)
Create file fullSetup.sh
and add the script to it
Execute with sh fullSetup.sh
Open a new terminal and execute anvil
here you will see all the logs of the L1
Open a new terminal, go to apps/blockchain/starknet/
and execute katana --messaging ./data/anvil.messaging.json --seed 0
, here you will see all the logs of L2
Go to the original terminal (apps/blockchain/
)
From here on, please follow the instructions of the specific PoC provided after this
It creates the following files needed to execute individual PoCs:
General files
foundry.toml
.env
Scripts (they get made executable with chmod +x <SCRIPTS>
):
deploy.sh
adjustL1Bridge.sh
setMappingL1.sh
setMappingL2.sh
deployNFTonL1.sh
deployNFTonL2.sh
enableBridgeL1.sh
enableBridgeL2.sh
deployMetadataNFTonL1.sh
sourceAllMetadata.sh
Solidity files:
./ethereum/src/token/ERC721BridgeableMetadata.sol
./ethereum/src/token/IERC721BridgeableMetadata.sol
./ethereum/script/ERC721Metadata.s.sol
Additionally it creates a ./ethereum/logs
dir and creates the files ./ethereum/logs/local_messaging_deploy.json
and ./ethereum/logs/starklane_deploy.json
, which was needed for me to get script execution working.
At this point you should have three running terminals. One executing anvil
, one executing katana ...
and a third one allowing us to interact with the testnet.
To show that bridging an NFT with a URI from L2 to L1 causes the URI to be lost on L1, please now do the following:
Add the following content to a new file poc-LostMetadata.sh
:
Then execute it with source ./poc-LostMetadata.sh
This does the following:
set up the bridge (L1 and L2)
deploy NFTs on L1 and L2
set up L1 <-> L2 mappings on both chains
mints NFTs to user on L2
approves NFT 1 for bridging
Now that everything is set up, we can finally show the bug by doing the following:
We get the uri
of NFT 1 on L2 by executing: starkli_user call ${ERC721_L2_ADDR} token_uri u256:1
this shows that the URI of our NFT is URI1
(encoded)
We bridge NFT 1 to L1: starkli_user invoke --watch ${BRIDGE_L2_ADDR} deposit_tokens 10 ${ERC721_L2_ADDR} ${ETHEREUM_USER_ADDR} 1 u256:1 0 0
In order to get the message payload we need to withdraw on L1, do the following:
Get the transaction hash which was printed in the console. For example: Transaction 0x030aa979a12028a2125ea88eda5ad98a0d3decc991d4d55ef28e984689da3000 confirmed
-> 0x030aa979a12028a2125ea88eda5ad98a0d3decc991d4d55ef28e984689da3000
Execute starkli_user tx-receipt 0x030aa979a12028a2125ea88eda5ad98a0d3decc991d4d55ef28e984689da3000 | jq '.messages_sent[0].payload'
with your transaction hash
Take the result and format it into the following format: "[0x1,0x2,0x3,0x4,0x5,...]"
It should look similar to this: [0x101,0x12da98742ed634a4e595621efa974a81,0x384bca09b8087bb0952c044578fae999,0xa513e6e4b8f2a923d98304ec87f64353c4d5c853,0x1598c82cc5694894da10861fa3a509b62d91b4ab68740169d5f2c9d1c23c3ab,0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266,0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03,0x0,0x636f6c6c656374696f6e5f74657374,0xf,0x0,0x4354455354,0x5,0x0,0x0,0x0,0x1,0x1e,0x0,0x0,0x1,0x0,0x5552493330,0x5,0x0]
Now to withdraw on L2, execute the following: cast_user1_send ${BRIDGE_L1_ADDR} "withdrawTokens(uint256[])" "<YOUR_REQ>"
We now check the uri
on L1 by calling: cast call --private-key ${ETHEREUM_USER_PRIVATE_KEY} ${ERC721_L1_ADDR} "tokenURI(uint256)" 1
This will return something along the lines of 0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000
showing that the NFT's uri
is not set on L1!
Manual review
In order to fix this, one could either implement the mintFromBridgeUri
function in ERC721Bridgeable
and call that if the request contains token uris, or add some other functionality setting the NFT's uri in that case.
URI is not lost on the origin chain and it can be modified with `ERC721UriImpl`. As explained in the TODO below, that’s a design choice and it will be implemented as a future feature. https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/ethereum/src/Bridge.sol#L206 `ERC721Bridgable` is out of scope.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.