If users use the use_withdraw_auto
feature when bridging from L2 to L1, their NFTs will be stuck in the L1 bridge.
On L1, any requests using the use_withdraw_auto
feature cannot be withdrawn as it has been disabled after the previous audit.
The problem is, that the L2 side of the bridge currently still allows deposit
requests with that flag set to true.
This means that a user can send a deposit with use_withdraw_auto=true
which will be successful on L2 but any withdrawals on L1 will fail.
The impact of this is permanent locking of user NFTs in the L1 bridge in some scenarios.
Let's dive into why it is permanent:
So there are two scenarios:
The bridged NFT comes originally from L2 and has not yet been minted on L1
The bridged NFT origins on L1 and therefore is escrowed in the L1 bridge
In scenario 1)
, the NFT could be retrieved by an admin as it would probably be possible to mint the NFT on L1 as it was not yet minted.
In scenarion 2)
however (which is the case for the Everai
collection as it origins from L1) the NFT would be permanently locked in the bridge.
This is because as it was already minted it is not possible to mint it again. And since the only possibilities to get NFTs from the bridge other than minting are with withdrawTokens
, which will revert, and cancelRequest
, which can only be called for a pending L1 -> L2
bridge. Therefore not even an admin can retrieve the NFT.
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.
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.
Now to show this vulnerability, please do the following:
Add the following content to a new file poc-WithdrawAuto.sh
:
Then execute it with source ./poc-WithdrawAuto.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 30 for bridging
Now that everything is set up, we can finally show the bug by doing the following:
We then bridge the NFT with the use_withdraw_auto
flag set to 1
which is allowed on L2: starkli_user invoke --watch ${BRIDGE_L2_ADDR} deposit_tokens $(date +%s%N) ${ERC721_L2_ADDR} ${ETH_ACCOUNT} 1 u256:30 1 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: [0x1000101,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>"
This will revert with the selector of NotSupportedYetError
locking the NFT in the bridge
Manual review
I would recommend disallowing use_withdraw_auto
withdrawals on L2 until they are enabled again on L1. Just pass false
when building the request header, preventing this issue.
Impact: High, token will be stuck in L2 bridge. Likelyhood: Very low, option is available in L2 but has been disabled since March on L1, would be almost a user error.
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.