An attack can be performed on the L1 bridge, such that an NFT gets stuck on the L1 bridge, but is withdrawable and useable on L2. When this NFT is bridged back to L1, it won't be withdrawable or useable on L1.
On the L1 bridge, if a NFT bridge request fails on L2, the bridge owner can start the cancellation of the request using the startRequestCancellation()
function in Bridge.sol
:
After the cancellation is started, anyone can call cancelRequest()
function in Bridge.sol
after a certain period to finalize the request cancellation :
This function cancels the message on the starknetCore
contract and sends all the escrowed NFTs to the owner of the NFTs using the _cancelRequest()
function, which subsequently calls the _withdrawFromEscrow()
function of Escrow.sol
.
As you can see, in the _withdrawFromEscrow()
function, the IERC721(collection).safeTransferFrom(from, to, id)
function is used to transfer the NFTs back to the owner.
The safeTransferFrom()
function, makes an external call to the to
address, if it is a contract. This makes it possible for the to
contract to execute some arbitrary logic before the transaction is completed.
Consider the following scenario :
A NFT owner creates a deposit request for 1 NFT, and purposely sets the parameters such that the withdrawal fails on L2, for eg : setting ownerL2
to an invalid starknet address.
The bridge request fails on L2, and L1 bridge owner calls startRequestCancellation()
on L1 bridge.
After the wait time passes, the NFT owner calls cancelRequest()
on L1 bridge, which eventually calls safeTransferFrom()
function and the onERC721Received()
function of the NFT owner contract is called.
In the onERC721Received()
, the NFT owner sets the logic so that it calls the depositTokens()
function of Bridge.sol
and deposits the NFT that it just received to the escrow, therefore creating an L1 to L2 bridge request for the NFT. This sets _escrow[collection][tokenId]
to a non-zero value :
However, when the execution continues in _withdrawFromEscrow()
after the onERC721Received()
function executes, the _escrow[collection][tokenId]
is set to zero.
This means that the contract state reflects that the NFT is not escrowed in L1 bridge, however, the deposit request has been created, and the NFT owner can successfully withdraw the NFT on L2 now.
Now, when a user tries to bridge this NFT from L2 to L1, the L2 bridge will successfully execute the request. However, withdrawTokens()
on the L1 bridge will revert as isEscrowed
will be false. There isn't any way to withdraw the tokens on either L1 or L2 after this, and the NFTs are forever lost.
High - The user can freely use the NFT on L2, and even sell it to another user on L2. When the new owner tries to bridge the NFT back to L1, the bridge request will fail and the NFT will get stuck forever in both L2 AND L1, along with any other tokens that were a part of the L2 to L1 bridge request.
To run the test, place the Attack
contract in a new file and import it in Bridge.t.sol
. After this, copy the test_POC()
snippet in Bridge.t.sol
.
Note: the isEscrowed()
function in Escrow.sol
has been changed to public for this test. This has no effect except that we can check if the NFT is escrowed in the contract or not.
Run forge test --match-test "test_POC"
in the command line.
Foundry
Use Re-entrancy guard
Impact: - NFT already bridged won’t be bridgeable anymore without being stuck. Likelyhood: Low. - Attackers will corrupt their own tokens, deploying a risky contract interacting with an upgradable proxy. They have to buy and sell them without real benefits, except being mean. Some really specific and rare scenario can also trigger that bug.
Impact: - NFT already bridged won’t be bridgeable anymore without being stuck. Likelyhood: Low. - Attackers will corrupt their own tokens, deploying a risky contract interacting with an upgradable proxy. They have to buy and sell them without real benefits, except being mean. Some really specific and rare scenario can also trigger that bug.
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.