From 02c457a5195b10b24bbfd78384dba2d3944e2c87 Mon Sep 17 00:00:00 2001
From: smartcontracts <kelvin@optimism.io>
Date: Tue, 27 Sep 2022 17:58:30 -0400
Subject: [PATCH] feat(ctp): no refunds for NFTs (#3582)

Removes refunds for failed NFT deposits.

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
---
 .changeset/calm-ladybugs-occur.md             |  6 ++
 integration-tests/test/nft-bridge.spec.ts     | 61 ----------------
 .../contracts/L1/L1ERC721Bridge.sol           | 52 +-------------
 .../contracts/L2/L2ERC721Bridge.sol           | 59 ++--------------
 .../universal/op-erc721/ERC721Bridge.sol      | 27 --------
 .../test/contracts/L1/L1ERC721Bridge.spec.ts  | 63 -----------------
 .../test/contracts/L2/L2ERC721Bridge.spec.ts  | 69 -------------------
 7 files changed, 15 insertions(+), 322 deletions(-)
 create mode 100644 .changeset/calm-ladybugs-occur.md

diff --git a/.changeset/calm-ladybugs-occur.md b/.changeset/calm-ladybugs-occur.md
new file mode 100644
index 000000000..00659e1ef
--- /dev/null
+++ b/.changeset/calm-ladybugs-occur.md
@@ -0,0 +1,6 @@
+---
+'@eth-optimism/integration-tests': patch
+'@eth-optimism/contracts-periphery': patch
+---
+
+Removes NFT refund logic if withdrawals fail.
diff --git a/integration-tests/test/nft-bridge.spec.ts b/integration-tests/test/nft-bridge.spec.ts
index b6632a14d..0640d97d8 100644
--- a/integration-tests/test/nft-bridge.spec.ts
+++ b/integration-tests/test/nft-bridge.spec.ts
@@ -3,7 +3,6 @@ import { Contract, ContractFactory, utils, Wallet } from 'ethers'
 import { ethers } from 'hardhat'
 import { getChainId } from '@eth-optimism/core-utils'
 import { predeploys } from '@eth-optimism/contracts'
-import { MessageLike } from '@eth-optimism/sdk'
 import Artifact__TestERC721 from '@eth-optimism/contracts-periphery/artifacts/contracts/testing/helpers/TestERC721.sol/TestERC721.json'
 import Artifact__L1ERC721Bridge from '@eth-optimism/contracts-periphery/artifacts/contracts/L1/L1ERC721Bridge.sol/L1ERC721Bridge.json'
 import Artifact__L2ERC721Bridge from '@eth-optimism/contracts-periphery/artifacts/contracts/L2/L2ERC721Bridge.sol/L2ERC721Bridge.json'
@@ -18,9 +17,6 @@ import { withdrawalTest } from './shared/utils'
 const TOKEN_ID: number = 1
 const FINALIZATION_GAS: number = 600_000
 const NON_NULL_BYTES: string = '0x1111'
-const DUMMY_L1ERC721_ADDRESS: string = ethers.utils.getAddress(
-  '0x' + 'acdc'.repeat(10)
-)
 
 describe('ERC721 Bridge', () => {
   let env: OptimismEnv
@@ -313,61 +309,4 @@ describe('ERC721 Bridge', () => {
       expect(await L1ERC721.ownerOf(TOKEN_ID)).to.equal(L1ERC721Bridge.address)
     }
   )
-
-  withdrawalTest(
-    'should refund an L2 NFT that fails to be finalized on l1',
-    async () => {
-      // Deploy an L2 native NFT, which:
-      // - Mimics the interface of an OptimismMintableERC721.
-      // - Allows anyone to mint tokens.
-      // - Has a `remoteToken` state variable that returns the address of a non-existent L1 ERC721.
-      //     This will cause the bridge to fail on L1, triggering a refund on L2.
-      const L2NativeNFT = await (
-        await ethers.getContractFactory(
-          'FakeOptimismMintableERC721',
-          aliceWalletL2
-        )
-      ).deploy(
-        L2ERC721Bridge.address,
-        DUMMY_L1ERC721_ADDRESS,
-        await getChainId(env.l1Wallet.provider)
-      )
-      await L2NativeNFT.deployed()
-
-      // Alice mints an NFT from the L2 native ERC721 contract
-      const tx = await L2NativeNFT.safeMint(aliceAddress, TOKEN_ID)
-      await tx.wait()
-
-      // Check that Alice owns the L2 NFT
-      expect(await L2NativeNFT.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
-
-      // Alice bridges her L2 native NFT to L1, which burns the L2 NFT.
-      const withdrawalTx = await L2ERC721Bridge.connect(
-        aliceWalletL2
-      ).bridgeERC721(
-        L2NativeNFT.address,
-        DUMMY_L1ERC721_ADDRESS,
-        TOKEN_ID,
-        FINALIZATION_GAS,
-        NON_NULL_BYTES
-      )
-      await withdrawalTx.wait()
-
-      // Check that the token was burnt on L2 (pre-refund).
-      await expect(L2NativeNFT.ownerOf(TOKEN_ID)).to.be.revertedWith(
-        'ERC721: owner query for nonexistent token'
-      )
-
-      // Relay the cross-domain transaction to L1, which initiates an L1 -> L2 message to refund
-      // Alice her L2 NFT.
-      await env.relayXDomainMessages(withdrawalTx)
-
-      // Wait for the L1 -> L2 message to finalize on L2
-      const txPair = await env.waitForXDomainTransaction(withdrawalTx)
-      await env.messenger.waitForMessageReceipt(txPair.remoteTx as MessageLike)
-
-      // Check that the L2 NFT has been refunded to Alice.
-      expect(await L2NativeNFT.ownerOf(TOKEN_ID)).to.equal(aliceAddress)
-    }
-  )
 })
diff --git a/packages/contracts-periphery/contracts/L1/L1ERC721Bridge.sol b/packages/contracts-periphery/contracts/L1/L1ERC721Bridge.sol
index 74f94d7d2..5e8fa6bc8 100644
--- a/packages/contracts-periphery/contracts/L1/L1ERC721Bridge.sol
+++ b/packages/contracts-periphery/contracts/L1/L1ERC721Bridge.sol
@@ -55,55 +55,6 @@ contract L1ERC721Bridge is ERC721Bridge, Semver {
         uint256 _tokenId,
         bytes calldata _extraData
     ) external onlyOtherBridge {
-        try this.completeOutboundTransfer(_localToken, _remoteToken, _to, _tokenId) {
-            // slither-disable-next-line reentrancy-events
-            emit ERC721BridgeFinalized(_localToken, _remoteToken, _from, _to, _tokenId, _extraData);
-        } catch {
-            // If the token ID for this L1/L2 NFT pair is not escrowed in the L1 Bridge or if
-            // another error occurred during finalization, we initiate a cross-domain message to
-            // send the NFT back to its original owner on L2. This can happen if an L2 native NFT is
-            // bridged to L1, or if a user mistakenly entered an incorrect L1 ERC721 address.
-            // In either case, we stop the process here and construct a withdrawal in which we
-            // flip the to and from addresses. This ensures that event-based accounting
-            // will indicate net-zero transfer to the recipient. The ERC721BridgeFailed event
-            // emitted below can also be used to identify this occurence.
-            bytes memory message = abi.encodeWithSelector(
-                L2ERC721Bridge.finalizeBridgeERC721.selector,
-                _remoteToken,
-                _localToken,
-                _to,
-                _from, // Refund the NFT to the original owner on the remote chain.
-                _tokenId,
-                _extraData
-            );
-
-            // Send the message to the L2 bridge.
-            // slither-disable-next-line reentrancy-events
-            messenger.sendMessage(otherBridge, message, 600_000);
-
-            // slither-disable-next-line reentrancy-events
-            emit ERC721BridgeFailed(_localToken, _remoteToken, _from, _to, _tokenId, _extraData);
-        }
-    }
-
-    /**
-     * @notice Completes an outbound token transfer. Public function, but can only be called by
-     *         this contract. It's security critical that there be absolutely no way for anyone to
-     *         trigger this function, except by explicit trigger within this contract. Used as a
-     *         simple way to be able to try/catch any type of revert that could occur during an
-     *         ERC721 mint/transfer.
-     *
-     * @param _localToken  Address of the ERC721 on this chain.
-     * @param _remoteToken Address of the corresponding token on the remote chain.
-     * @param _to          Address of the receiver.
-     * @param _tokenId     ID of the token being deposited.
-     */
-    function completeOutboundTransfer(
-        address _localToken,
-        address _remoteToken,
-        address _to,
-        uint256 _tokenId
-    ) external onlySelf {
         require(_localToken != address(this), "L1ERC721Bridge: local token cannot be self");
 
         // Checks that the L1/L2 NFT pair has a token ID that is escrowed in the L1 Bridge.
@@ -119,6 +70,9 @@ contract L1ERC721Bridge is ERC721Bridge, Semver {
         // When a withdrawal is finalized on L1, the L1 Bridge transfers the NFT to the
         // withdrawer.
         IERC721(_localToken).safeTransferFrom(address(this), _to, _tokenId);
+
+        // slither-disable-next-line reentrancy-events
+        emit ERC721BridgeFinalized(_localToken, _remoteToken, _from, _to, _tokenId, _extraData);
     }
 
     /**
diff --git a/packages/contracts-periphery/contracts/L2/L2ERC721Bridge.sol b/packages/contracts-periphery/contracts/L2/L2ERC721Bridge.sol
index 18ae5ed66..7ff753d71 100644
--- a/packages/contracts-periphery/contracts/L2/L2ERC721Bridge.sol
+++ b/packages/contracts-periphery/contracts/L2/L2ERC721Bridge.sol
@@ -51,65 +51,15 @@ contract L2ERC721Bridge is ERC721Bridge, Semver {
         uint256 _tokenId,
         bytes calldata _extraData
     ) external onlyOtherBridge {
-        try this.completeOutboundTransfer(_localToken, _remoteToken, _to, _tokenId) {
-            // slither-disable-next-line reentrancy-events
-            emit ERC721BridgeFinalized(_localToken, _remoteToken, _from, _to, _tokenId, _extraData);
-        } catch {
-            // Either the L2 token which is being deposited-into disagrees about the correct address
-            // of its L1 token, or does not support the correct interface.
-            // This should only happen if there is a malicious L2 token, or if a user somehow
-            // specified the wrong L2 token address to deposit into.
-            // There is no way to prevent malicious token contracts altogether, but this does limit
-            // user error and mitigate some forms of malicious contract behavior.
-            /// In either case, we stop the process here and construct a withdrawal in which we
-            // flip the to and from addresses. This ensures that event-based accounting
-            // will indicate net-zero transfer to the recipient. The ERC721BridgeFailed event
-            // emitted below can also be used to identify this occurence.
-            bytes memory message = abi.encodeWithSelector(
-                L1ERC721Bridge.finalizeBridgeERC721.selector,
-                _remoteToken,
-                _localToken,
-                _to,
-                _from, // Refund the NFT to the original owner on the remote chain.
-                _tokenId,
-                _extraData
-            );
-
-            // Send the message to the L1 bridge
-            // slither-disable-next-line reentrancy-events
-            messenger.sendMessage(otherBridge, message, 0);
-
-            // slither-disable-next-line reentrancy-events
-            emit ERC721BridgeFailed(_localToken, _remoteToken, _from, _to, _tokenId, _extraData);
-        }
-    }
-
-    /**
-     * @notice Completes an outbound token transfer. Public function, but can only be called by
-     *         this contract. It's security critical that there be absolutely no way for anyone to
-     *         trigger this function, except by explicit trigger within this contract. Used as a
-     *         simple way to be able to try/catch any type of revert that could occur during an
-     *         ERC721 mint/transfer.
-     *
-     * @param _localToken  Address of the ERC721 on this chain.
-     * @param _remoteToken Address of the corresponding token on the remote chain.
-     * @param _to          Address of the receiver.
-     * @param _tokenId     ID of the token being deposited.
-     */
-    function completeOutboundTransfer(
-        address _localToken,
-        address _remoteToken,
-        address _to,
-        uint256 _tokenId
-    ) external onlySelf {
         require(_localToken != address(this), "L2ERC721Bridge: local token cannot be self");
 
+        // Note that supportsInterface makes a callback to the _localToken address which is user
+        // provided.
         require(
-            // Note that supportsInterface makes a callback to the _localToken address
-            // which is user provided.
             ERC165Checker.supportsInterface(_localToken, type(IOptimismMintableERC721).interfaceId),
             "L2ERC721Bridge: local token interface is not compliant"
         );
+
         require(
             _remoteToken == IOptimismMintableERC721(_localToken).remoteToken(),
             "L2ERC721Bridge: wrong remote token for Optimism Mintable ERC721 local token"
@@ -118,6 +68,9 @@ contract L2ERC721Bridge is ERC721Bridge, Semver {
         // When a deposit is finalized, we give the NFT with the same tokenId to the account
         // on L2. Note that safeMint makes a callback to the _to address which is user provided.
         IOptimismMintableERC721(_localToken).safeMint(_to, _tokenId);
+
+        // slither-disable-next-line reentrancy-events
+        emit ERC721BridgeFinalized(_localToken, _remoteToken, _from, _to, _tokenId, _extraData);
     }
 
     /**
diff --git a/packages/contracts-periphery/contracts/universal/op-erc721/ERC721Bridge.sol b/packages/contracts-periphery/contracts/universal/op-erc721/ERC721Bridge.sol
index 8aead3daa..df892cf61 100644
--- a/packages/contracts-periphery/contracts/universal/op-erc721/ERC721Bridge.sol
+++ b/packages/contracts-periphery/contracts/universal/op-erc721/ERC721Bridge.sol
@@ -49,25 +49,6 @@ abstract contract ERC721Bridge {
         bytes extraData
     );
 
-    /**
-     * @notice Emitted when an ERC721 bridge from the other network fails.
-     *
-     * @param localToken  Address of the token on this domain.
-     * @param remoteToken Address of the token on the remote domain.
-     * @param from        Address that initiated bridging action.
-     * @param to          Address to receive the token.
-     * @param tokenId     ID of the specific token deposited.
-     * @param extraData   Extra data for use on the client-side.
-     */
-    event ERC721BridgeFailed(
-        address indexed localToken,
-        address indexed remoteToken,
-        address indexed from,
-        address to,
-        uint256 tokenId,
-        bytes extraData
-    );
-
     /**
      * @notice Messenger contract on this domain.
      */
@@ -94,14 +75,6 @@ abstract contract ERC721Bridge {
         _;
     }
 
-    /**
-     * @notice Ensures that the caller is this contract.
-     */
-    modifier onlySelf() {
-        require(msg.sender == address(this), "ERC721Bridge: function can only be called by self");
-        _;
-    }
-
     /**
      * @param _messenger   Address of the CrossDomainMessenger on this network.
      * @param _otherBridge Address of the ERC721 bridge on the other network.
diff --git a/packages/contracts-periphery/test/contracts/L1/L1ERC721Bridge.spec.ts b/packages/contracts-periphery/test/contracts/L1/L1ERC721Bridge.spec.ts
index 24d49ee6e..8246eba2d 100644
--- a/packages/contracts-periphery/test/contracts/L1/L1ERC721Bridge.spec.ts
+++ b/packages/contracts-periphery/test/contracts/L1/L1ERC721Bridge.spec.ts
@@ -385,56 +385,6 @@ describe('L1ERC721Bridge', () => {
         )
       })
 
-      it('should refund an L2 NFT that fails to be finalized on l1', async () => {
-        const RANDOM_L1_ERC721_ADDRESS = ethers.utils.getAddress(
-          '0x' + 'cdbc'.repeat(10)
-        )
-        // alice sends bob an nft that has an incorrect l1 erc721 address
-        await expect(
-          L1ERC721Bridge.finalizeBridgeERC721(
-            RANDOM_L1_ERC721_ADDRESS, // incorrect address for the l1 erc721
-            DUMMY_L2_ERC721_ADDRESS,
-            aliceAddress,
-            bobsAddress,
-            tokenId,
-            NON_NULL_BYTES32,
-            {
-              from: Fake__L1CrossDomainMessenger.address,
-            }
-          )
-        )
-          .to.emit(L1ERC721Bridge, 'ERC721BridgeFailed')
-          .withArgs(
-            RANDOM_L1_ERC721_ADDRESS,
-            DUMMY_L2_ERC721_ADDRESS,
-            aliceAddress,
-            bobsAddress,
-            tokenId,
-            NON_NULL_BYTES32
-          )
-
-        // Get the second call from `Fake__L1CrossDomainMessenger` because the first call is `finalizeBridgeERC721`.
-        const depositCallToMessenger =
-          Fake__L1CrossDomainMessenger.sendMessage.getCall(1)
-
-        // Check the correct cross-chain call was sent:
-        // Message should be sent to the L2 bridge
-        expect(depositCallToMessenger.args[0]).to.equal(DUMMY_L2_BRIDGE_ADDRESS)
-        // Message data should be a call telling the L2DepositedERC721 to finalize the deposit
-        expect(depositCallToMessenger.args[1]).to.equal(
-          IL2ERC721Bridge.encodeFunctionData('finalizeBridgeERC721', [
-            DUMMY_L2_ERC721_ADDRESS,
-            RANDOM_L1_ERC721_ADDRESS,
-            bobsAddress,
-            aliceAddress,
-            tokenId,
-            NON_NULL_BYTES32,
-          ])
-        )
-        // Gas limit is 0
-        expect(depositCallToMessenger.args[2]).to.equal(FINALIZATION_GAS)
-      })
-
       it('should credit funds to the withdrawer to finalize withdrawal', async () => {
         // finalizing the withdrawal emits an ERC721BridgeFinalized event with the correct arguments.
         await expect(
@@ -472,17 +422,4 @@ describe('L1ERC721Bridge', () => {
       })
     })
   })
-
-  describe('completeOutboundTransfer', async () => {
-    it('reverts if caller is not L1 bridge', async () => {
-      await expect(
-        L1ERC721Bridge.completeOutboundTransfer(
-          L1ERC721.address,
-          DUMMY_L2_ERC721_ADDRESS,
-          bobsAddress,
-          tokenId
-        )
-      ).to.be.revertedWith('ERC721Bridge: function can only be called by self')
-    })
-  })
 })
diff --git a/packages/contracts-periphery/test/contracts/L2/L2ERC721Bridge.spec.ts b/packages/contracts-periphery/test/contracts/L2/L2ERC721Bridge.spec.ts
index 2bf492e95..aeb1f4c63 100644
--- a/packages/contracts-periphery/test/contracts/L2/L2ERC721Bridge.spec.ts
+++ b/packages/contracts-periphery/test/contracts/L2/L2ERC721Bridge.spec.ts
@@ -130,62 +130,6 @@ describe('L2ERC721Bridge', () => {
       ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MESSAGE)
     })
 
-    it('should initialize a withdrawal if the L2 token is not compliant', async () => {
-      // Deploy a non compliant ERC721
-      const NonCompliantERC721 = await (
-        await ethers.getContractFactory(
-          '@openzeppelin/contracts/token/ERC721/ERC721.sol:ERC721'
-        )
-      ).deploy('L2Token', 'L2T')
-
-      Fake__L2CrossDomainMessenger.xDomainMessageSender.returns(
-        DUMMY_L1BRIDGE_ADDRESS
-      )
-
-      // A failed attempt to finalize the deposit causes an ERC721BridgeFailed event to be emitted.
-      await expect(
-        L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeBridgeERC721(
-          NonCompliantERC721.address,
-          DUMMY_L1ERC721_ADDRESS,
-          aliceAddress,
-          bobsAddress,
-          TOKEN_ID,
-          NON_NULL_BYTES32,
-          {
-            from: Fake__L2CrossDomainMessenger.address,
-          }
-        )
-      )
-        .to.emit(L2ERC721Bridge, 'ERC721BridgeFailed')
-        .withArgs(
-          NonCompliantERC721.address,
-          DUMMY_L1ERC721_ADDRESS,
-          aliceAddress,
-          bobsAddress,
-          TOKEN_ID,
-          NON_NULL_BYTES32
-        )
-
-      const withdrawalCallToMessenger =
-        Fake__L2CrossDomainMessenger.sendMessage.getCall(0)
-
-      expect(withdrawalCallToMessenger.args[0]).to.equal(DUMMY_L1BRIDGE_ADDRESS)
-      expect(withdrawalCallToMessenger.args[1]).to.equal(
-        Factory__L1ERC721Bridge.interface.encodeFunctionData(
-          'finalizeBridgeERC721',
-          [
-            DUMMY_L1ERC721_ADDRESS,
-            NonCompliantERC721.address,
-            bobsAddress,
-            aliceAddress,
-            TOKEN_ID,
-            NON_NULL_BYTES32,
-          ]
-        )
-      )
-      expect(withdrawalCallToMessenger.args[2]).to.equal(0)
-    })
-
     it('should credit funds to the depositor', async () => {
       Fake__L2CrossDomainMessenger.xDomainMessageSender.returns(
         DUMMY_L1BRIDGE_ADDRESS
@@ -234,19 +178,6 @@ describe('L2ERC721Bridge', () => {
     })
   })
 
-  describe('completeOutboundTransfer', async () => {
-    it('reverts if caller is not L2 bridge', async () => {
-      await expect(
-        L2ERC721Bridge.completeOutboundTransfer(
-          L2ERC721.address,
-          DUMMY_L1ERC721_ADDRESS,
-          bobsAddress,
-          TOKEN_ID
-        )
-      ).to.be.revertedWith('ERC721Bridge: function can only be called by self')
-    })
-  })
-
   describe('withdrawals', () => {
     let L2Token: Contract
     beforeEach(async () => {
-- 
2.23.0