A malicious actor can stuck other unsuspecting users' tokens forever.
The problem stems from the fact that there's a reentrancy vulnerability in the Escrow::_withdrawFromEscrow() function that can be used on any collection to trap unsuspecting users to lock their tokens permanently if they use the bridge.
The attack can be performed on any collection so let's take Everai for example.
Let's say that I've bridged my NFT with tokenId from L1 -> L2 and vice versa. Currently, the token is sitting in the L1 escrow and I have it on L2.
Now here's how the attack would go.
I bridge my token from L2 -> L1 where I specify the owner_l1 address i.e the to address that will receive the NFT on L1 to be an address to a contract I control with the following functionality:
The code above illustrates the basic functionality of the attacker contract.
Now, I'll call the withdrawFromBridge from my contract which will call the withdrawTokens function on the bridge contract and we'll enter the following functionality:
Let's see what _withdrawFromEscrow does:
Notice how setting the depositor to address(0) with _escrow[collection][id] = address(0x0); is made after the transfer.
Since our token is currently sitting in the escrow on L1, the if is skipped, and the token is transferred with this IERC721(collection).safeTransferFrom(from, to, id);
The important thing to know is that safeTransferFrom will call the onERC721Received function on the receiving address if the address is a contract. Here's the ERC721 implementation:
As you can see, the tokenId is transferred then checkOnERC721Received is called and this is the functionality of the function:
So as confirmed with the code above we established that the token will be transferred and then the onERC721Received function will be called on the receiving contract.
Which, in our case, will deposit the token again to the bridge and bridge it from L1 --> L2. During this _escrow[collection][id] will be set to our contract's address, however, as soon as the onERC721Received is executed i.e the deposit function on the bridge, we'll return to execution on the _withdrawFromEscrow function and we'll execute the last line:
Let's summarize what we did:
we had tokenId in L1 escrow, and we owned in on L2
we bridged it from L1 -> L2, withdrew and with reentering we deposited it again from L1 to L2
execution continues in the _withdrawFromEscrow function and sets _escrow[collection][id] = address(0x0); although this mapping should point to the depositor(us)
Now the token is in the L1 escrow with depositor set as address(0) and we have the token on L2.
If we were to sell this token to any unsuspecting user and that user tries to bridge it, the token will be locked forever.
Let's see why.
He'll bridge it from L2 -> L1, and will call withdrawTokens on L1 which brings us to this functionality again:
When we enter the _withdrawFromEscrow function wasEscrowed will return false:
since the depositor is set to address(0) and this is what the _isEscrowed function is checking:
and withdrawTokens will continue with this:
which will revert since it'll try to mint a tokenId that already exists(it's currently in the bridge contract). Or it'll revert if the the collection was a native L1 collection and mintFromBridge function doesn't exist.
Now the unsuspecting user's tokens are permanently stuck on L1 and L2, and there's nothing he can do to unlock them.
This attack doesn't cost anything other than some bridging fees and can be done for a big % of the total supply of any collection. For example, if that's done on most of the Everai NFTs it's almost guaranteed that some user will try to bridge his NFT and get his tokens stuck.
User's NFTs are lost forever which greatly damages the reputation of the protocol.
Manual review
One option is to use transferFrom instead of safeTransferFrom. However, an even better option is to just move _escrow[collection][id] = address(0x0); above in the function before any interactions. Like that:
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.