/* Imports */ import { ethers } from 'hardhat' import { Signer, ContractFactory, Contract, constants } from 'ethers' import { smock, FakeContract } from '@defi-wonderland/smock' import ICrossDomainMessenger from '@eth-optimism/contracts/artifacts/contracts/libraries/bridge/ICrossDomainMessenger.sol/ICrossDomainMessenger.json' import { toRpcHexString } from '@eth-optimism/core-utils' import { NON_NULL_BYTES32, NON_ZERO_ADDRESS } from '../../helpers' import { expect } from '../../setup' const ERR_INVALID_X_DOMAIN_MESSAGE = 'ERC721Bridge: function can only be called from the other bridge' const DUMMY_L1BRIDGE_ADDRESS: string = '0x1234123412341234123412341234123412341234' const DUMMY_L1ERC721_ADDRESS: string = '0x2234223412342234223422342234223422342234' const ERR_INVALID_WITHDRAWAL: string = 'Withdrawal is not being initiated by NFT owner' const TOKEN_ID: number = 10 const FINALIZATION_GAS = 600_000 describe('L2ERC721Bridge', () => { let alice: Signer let aliceAddress: string let bob: Signer let bobsAddress: string let l2MessengerImpersonator: Signer let Factory__L1ERC721Bridge: ContractFactory before(async () => { // Create a special signer which will enable us to send messages from the L2Messenger contract ;[l2MessengerImpersonator, alice, bob] = await ethers.getSigners() aliceAddress = await alice.getAddress() bobsAddress = await bob.getAddress() Factory__L1ERC721Bridge = await ethers.getContractFactory('L1ERC721Bridge') }) let Factory__L2ERC721Bridge: ContractFactory let L2ERC721Bridge: Contract let L2ERC721: Contract let Fake__L2CrossDomainMessenger: FakeContract beforeEach(async () => { // Get a new fake L2 messenger Fake__L2CrossDomainMessenger = await smock.fake( new ethers.utils.Interface(ICrossDomainMessenger.abi), // This allows us to use an ethers override {from: Fake__L2CrossDomainMessenger.address} to mock calls { address: await l2MessengerImpersonator.getAddress() } ) // Deploy the contract under test Factory__L2ERC721Bridge = await ethers.getContractFactory('L2ERC721Bridge') L2ERC721Bridge = await Factory__L2ERC721Bridge.deploy( Fake__L2CrossDomainMessenger.address, DUMMY_L1BRIDGE_ADDRESS ) // Deploy an L2 ERC721 L2ERC721 = await ( await ethers.getContractFactory('OptimismMintableERC721') ).deploy( L2ERC721Bridge.address, 100, DUMMY_L1ERC721_ADDRESS, 'L2Token', 'L2T', { gasLimit: 4_000_000 } // Necessary to avoid an out-of-gas error ) }) describe('constructor', async () => { it('reverts if cross domain messenger is address(0)', async () => { await expect( Factory__L2ERC721Bridge.deploy( constants.AddressZero, DUMMY_L1BRIDGE_ADDRESS ) ).to.be.revertedWith('ERC721Bridge: messenger cannot be address(0)') }) it('reverts if other bridge is address(0)', async () => { await expect( Factory__L2ERC721Bridge.deploy( Fake__L2CrossDomainMessenger.address, constants.AddressZero ) ).to.be.revertedWith('ERC721Bridge: other bridge cannot be address(0)') }) it('initializes correctly', async () => { expect(await L2ERC721Bridge.messenger()).equals( Fake__L2CrossDomainMessenger.address ) expect(await L2ERC721Bridge.otherBridge()).equals(DUMMY_L1BRIDGE_ADDRESS) }) }) // test the transfer flow of moving a token from L1 to L2 describe('finalizeBridgeERC721', () => { it('onlyFromCrossDomainAccount: should revert on calls from a non-crossDomainMessenger L2 account', async () => { await expect( L2ERC721Bridge.connect(alice).finalizeBridgeERC721( DUMMY_L1ERC721_ADDRESS, NON_ZERO_ADDRESS, NON_ZERO_ADDRESS, NON_ZERO_ADDRESS, TOKEN_ID, NON_NULL_BYTES32 ) ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MESSAGE) }) it('onlyFromCrossDomainAccount: should revert on calls from the right crossDomainMessenger, but wrong xDomainMessageSender (ie. not the L1ERC721Bridge)', async () => { Fake__L2CrossDomainMessenger.xDomainMessageSender.returns( NON_ZERO_ADDRESS ) await expect( L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeBridgeERC721( DUMMY_L1ERC721_ADDRESS, NON_ZERO_ADDRESS, NON_ZERO_ADDRESS, NON_ZERO_ADDRESS, TOKEN_ID, NON_NULL_BYTES32, { from: Fake__L2CrossDomainMessenger.address, } ) ).to.be.revertedWith(ERR_INVALID_X_DOMAIN_MESSAGE) }) it('should credit funds to the depositor', async () => { Fake__L2CrossDomainMessenger.xDomainMessageSender.returns( DUMMY_L1BRIDGE_ADDRESS ) // Assert that nobody owns the L2 token initially await expect(L2ERC721.ownerOf(TOKEN_ID)).to.be.revertedWith( 'ERC721: invalid token ID' ) // Successfully finalizes the deposit. const expectedResult = expect( L2ERC721Bridge.connect(l2MessengerImpersonator).finalizeBridgeERC721( L2ERC721.address, DUMMY_L1ERC721_ADDRESS, aliceAddress, bobsAddress, TOKEN_ID, NON_NULL_BYTES32, { from: Fake__L2CrossDomainMessenger.address, } ) ) // Depositing causes an ERC721BridgeFinalized event to be emitted. await expectedResult.to .emit(L2ERC721Bridge, 'ERC721BridgeFinalized') .withArgs( L2ERC721.address, DUMMY_L1ERC721_ADDRESS, aliceAddress, bobsAddress, TOKEN_ID, NON_NULL_BYTES32 ) // Causes a Transfer event to be emitted from the L2 ERC721. await expectedResult.to .emit(L2ERC721, 'Transfer') .withArgs(constants.AddressZero, bobsAddress, TOKEN_ID) // Bob is now the owner of the L2 ERC721 const tokenIdOwner = await L2ERC721.ownerOf(TOKEN_ID) tokenIdOwner.should.equal(bobsAddress) }) }) describe('withdrawals', () => { let L2Token: Contract beforeEach(async () => { L2Token = await ( await ethers.getContractFactory('OptimismMintableERC721') ).deploy( L2ERC721Bridge.address, 100, DUMMY_L1ERC721_ADDRESS, 'L2Token', 'L2T' ) await ethers.provider.send('hardhat_impersonateAccount', [ L2ERC721Bridge.address, ]) await ethers.provider.send('hardhat_setBalance', [ L2ERC721Bridge.address, toRpcHexString(ethers.utils.parseEther('1')), ]) const signer = await ethers.getSigner(L2ERC721Bridge.address) await L2Token.connect(signer).safeMint(aliceAddress, TOKEN_ID) }) it('bridgeERC721() reverts if remote token is address(0)', async () => { await expect( L2ERC721Bridge.connect(alice).bridgeERC721( L2Token.address, constants.AddressZero, TOKEN_ID, FINALIZATION_GAS, NON_NULL_BYTES32 ) ).to.be.revertedWith('ERC721Bridge: remote token cannot be address(0)') }) it('bridgeERC721() reverts when called by non-owner of nft', async () => { await expect( L2ERC721Bridge.connect(bob).bridgeERC721( L2Token.address, DUMMY_L1ERC721_ADDRESS, TOKEN_ID, 0, NON_NULL_BYTES32 ) ).to.be.revertedWith(ERR_INVALID_WITHDRAWAL) }) it('bridgeERC721() reverts if called by a contract', async () => { await expect( L2ERC721Bridge.connect(l2MessengerImpersonator).bridgeERC721( L2Token.address, DUMMY_L1ERC721_ADDRESS, TOKEN_ID, 0, NON_NULL_BYTES32 ) ).to.be.revertedWith('ERC721Bridge: account is not externally owned') }) it('bridgeERC721() burns and sends the correct withdrawal message', async () => { // Make sure that alice begins as the NFT owner expect(await L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress) // Initiates a successful withdrawal. const expectedResult = expect( L2ERC721Bridge.connect(alice).bridgeERC721( L2Token.address, DUMMY_L1ERC721_ADDRESS, TOKEN_ID, 0, NON_NULL_BYTES32 ) ) // A successful withdrawal causes an ERC721BridgeInitiated event to be emitted from the L2 ERC721 Bridge. await expectedResult.to .emit(L2ERC721Bridge, 'ERC721BridgeInitiated') .withArgs( L2Token.address, DUMMY_L1ERC721_ADDRESS, aliceAddress, aliceAddress, TOKEN_ID, NON_NULL_BYTES32 ) // A withdrawal also causes a Transfer event to be emitted the L2 ERC721, signifying that the L2 token // has been burnt. await expectedResult.to .emit(L2Token, 'Transfer') .withArgs(aliceAddress, constants.AddressZero, TOKEN_ID) // Assert Alice's balance went down const aliceBalance = await L2Token.balanceOf(aliceAddress) expect(aliceBalance).to.equal(0) // Assert that the token isn't owned by anyone await expect(L2Token.ownerOf(TOKEN_ID)).to.be.revertedWith( 'ERC721: invalid token ID' ) const withdrawalCallToMessenger = Fake__L2CrossDomainMessenger.sendMessage.getCall(0) // Assert the correct cross-chain call was sent: // Message should be sent to the L1ERC721Bridge on L1 expect(withdrawalCallToMessenger.args[0]).to.equal(DUMMY_L1BRIDGE_ADDRESS) // Message data should be a call telling the L1ERC721Bridge to finalize the withdrawal expect(withdrawalCallToMessenger.args[1]).to.equal( Factory__L1ERC721Bridge.interface.encodeFunctionData( 'finalizeBridgeERC721', [ DUMMY_L1ERC721_ADDRESS, L2Token.address, aliceAddress, aliceAddress, TOKEN_ID, NON_NULL_BYTES32, ] ) ) // gaslimit should be correct expect(withdrawalCallToMessenger.args[2]).to.equal(0) }) it('bridgeERC721To() reverts if NFT receiver is address(0)', async () => { await expect( L2ERC721Bridge.connect(alice).bridgeERC721To( L2Token.address, DUMMY_L1ERC721_ADDRESS, constants.AddressZero, TOKEN_ID, 0, NON_NULL_BYTES32 ) ).to.be.revertedWith('ERC721Bridge: nft recipient cannot be address(0)') }) it('bridgeERC721To() reverts when called by non-owner of nft', async () => { await expect( L2ERC721Bridge.connect(bob).bridgeERC721To( L2Token.address, DUMMY_L1ERC721_ADDRESS, bobsAddress, TOKEN_ID, 0, NON_NULL_BYTES32 ) ).to.be.revertedWith(ERR_INVALID_WITHDRAWAL) }) it('bridgeERC721To() burns and sends the correct withdrawal message', async () => { // Make sure that alice begins as the NFT owner expect(await L2Token.ownerOf(TOKEN_ID)).to.equal(aliceAddress) // Initiates a successful withdrawal. const expectedResult = expect( L2ERC721Bridge.connect(alice).bridgeERC721To( L2Token.address, DUMMY_L1ERC721_ADDRESS, bobsAddress, TOKEN_ID, 0, NON_NULL_BYTES32 ) ) // A successful withdrawal causes an ERC721BridgeInitiated event to be emitted from the L2 ERC721 Bridge. await expectedResult.to .emit(L2ERC721Bridge, 'ERC721BridgeInitiated') .withArgs( L2Token.address, DUMMY_L1ERC721_ADDRESS, aliceAddress, bobsAddress, TOKEN_ID, NON_NULL_BYTES32 ) // A withdrawal also causes a Transfer event to be emitted the L2 ERC721, signifying that the L2 token // has been burnt. await expectedResult.to .emit(L2Token, 'Transfer') .withArgs(aliceAddress, constants.AddressZero, TOKEN_ID) // Assert Alice's balance went down const aliceBalance = await L2Token.balanceOf(aliceAddress) expect(aliceBalance).to.equal(0) // Assert that the token isn't owned by anyone await expect(L2Token.ownerOf(TOKEN_ID)).to.be.revertedWith( 'ERC721: invalid token ID' ) const withdrawalCallToMessenger = Fake__L2CrossDomainMessenger.sendMessage.getCall(0) // Assert the correct cross-chain call was sent. // Message should be sent to the L1ERC721Bridge on L1 expect(withdrawalCallToMessenger.args[0]).to.equal(DUMMY_L1BRIDGE_ADDRESS) // The message data should be a call telling the L1ERC721Bridge to finalize the withdrawal expect(withdrawalCallToMessenger.args[1]).to.equal( Factory__L1ERC721Bridge.interface.encodeFunctionData( 'finalizeBridgeERC721', [ DUMMY_L1ERC721_ADDRESS, L2Token.address, aliceAddress, bobsAddress, TOKEN_ID, NON_NULL_BYTES32, ] ) ) // gas value is ignored and set to 0. expect(withdrawalCallToMessenger.args[2]).to.equal(0) }) }) })