An attacker by applying reentrancy in the function _withdrawFromEscrow
can make an NFT (whether L1-native or L2-native) un-bridgeable to L1 permanently.
Alice buys an L1-native NFT from Bob, and it is supposed to be transferred to Alice on L2.
Bob, bridges this NFT from L1 to his own address on L2.
Then, Bob transfers this NFT from L2 to L1 to a malicious contract already deployed by Bob on L1.
During withdrawal on L1, when safeTransferFrom
is called, the malicious contract is called before the _escrow
is set to address(0)
.
Now, the malicious contract re-enters and deposits this NFT to be bridged from L1 to Alice on L2.
During the deposit, the NFT is escrowed on L1, and the mapping _escrow
is set to address of the malicious contract.
When reentrancy is finished, the withdrawal on L1 continues, and it sets the mapping _escrow
to address(0)
.
Then, on L2, Alice receives the NFT.
But, Alice can never bridge this NFT to L1 (although this NFT is native on L1). Because the mapping _escrow
on L1 is equal to address(0)
, so the protocol assumes that this NFT is not escrowed. So, if she tries to bridge it to L1, during withdrawal on L1, protocol tries to mint it (since _escrow = address(0)
), and it will revert, as the NFT is already minted on L1 and it is owned by the bridge on L1.
Please note that the scenario explained above is about an L1-native NFT but this attack can be applied to L2-native NFTs as well.
Suppose Bob (malicious user) owns an L1-native NFT on L1. He is going to give this NFT to Alice (honest user) on L2, and in return, Alice pays Bob. Note that Alice is not necessarily an EOA, it could be also like an NFT market place on L2. But, for simplicity, let's assume Alice is an EOA on L2.
Bob bridges this NFT from L1 to L2 to his own address on L2. So, after the withdrawal on L2, Bob owns this NFT on L2, while he has escrowed it on L1.
Then Bob, bridges this NFT from L2 to L1 to a malicious contract that is already deployed by Bob on L1. So, this NFT will be escrowed on L2.
During the withdrawal on L1, the malicious contract applies the following trick:
During withdrawal on L1, the function _withdrawFromEscrow
would be called.
https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/ethereum/src/Bridge.sol#L201
Since the NFT is already escrowed on L1, it will call safeTransferFrom
to transfer the NFT from the bridge to the malicious contract, and also it will call the function onERC721Received
on the malicious contract.
The malicious contract applies reentrancy. Note that reentrancy happens at the following line:
https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/ethereum/src/Escrow.sol#L79
Now that the control is with the malicious contract, it calls the function depositTokens
to bridge the NFT to Alice on L2.
During depositing, the NFT will be escrowed on L1, so we will have: _escrow[collection][id] = msg.sender;
where msg.sender
is the address of the malicious contract.
https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/ethereum/src/Escrow.sol#L49
After the depositing is finished, the control returns to the bridge contract.
It continues in the function _withdrawFromEscrow
, where at end of this function, it sets the mapping _escrow
as: _escrow[collection][id] = address(0x0);
https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/ethereum/src/Escrow.sol#L86
Now the transaction on L1 is finished.
On L2 side, the NFT will be transferred to Alice, and Alice pays in return to Bob (we do not care about the payment here, it is just to make the scenario understandable).
What is the state now?
Alice owns the NFT on L2.
Bob has received the payment in return of transferring the NFT to Alice.
_escrow[collection][id] = address(0x0)
on L1.
_escrow[collection][id] = address(0x0)
means that Alice is owning an L1-native NFT on L2 which is not escrowed on L1. In other words, if Alice bridges this NFT to L1, during withdrawal, it checks whether the NFT is escrowed or not.
https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/ethereum/src/Escrow.sol#L72
Since, escrow[collection][id] = address(0x0)
, it returns false
.
https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/ethereum/src/Escrow.sol#L99
Then the bridge tries to mint it.
https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/ethereum/src/Bridge.sol#L208
In the function _mint
, Since the NFT is already minted and is owned by the bridge, it does not allow to mint it again. So, it reverts.
A simple malicious contract can be implemented as follows:
Making an NFT impossible to be bridged.
Selling an L1-native NFT to an user on L2, so that the user can not bridge it back to L1 forever.
Reentrancy guard is recommended on two functions depositTokens
and withdrawTokens
, or using transferFrom
instead of safeTransferFrom
during withdrawing an ERC721 token.
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.