diff --git a/SUMMARY.md b/SUMMARY.md index ad7d8c11ef..c45164fc77 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -44,7 +44,7 @@ * [Other](packages/README.md) * [Liquidity Pools](packages/boba/contracts/contracts/LP/README.md) - * [Boba NFT Bridges](packages/boba/contracts/contracts/bridges/README.md) + * [Boba NFT Bridges](packages/boba/contracts/contracts/ERC721Bridges/README.md) * [Boba Straw Price Feed Oracle](packages/boba/contracts/contracts/oracle/README.md) * [Gas Price Oracle](packages/boba/gas-price-oracle/README.md) * [Contracts Registration](packages/boba/register/README.md) diff --git a/boba_community/turing-monsters/test/NFTMonsterV2.ts b/boba_community/turing-monsters/test/NFTMonsterV2.ts index fbd0919a5c..265386999d 100644 --- a/boba_community/turing-monsters/test/NFTMonsterV2.ts +++ b/boba_community/turing-monsters/test/NFTMonsterV2.ts @@ -10,7 +10,7 @@ import ERC721Json from "../artifacts/contracts/NFTMonsterV2.sol/NFTMonsterV2.jso import L2BridgeMessengerMockJson from "../artifacts/contracts/L2BridgeMockMessenger.sol/L2BridgeMockMessenger.json" import TuringHelperJson from "../artifacts/contracts/TuringHelper.sol/TuringHelper.json" import L2GovernanceERC20Json from '@boba/contracts/artifacts/contracts/standards/L2GovernanceERC20.sol/L2GovernanceERC20.json' -import L2NFTBridgeJson from '@boba/contracts/artifacts/contracts/bridges/L2NFTBridge.sol/L2NFTBridge.json' +import L2NFTBridgeJson from '@boba/contracts/artifacts/contracts/ERC721Bridges/L2NFTBridge.sol/L2NFTBridge.json' const cfg = hre.network.config diff --git a/boba_examples/nft_bridging/README.md b/boba_examples/nft_bridging/README.md index 4e7dba72b5..74a15b5a8f 100644 --- a/boba_examples/nft_bridging/README.md +++ b/boba_examples/nft_bridging/README.md @@ -20,7 +20,7 @@ To withdraw to L1: This example will walk you through the process of bridging any L2 native ERC721 to L1 (Ethereum) and also bridging them back to L2. -This example is towards bridging in/out a Layer-2 native ERC721 (meaning a NFT originally deployed to L2 Boba). However, the bridge can support the same features with Layer-1 NFTs as well, for which please refer to the [more elaborate documentation](../../packages/boba/contracts/contracts/bridges/README.md). +This example is towards bridging in/out a Layer-2 native ERC721 (meaning a NFT originally deployed to L2 Boba). However, the bridge can support the same features with Layer-1 NFTs as well, for which please refer to the [more elaborate documentation](../../packages/boba/contracts/contracts/ERC721Bridges/README.md). ## Quickstart - Rinkeby @@ -168,7 +168,7 @@ NFT bridged back to L2 successfully! ################################# ``` -Thanks for making it to the end of the tutorial! +Thanks for making it to the end of the tutorial! And, as promised - to clear the air about what super-special NFTs mean in terms of the bridge and how your bridging can be gas effective for them. diff --git a/boba_examples/nft_bridging/src/index.js b/boba_examples/nft_bridging/src/index.js index 98dd39ad5d..de2aa42382 100644 --- a/boba_examples/nft_bridging/src/index.js +++ b/boba_examples/nft_bridging/src/index.js @@ -6,8 +6,8 @@ require('dotenv').config() const SampleERC721Json = require('../artifacts/contracts/SampleERC721.sol/SampleERC721.json') const L1StandardERC721Json = require('@boba/contracts/artifacts/contracts/standards/L1StandardERC721.sol/L1StandardERC721.json') -const L1NFTBridgeJson = require('@boba/contracts/artifacts/contracts/bridges/L1NFTBridge.sol/L1NFTBridge.json') -const L2NFTBridgeJson = require('@boba/contracts/artifacts/contracts/bridges/L2NFTBridge.sol/L2NFTBridge.json') +const L1NFTBridgeJson = require('@boba/contracts/artifacts/contracts/ERC721Bridges/L1NFTBridge.sol/L1NFTBridge.json') +const L2NFTBridgeJson = require('@boba/contracts/artifacts/contracts/ERC721Bridges/L2NFTBridge.sol/L2NFTBridge.json') const { bridgeToL1 } = require('./bridgeToL1') const { bridgeBackToL2 } = require('./bridgeBackToL2') diff --git a/boba_examples/nft_bridging/src/quickStart-rinkeby.js b/boba_examples/nft_bridging/src/quickStart-rinkeby.js index 53b6f2e2f0..e27108f1b6 100644 --- a/boba_examples/nft_bridging/src/quickStart-rinkeby.js +++ b/boba_examples/nft_bridging/src/quickStart-rinkeby.js @@ -6,8 +6,8 @@ require('dotenv').config() const SampleERC721Json = require('../quickStart-Rinkeby/SampleERC721.json') const L1StandardERC721Json = require('@boba/contracts/artifacts/contracts/standards/L1StandardERC721.sol/L1StandardERC721.json') -const L1NFTBridgeJson = require('@boba/contracts/artifacts/contracts/bridges/L1NFTBridge.sol/L1NFTBridge.json') -const L2NFTBridgeJson = require('@boba/contracts/artifacts/contracts/bridges/L2NFTBridge.sol/L2NFTBridge.json') +const L1NFTBridgeJson = require('@boba/contracts/artifacts/contracts/ERC721Bridges/L1NFTBridge.sol/L1NFTBridge.json') +const L2NFTBridgeJson = require('@boba/contracts/artifacts/contracts/ERC721Bridges/L2NFTBridge.sol/L2NFTBridge.json') const { bridgeToL1 } = require('./bridgeToL1') const { bridgeBackToL2 } = require('./bridgeBackToL2') diff --git a/integration-tests/contracts/TestFailingMintL1StandardERC1155.sol b/integration-tests/contracts/TestFailingMintL1StandardERC1155.sol new file mode 100644 index 0000000000..e308c123dc --- /dev/null +++ b/integration-tests/contracts/TestFailingMintL1StandardERC1155.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; + +import "@boba/contracts/contracts/standards/L1StandardERC1155.sol"; + +/** +* A Failing mint L1ERC1155 contract +*/ +contract TestFailingMintL1StandardERC1155 is L1StandardERC1155 { + /** + * @param _l1Bridge Address of the L1 standard bridge. + * @param _l2Contract Address of the corresponding L2 token contract. + * @param _uri uri. + */ + constructor( + address _l1Bridge, + address _l2Contract, + string memory _uri + ) + L1StandardERC1155(_l1Bridge, _l2Contract, _uri) {} + + function mint(address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) public virtual override onlyL1Bridge { + revert("mint failing"); + } +} diff --git a/integration-tests/contracts/TestFailingMintL2StandardERC1155.sol b/integration-tests/contracts/TestFailingMintL2StandardERC1155.sol new file mode 100644 index 0000000000..76001ccfef --- /dev/null +++ b/integration-tests/contracts/TestFailingMintL2StandardERC1155.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; + +import "@boba/contracts/contracts/standards/L2StandardERC1155.sol"; + +/** +* A Failing mint L2ERC1155 contract +*/ +contract TestFailingMintL2StandardERC1155 is L2StandardERC1155 { + /** + * @param _l2Bridge Address of the L2 standard bridge. + * @param _l1Contract Address of the corresponding L1 token contract. + * @param _uri uri. + */ + constructor( + address _l2Bridge, + address _l1Contract, + string memory _uri + ) + L2StandardERC1155(_l2Bridge, _l1Contract, _uri) {} + + function mint(address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) public virtual override onlyL2Bridge { + revert("mint failing"); + } +} diff --git a/integration-tests/test/erc1155_bridge.spec.ts b/integration-tests/test/erc1155_bridge.spec.ts new file mode 100644 index 0000000000..7f8c186234 --- /dev/null +++ b/integration-tests/test/erc1155_bridge.spec.ts @@ -0,0 +1,1855 @@ +import chai, { expect } from 'chai' +import chaiAsPromised from 'chai-as-promised' +chai.use(chaiAsPromised) +import { Contract, ContractFactory, utils } from 'ethers' + +import { getFilteredLogIndex } from './shared/utils' + +import L1ERC1155BridgeJson from '@boba/contracts/artifacts/contracts/ERC1155Bridges/L1ERC1155Bridge.sol/L1ERC1155Bridge.json' +import L2ERC1155BridgeJson from '@boba/contracts/artifacts/contracts/ERC1155Bridges/L2ERC1155Bridge.sol/L2ERC1155Bridge.json' +import ERC1155Json from '@boba/contracts/artifacts/contracts/test-helpers/L1ERC1155.sol/L1ERC1155.json' +import L1StandardERC1155Json from '@boba/contracts/artifacts/contracts/standards/L1StandardERC1155.sol/L1StandardERC1155.json' +import L2StandardERC1155Json from '@boba/contracts/artifacts/contracts/standards/L2StandardERC1155.sol/L2StandardERC1155.json' + +import L2BillingContractJson from '@boba/contracts/artifacts/contracts/L2BillingContract.sol/L2BillingContract.json' +import L2GovernanceERC20Json from '@boba/contracts/artifacts/contracts/standards/L2GovernanceERC20.sol/L2GovernanceERC20.json' + +import L1ERC1155FailingMintJson from '../artifacts/contracts/TestFailingMintL1StandardERC1155.sol/TestFailingMintL1StandardERC1155.json' +import L2ERC1155FailingMintJson from '../artifacts/contracts/TestFailingMintL2StandardERC1155.sol/TestFailingMintL2StandardERC1155.json' + +import { OptimismEnv } from './shared/env' +import { ethers } from 'hardhat' + +describe('ERC1155 Bridge Test', async () => { + let Factory__L1ERC1155: ContractFactory + let Factory__L2ERC1155: ContractFactory + let L1Bridge: Contract + let L2Bridge: Contract + let L1ERC1155: Contract + let L2ERC1155: Contract + + let L2BOBAToken: Contract + let BOBABillingContract: Contract + + let env: OptimismEnv + + const DUMMY_URI_1 = 'first-unique-uri' + const DUMMY_TOKEN_ID_1 = 1 + const DUMMY_TOKEN_ID_2 = 2 + const DUMMY_TOKEN_ID_3 = 3 + const DUMMY_TOKEN_AMOUNT_1 = 100 + const DUMMY_TOKEN_AMOUNT_2 = 200 + const DUMMY_TOKEN_AMOUNT_3 = 300 + + before(async () => { + env = await OptimismEnv.new() + + L1Bridge = new Contract( + env.addressesBOBA.Proxy__L1ERC1155Bridge, + L1ERC1155BridgeJson.abi, + env.l1Wallet + ) + + L2Bridge = new Contract( + env.addressesBOBA.Proxy__L2ERC1155Bridge, + L2ERC1155BridgeJson.abi, + env.l2Wallet + ) + + L2BOBAToken = new Contract( + env.addressesBOBA.TOKENS.BOBA.L2, + L2GovernanceERC20Json.abi, + env.l2Wallet + ) + + BOBABillingContract = new Contract( + env.addressesBOBA.Proxy__BobaBillingContract, + L2BillingContractJson.abi, + env.l2Wallet + ) + }) + + describe('L1 native ERC1155 token tests', async () => { + before(async () => { + Factory__L1ERC1155 = new ContractFactory( + ERC1155Json.abi, + ERC1155Json.bytecode, + env.l1Wallet + ) + + Factory__L2ERC1155 = new ContractFactory( + L2StandardERC1155Json.abi, + L2StandardERC1155Json.bytecode, + env.l2Wallet + ) + + // deploy a L1 native token token each time if existing contracts are used for tests + L1ERC1155 = await Factory__L1ERC1155.deploy(DUMMY_URI_1) + + await L1ERC1155.deployTransaction.wait() + + L2ERC1155 = await Factory__L2ERC1155.deploy( + L2Bridge.address, + L1ERC1155.address, + DUMMY_URI_1 + ) + await L2ERC1155.deployTransaction.wait() + + // register token + const registerL1BridgeTx = await L1Bridge.registerPair( + L1ERC1155.address, + L2ERC1155.address, + 'L1' + ) + await registerL1BridgeTx.wait() + + const registerL2BridgeTx = await L2Bridge.registerPair( + L1ERC1155.address, + L2ERC1155.address, + 'L1' + ) + await registerL2BridgeTx.wait() + }) + + it('should deposit token to L2', async () => { + // mint token + const mintTx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintTx.wait() + + const preL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const preL2Balance = await L2ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + + const approveTx = await L1ERC1155.setApprovalForAll( + L1Bridge.address, + DUMMY_TOKEN_ID_1 + ) + await approveTx.wait() + + await env.waitForXDomainTransaction( + L1Bridge.deposit( + L1ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3, + '0x', + 999999 + ) + ) + + const postL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const postL2Balance = await L2ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + + expect(postL1Balance).to.deep.eq(preL1Balance.sub(DUMMY_TOKEN_AMOUNT_3)) + expect(postL2Balance).to.deep.eq(preL2Balance.add(DUMMY_TOKEN_AMOUNT_3)) + }) + + it('should be able to transfer tokens on L2', async () => { + const preOwnerBalance = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const preBalance = await L2ERC1155.balanceOf( + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + const transferTx = await L2ERC1155.safeTransferFrom( + env.l2Wallet.address, + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x' + ) + await transferTx.wait() + + const postOwnerBalance = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const postBalance = await L2ERC1155.balanceOf( + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + expect(postBalance).to.deep.eq(preBalance.add(DUMMY_TOKEN_AMOUNT_1)) + expect(preOwnerBalance).to.deep.eq( + postOwnerBalance.add(DUMMY_TOKEN_AMOUNT_1) + ) + }) + + it('should not be able to withdraw non-owned tokens', async () => { + await expect( + L2Bridge.connect(env.l2Wallet).withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3, + '0x', + 999999 + ) + ).to.be.reverted + await expect( + L2Bridge.connect(env.l2Wallet).withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_2, + DUMMY_TOKEN_AMOUNT_3, + '0x', + 999999 + ) + ).to.be.reverted + }) + + it('should fail to withdraw the token if not enough Boba balance', async () => { + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + + await expect( + L2Bridge.connect(newWallet).withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ).to.be.reverted + }) + + it('should fail to withdraw the token if not approving Boba', async () => { + await L2BOBAToken.connect(env.l2Wallet_2).approve(L2Bridge.address, 0) + await expect( + L2Bridge.connect(env.l2Wallet_2).withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ).to.be.reverted + }) + + it('should fail to withdraw the token if the amount is 0', async () => { + const approveTX = await L2ERC1155.connect( + env.l2Wallet_2 + ).setApprovalForAll(L2Bridge.address, true) + await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await expect( + L2Bridge.connect(env.l2Wallet_2).withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + 0, + '0x', + 999999 + ) + ).to.be.revertedWith('Amount should be greater than 0') + }) + + it('should be able to withdraw tokens to L1', async () => { + const preL1Balance = await L1ERC1155.balanceOf( + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + const preL2Balance = await L2ERC1155.balanceOf( + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + + const approveTX = await L2ERC1155.connect( + env.l2Wallet_2 + ).setApprovalForAll(L2Bridge.address, true) + await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + + const withdrawTx = await env.waitForXDomainTransaction( + L2Bridge.connect(env.l2Wallet_2).withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ) + + // check event WithdrawalInitiated is emitted with empty data + const returnedlogIndex = await getFilteredLogIndex( + withdrawTx.receipt, + L2ERC1155BridgeJson.abi, + L2Bridge.address, + 'WithdrawalInitiated' + ) + const ifaceL2Bridge = new ethers.utils.Interface(L2ERC1155BridgeJson.abi) + const log = ifaceL2Bridge.parseLog( + withdrawTx.receipt.logs[returnedlogIndex] + ) + + expect(log.args._data).to.deep.eq('0x') + + const postL1Balance = await L1ERC1155.balanceOf( + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + const postL2Balance = await L2ERC1155.balanceOf( + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + expect(postL2Balance).to.deep.eq(preL2Balance.sub(DUMMY_TOKEN_AMOUNT_1)) + expect(postL1Balance).to.deep.eq(preL1Balance.add(DUMMY_TOKEN_AMOUNT_1)) + }) + + it('should not be able to deposit unregistered token ', async () => { + const L1ERC721Test = await Factory__L1ERC1155.deploy(DUMMY_URI_1) + await L1ERC721Test.deployTransaction.wait() + + const mintTx = await L1ERC721Test.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1 + ) + await mintTx.wait() + const approveTx = await L1ERC721Test.setApprovalForAll( + L1Bridge.address, + true + ) + await approveTx.wait() + + await expect( + L1Bridge.deposit( + L1ERC721Test.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ).to.be.revertedWith("Can't Find L2 token Contract") + await expect( + L1Bridge.depositTo( + L1ERC721Test.address, + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ).to.be.revertedWith("Can't Find L2 token Contract") + }) + + it('should not be able to mint token on L2', async () => { + await expect( + L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_2, + DUMMY_TOKEN_AMOUNT_1, + '0x' + ) + ).to.be.revertedWith('Only L2 Bridge can mint and burn') + }) + + it('should not be able to burn token on L2', async () => { + await expect( + L2ERC1155.burn( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1 + ) + ).to.be.revertedWith('Only L2 Bridge can mint and burn') + }) + + it('should be able to deposit zero amount of token to L2', async () => { + // mint token + const mintTx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintTx.wait() + await expect( + L1Bridge.deposit(L1ERC1155.address, DUMMY_TOKEN_ID_1, 0, '0x', 999999) + ).to.be.revertedWith('Amount should be greater than 0') + }) + + it('should be able to deposit token to another wallet on L2', async () => { + const preOwnerL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const preL2Balance = await L2ERC1155.balanceOf( + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + await env.waitForXDomainTransaction( + L1Bridge.depositTo( + L1ERC1155.address, + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3, + '0x', + 999999 + ) + ) + const postOwnerL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const postL2Balance = await L2ERC1155.balanceOf( + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + expect(postOwnerL1Balance).to.deep.eq( + preOwnerL1Balance.sub(DUMMY_TOKEN_AMOUNT_3) + ) + expect(postL2Balance).to.deep.eq(preL2Balance.add(DUMMY_TOKEN_AMOUNT_3)) + }) + + it('should be able to withdraw token to another wallet on L1', async () => { + const preOwnerL2Balance = await L2ERC1155.balanceOf( + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + const preL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + + const approveTX = await L2ERC1155.connect( + env.l2Wallet_2 + ).setApprovalForAll(L2Bridge.address, true) + await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + + await env.waitForXDomainTransaction( + L2Bridge.connect(env.l2Wallet_2).withdrawTo( + L2ERC1155.address, + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ) + + const postOwnerL2Balance = await L2ERC1155.balanceOf( + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + const postL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + expect(postOwnerL2Balance).to.deep.eq( + preOwnerL2Balance.sub(DUMMY_TOKEN_AMOUNT_1) + ) + expect(postL1Balance).to.deep.eq(preL1Balance.add(DUMMY_TOKEN_AMOUNT_1)) + }) + + it('should be able to deposit a batch of tokens to L2', async () => { + const mintType1Tx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType1Tx.wait() + const mintType2Tx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_2, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType2Tx.wait() + const mintType3Tx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_3, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType3Tx.wait() + const [preL1Balance1, preL1Balance2, preL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [preL2Balance1, preL2Balance2, preL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + await env.waitForXDomainTransaction( + L1Bridge.depositBatch( + L1ERC1155.address, + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3], + [DUMMY_TOKEN_AMOUNT_3, DUMMY_TOKEN_AMOUNT_3, DUMMY_TOKEN_AMOUNT_3], + '0x', + 999999 + ) + ) + const [postL1Balance1, postL1Balance2, postL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [postL2Balance1, postL2Balance2, postL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + expect(postL2Balance1).to.deep.eq(preL2Balance1.add(DUMMY_TOKEN_AMOUNT_3)) + expect(postL2Balance2).to.deep.eq(preL2Balance2.add(DUMMY_TOKEN_AMOUNT_3)) + expect(postL2Balance3).to.deep.eq(preL2Balance3.add(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance1).to.deep.eq(preL1Balance1.sub(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance2).to.deep.eq(preL1Balance2.sub(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance3).to.deep.eq(preL1Balance3.sub(DUMMY_TOKEN_AMOUNT_3)) + }) + + it('should not deposit a batch of tokens to L2 if the amount is zero', async () => { + const mintType1Tx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType1Tx.wait() + const mintType2Tx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_2, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType2Tx.wait() + const mintType3Tx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_3, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType3Tx.wait() + + await expect( + L1Bridge.depositBatch( + L1ERC1155.address, + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3], + [DUMMY_TOKEN_AMOUNT_3, 0, DUMMY_TOKEN_AMOUNT_3], + '0x', + 999999 + ) + ).to.be.revertedWith('Amount should be greater than 0') + }) + + it('should withdraw a batch of tokens from L2 to L1', async () => { + const [preL1Balance1, preL1Balance2, preL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [preL2Balance1, preL2Balance2, preL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + + const approveTX = await L2ERC1155.connect(env.l2Wallet).setApprovalForAll( + L2Bridge.address, + true + ) + await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + + await env.waitForXDomainTransaction( + L2Bridge.withdrawBatch( + L2ERC1155.address, + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3], + [DUMMY_TOKEN_AMOUNT_1, DUMMY_TOKEN_AMOUNT_2, DUMMY_TOKEN_AMOUNT_3], + '0x', + 999999 + ) + ) + + const [postL1Balance1, postL1Balance2, postL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [postL2Balance1, postL2Balance2, postL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + expect(postL2Balance1).to.deep.eq(preL2Balance1.sub(DUMMY_TOKEN_AMOUNT_1)) + expect(postL2Balance2).to.deep.eq(preL2Balance2.sub(DUMMY_TOKEN_AMOUNT_2)) + expect(postL2Balance3).to.deep.eq(preL2Balance3.sub(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance1).to.deep.eq(preL1Balance1.add(DUMMY_TOKEN_AMOUNT_1)) + expect(postL1Balance2).to.deep.eq(preL1Balance2.add(DUMMY_TOKEN_AMOUNT_2)) + expect(postL1Balance3).to.deep.eq(preL1Balance3.add(DUMMY_TOKEN_AMOUNT_3)) + }) + + it('should be able to deposit a batch of tokens to another wallet on L2', async () => { + const mintType1Tx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType1Tx.wait() + const mintType2Tx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_2, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType2Tx.wait() + const mintType3Tx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_3, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType3Tx.wait() + const [preL1Balance1, preL1Balance2, preL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [preL2Balance1, preL2Balance2, preL2Balance3] = + await L2ERC1155.balanceOfBatch( + [ + env.l2Wallet_2.address, + env.l2Wallet_2.address, + env.l2Wallet_2.address, + ], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + await env.waitForXDomainTransaction( + L1Bridge.depositBatchTo( + L1ERC1155.address, + env.l2Wallet_2.address, + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3], + [DUMMY_TOKEN_AMOUNT_3, DUMMY_TOKEN_AMOUNT_3, DUMMY_TOKEN_AMOUNT_3], + '0x', + 999999 + ) + ) + const [postL1Balance1, postL1Balance2, postL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [postL2Balance1, postL2Balance2, postL2Balance3] = + await L2ERC1155.balanceOfBatch( + [ + env.l2Wallet_2.address, + env.l2Wallet_2.address, + env.l2Wallet_2.address, + ], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + expect(postL2Balance1).to.deep.eq(preL2Balance1.add(DUMMY_TOKEN_AMOUNT_3)) + expect(postL2Balance2).to.deep.eq(preL2Balance2.add(DUMMY_TOKEN_AMOUNT_3)) + expect(postL2Balance3).to.deep.eq(preL2Balance3.add(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance1).to.deep.eq(preL1Balance1.sub(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance2).to.deep.eq(preL1Balance2.sub(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance3).to.deep.eq(preL1Balance3.sub(DUMMY_TOKEN_AMOUNT_3)) + }) + + it('should withdraw a batch of tokens to another wallet from L2 to L1', async () => { + const [preL1Balance1, preL1Balance2, preL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [preL2Balance1, preL2Balance2, preL2Balance3] = + await L2ERC1155.balanceOfBatch( + [ + env.l2Wallet_2.address, + env.l2Wallet_2.address, + env.l2Wallet_2.address, + ], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + + const approveTX = await L2ERC1155.connect( + env.l2Wallet_2 + ).setApprovalForAll(L2Bridge.address, true) + await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + + await env.waitForXDomainTransaction( + L2Bridge.connect(env.l2Wallet_2).withdrawBatchTo( + L2ERC1155.address, + env.l1Wallet.address, + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3], + [DUMMY_TOKEN_AMOUNT_1, DUMMY_TOKEN_AMOUNT_2, DUMMY_TOKEN_AMOUNT_3], + '0x', + 999999 + ) + ) + + const [postL1Balance1, postL1Balance2, postL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [postL2Balance1, postL2Balance2, postL2Balance3] = + await L2ERC1155.balanceOfBatch( + [ + env.l2Wallet_2.address, + env.l2Wallet_2.address, + env.l2Wallet_2.address, + ], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + expect(postL2Balance1).to.deep.eq(preL2Balance1.sub(DUMMY_TOKEN_AMOUNT_1)) + expect(postL2Balance2).to.deep.eq(preL2Balance2.sub(DUMMY_TOKEN_AMOUNT_2)) + expect(postL2Balance3).to.deep.eq(preL2Balance3.sub(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance1).to.deep.eq(preL1Balance1.add(DUMMY_TOKEN_AMOUNT_1)) + expect(postL1Balance2).to.deep.eq(preL1Balance2.add(DUMMY_TOKEN_AMOUNT_2)) + expect(postL1Balance3).to.deep.eq(preL1Balance3.add(DUMMY_TOKEN_AMOUNT_3)) + }) + }) + + describe('L2 native ERC1155 token tests', async () => { + before(async () => { + Factory__L2ERC1155 = new ContractFactory( + ERC1155Json.abi, + ERC1155Json.bytecode, + env.l2Wallet + ) + + Factory__L1ERC1155 = new ContractFactory( + L1StandardERC1155Json.abi, + L1StandardERC1155Json.bytecode, + env.l1Wallet + ) + + // deploy a L2 native token token each time if existing contracts are used for tests + L2ERC1155 = await Factory__L2ERC1155.deploy(DUMMY_URI_1) + + await L2ERC1155.deployTransaction.wait() + + L1ERC1155 = await Factory__L1ERC1155.deploy( + L1Bridge.address, + L2ERC1155.address, + DUMMY_URI_1 + ) + await L1ERC1155.deployTransaction.wait() + + // register token + const registerL1BridgeTx = await L1Bridge.registerPair( + L1ERC1155.address, + L2ERC1155.address, + 'L2' + ) + await registerL1BridgeTx.wait() + + const registerL2BridgeTx = await L2Bridge.registerPair( + L1ERC1155.address, + L2ERC1155.address, + 'L2' + ) + await registerL2BridgeTx.wait() + }) + + it('should withdraw token from L2 to L1', async () => { + // mint token + const mintTx = await L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintTx.wait() + + const preL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const preL2Balance = await L2ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + + const approveTx = await L2ERC1155.setApprovalForAll( + L2Bridge.address, + DUMMY_TOKEN_ID_1 + ) + await approveTx.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.approve(L2Bridge.address, exitFee) + await approveBOBATX.wait() + + await env.waitForXDomainTransaction( + L2Bridge.withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3, + '0x', + 999999 + ) + ) + + const postL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const postL2Balance = await L2ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + + expect(postL1Balance).to.deep.eq(preL1Balance.add(DUMMY_TOKEN_AMOUNT_3)) + expect(postL2Balance).to.deep.eq(preL2Balance.sub(DUMMY_TOKEN_AMOUNT_3)) + }) + + it('should be able to transfer tokens on L1', async () => { + const preOwnerBalance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const preBalance = await L1ERC1155.balanceOf( + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + const transferTx = await L1ERC1155.safeTransferFrom( + env.l1Wallet.address, + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x' + ) + await transferTx.wait() + + const postOwnerBalance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const postBalance = await L1ERC1155.balanceOf( + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + expect(postBalance).to.deep.eq(preBalance.add(DUMMY_TOKEN_AMOUNT_1)) + expect(preOwnerBalance).to.deep.eq( + postOwnerBalance.add(DUMMY_TOKEN_AMOUNT_1) + ) + }) + + it('should not be able to deposit non-owned tokens', async () => { + await expect( + L1Bridge.connect(env.l1Wallet).deposit( + L1ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3, + '0x', + 999999 + ) + ).to.be.reverted + await expect( + L1Bridge.connect(env.l1Wallet).deposit( + L2ERC1155.address, + DUMMY_TOKEN_ID_2, + DUMMY_TOKEN_AMOUNT_3, + '0x', + 999999 + ) + ).to.be.reverted + }) + + it('should fail to deposit the token if not enough Boba balance', async () => { + const newWallet = ethers.Wallet.createRandom().connect(env.l2Provider) + await env.l2Wallet.sendTransaction({ + to: newWallet.address, + value: ethers.utils.parseEther('1'), + }) + + const mintTx = await L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintTx.wait() + + const transferTx = await L2ERC1155.safeTransferFrom( + env.l2Wallet.address, + newWallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x' + ) + await transferTx.wait() + + await expect( + L2Bridge.connect(newWallet).withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ).to.be.revertedWith( + 'execution reverted: ERC20: transfer amount exceeds balance' + ) + }) + + it('should fail to withdraw the token if not approving Boba', async () => { + const mintTx = await L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintTx.wait() + + const transferTx = await L2ERC1155.safeTransferFrom( + env.l2Wallet.address, + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x' + ) + await transferTx.wait() + + await L2ERC1155.connect(env.l2Wallet_2).setApprovalForAll( + L2Bridge.address, + true + ) + + await L2BOBAToken.connect(env.l2Wallet_2).approve(L2Bridge.address, 0) + + await expect( + L2Bridge.connect(env.l2Wallet_2).withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ).to.be.reverted + }) + + it('should fail to withdraw the token if the amount is 0', async () => { + const approveTX = await L2ERC1155.connect( + env.l2Wallet_2 + ).setApprovalForAll(L2Bridge.address, true) + await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet_2).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + await expect( + L2Bridge.connect(env.l2Wallet_2).withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + 0, + '0x', + 999999 + ) + ).to.be.revertedWith('Amount should be greater than 0') + }) + + it('should be able to deposit tokens from L1 to L2', async () => { + const preL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const preL2Balance = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + + const depositTx = await env.waitForXDomainTransaction( + L1Bridge.connect(env.l1Wallet).deposit( + L1ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ) + + // check event WithdrawalInitiated is emitted with empty data + const returnedlogIndex = await getFilteredLogIndex( + depositTx.receipt, + L1ERC1155BridgeJson.abi, + L1Bridge.address, + 'DepositInitiated' + ) + const ifaceL1Bridge = new ethers.utils.Interface(L1ERC1155BridgeJson.abi) + const log = ifaceL1Bridge.parseLog( + depositTx.receipt.logs[returnedlogIndex] + ) + + expect(log.args._data).to.deep.eq('0x') + + const postL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const postL2Balance = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + expect(postL2Balance).to.deep.eq(preL2Balance.add(DUMMY_TOKEN_AMOUNT_1)) + expect(postL1Balance).to.deep.eq(preL1Balance.sub(DUMMY_TOKEN_AMOUNT_1)) + }) + + it('should not be able to withdraw unregistered token ', async () => { + const L2ERC1155Test = await Factory__L2ERC1155.deploy(DUMMY_URI_1) + await L2ERC1155Test.deployTransaction.wait() + + const mintTx = await L2ERC1155Test.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1 + ) + await mintTx.wait() + const approveTx = await L2ERC1155Test.setApprovalForAll( + L1Bridge.address, + true + ) + await approveTx.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.approve(L2Bridge.address, exitFee) + await approveBOBATX.wait() + + await expect( + L2Bridge.withdraw( + L2ERC1155Test.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ).to.be.revertedWith("Can't Find L1 token Contract") + await expect( + L2Bridge.withdrawTo( + L2ERC1155Test.address, + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ).to.be.revertedWith("Can't Find L1 token Contract") + }) + + it('should not be able to mint token on L1', async () => { + await expect( + L1ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_2, + DUMMY_TOKEN_AMOUNT_1, + '0x' + ) + ).to.be.revertedWith('Only L1 Bridge can mint and burn') + }) + + it('should not be able to burn token on L1', async () => { + await expect( + L1ERC1155.burn( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1 + ) + ).to.be.revertedWith('Only L1 Bridge can mint and burn') + }) + + it('should be able to withdraw token to another wallet on L1', async () => { + const preOwnerL2Balance = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const preL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.approve(L2Bridge.address, exitFee) + await approveBOBATX.wait() + + await env.waitForXDomainTransaction( + L2Bridge.withdrawTo( + L2ERC1155.address, + env.l2Wallet_2.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3, + '0x', + 999999 + ) + ) + const postOwnerL2Balance = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const postL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + expect(postOwnerL2Balance).to.deep.eq( + preOwnerL2Balance.sub(DUMMY_TOKEN_AMOUNT_3) + ) + expect(postL1Balance).to.deep.eq(preL1Balance.add(DUMMY_TOKEN_AMOUNT_3)) + }) + + it('should be able to deposit token to another wallet on L2', async () => { + const preOwnerL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + const preL2Balance = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + + const approveTX = await L1ERC1155.connect( + env.l2Wallet_2 + ).setApprovalForAll(L1Bridge.address, true) + await approveTX.wait() + + await env.waitForXDomainTransaction( + L1Bridge.connect(env.l1Wallet_2).depositTo( + L1ERC1155.address, + env.l2Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ) + + const postOwnerL1Balance = await L1ERC1155.balanceOf( + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1 + ) + const postL2Balance = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + expect(postOwnerL1Balance).to.deep.eq( + preOwnerL1Balance.sub(DUMMY_TOKEN_AMOUNT_1) + ) + expect(postL2Balance).to.deep.eq(preL2Balance.add(DUMMY_TOKEN_AMOUNT_1)) + }) + + it('should withdraw a batch of tokens from L2 to L1', async () => { + const mintType1Tx = await L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType1Tx.wait() + const mintType2Tx = await L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_2, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType2Tx.wait() + const mintType3Tx = await L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_3, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType3Tx.wait() + + const [preL1Balance1, preL1Balance2, preL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [preL2Balance1, preL2Balance2, preL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + + const approveTX = await L2ERC1155.connect(env.l2Wallet).setApprovalForAll( + L2Bridge.address, + true + ) + await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + + await env.waitForXDomainTransaction( + L2Bridge.withdrawBatch( + L2ERC1155.address, + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3], + [DUMMY_TOKEN_AMOUNT_1, DUMMY_TOKEN_AMOUNT_2, DUMMY_TOKEN_AMOUNT_3], + '0x', + 999999 + ) + ) + + const [postL1Balance1, postL1Balance2, postL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [postL2Balance1, postL2Balance2, postL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + expect(postL2Balance1).to.deep.eq(preL2Balance1.sub(DUMMY_TOKEN_AMOUNT_1)) + expect(postL2Balance2).to.deep.eq(preL2Balance2.sub(DUMMY_TOKEN_AMOUNT_2)) + expect(postL2Balance3).to.deep.eq(preL2Balance3.sub(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance1).to.deep.eq(preL1Balance1.add(DUMMY_TOKEN_AMOUNT_1)) + expect(postL1Balance2).to.deep.eq(preL1Balance2.add(DUMMY_TOKEN_AMOUNT_2)) + expect(postL1Balance3).to.deep.eq(preL1Balance3.add(DUMMY_TOKEN_AMOUNT_3)) + }) + + it('should not withdraw a batch of tokens to L1 if the amount is zero', async () => { + const mintType1Tx = await L2ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType1Tx.wait() + const mintType2Tx = await L2ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_2, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType2Tx.wait() + const mintType3Tx = await L2ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_3, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType3Tx.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + + await expect( + L2Bridge.withdrawBatch( + L2ERC1155.address, + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3], + [DUMMY_TOKEN_AMOUNT_3, 0, DUMMY_TOKEN_AMOUNT_3], + '0x', + 999999 + ) + ).to.be.revertedWith('Amount should be greater than 0') + }) + + it('should be able to deposit a batch of tokens to L2', async () => { + const [preL1Balance1, preL1Balance2, preL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [preL2Balance1, preL2Balance2, preL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + await L1ERC1155.setApprovalForAll(L1Bridge.address, true) + await env.waitForXDomainTransaction( + L1Bridge.depositBatch( + L1ERC1155.address, + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3], + [DUMMY_TOKEN_AMOUNT_1, DUMMY_TOKEN_AMOUNT_2, DUMMY_TOKEN_AMOUNT_3], + '0x', + 999999 + ) + ) + const [postL1Balance1, postL1Balance2, postL1Balance3] = + await L1ERC1155.balanceOfBatch( + [env.l1Wallet.address, env.l1Wallet.address, env.l1Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [postL2Balance1, postL2Balance2, postL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + expect(postL1Balance1).to.deep.eq(preL1Balance1.sub(DUMMY_TOKEN_AMOUNT_1)) + expect(postL1Balance2).to.deep.eq(preL1Balance2.sub(DUMMY_TOKEN_AMOUNT_2)) + expect(postL1Balance3).to.deep.eq(preL1Balance3.sub(DUMMY_TOKEN_AMOUNT_3)) + expect(postL2Balance1).to.deep.eq(preL2Balance1.add(DUMMY_TOKEN_AMOUNT_1)) + expect(postL2Balance2).to.deep.eq(preL2Balance2.add(DUMMY_TOKEN_AMOUNT_2)) + expect(postL2Balance3).to.deep.eq(preL2Balance3.add(DUMMY_TOKEN_AMOUNT_3)) + }) + + it('should withdraw a batch of tokens to another wallet from L2 to L1', async () => { + const mintType1Tx = await L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType1Tx.wait() + const mintType2Tx = await L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_2, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType2Tx.wait() + const mintType3Tx = await L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_3, + DUMMY_TOKEN_AMOUNT_3 + ) + await mintType3Tx.wait() + + const [preL1Balance1, preL1Balance2, preL1Balance3] = + await L1ERC1155.balanceOfBatch( + [ + env.l1Wallet_2.address, + env.l1Wallet_2.address, + env.l1Wallet_2.address, + ], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [preL2Balance1, preL2Balance2, preL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + + const approveTX = await L2ERC1155.setApprovalForAll( + L2Bridge.address, + true + ) + await approveTX.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.approve(L2Bridge.address, exitFee) + await approveBOBATX.wait() + + await env.waitForXDomainTransaction( + L2Bridge.withdrawBatchTo( + L2ERC1155.address, + env.l1Wallet_2.address, + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3], + [DUMMY_TOKEN_AMOUNT_1, DUMMY_TOKEN_AMOUNT_2, DUMMY_TOKEN_AMOUNT_3], + '0x', + 999999 + ) + ) + + const [postL1Balance1, postL1Balance2, postL1Balance3] = + await L1ERC1155.balanceOfBatch( + [ + env.l1Wallet_2.address, + env.l1Wallet_2.address, + env.l1Wallet_2.address, + ], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [postL2Balance1, postL2Balance2, postL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + expect(postL2Balance1).to.deep.eq(preL2Balance1.sub(DUMMY_TOKEN_AMOUNT_1)) + expect(postL2Balance2).to.deep.eq(preL2Balance2.sub(DUMMY_TOKEN_AMOUNT_2)) + expect(postL2Balance3).to.deep.eq(preL2Balance3.sub(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance1).to.deep.eq(preL1Balance1.add(DUMMY_TOKEN_AMOUNT_1)) + expect(postL1Balance2).to.deep.eq(preL1Balance2.add(DUMMY_TOKEN_AMOUNT_2)) + expect(postL1Balance3).to.deep.eq(preL1Balance3.add(DUMMY_TOKEN_AMOUNT_3)) + }) + + it('should be able to deposit a batch of tokens to another wallet on L2', async () => { + const [preL1Balance1, preL1Balance2, preL1Balance3] = + await L1ERC1155.balanceOfBatch( + [ + env.l1Wallet_2.address, + env.l1Wallet_2.address, + env.l1Wallet_2.address, + ], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [preL2Balance1, preL2Balance2, preL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + await L1ERC1155.connect(env.l1Wallet_2).setApprovalForAll( + L1Bridge.address, + true + ) + await env.waitForXDomainTransaction( + L1Bridge.connect(env.l1Wallet_2).depositBatchTo( + L1ERC1155.address, + env.l2Wallet.address, + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3], + [DUMMY_TOKEN_AMOUNT_1, DUMMY_TOKEN_AMOUNT_2, DUMMY_TOKEN_AMOUNT_3], + '0x', + 999999 + ) + ) + const [postL1Balance1, postL1Balance2, postL1Balance3] = + await L1ERC1155.balanceOfBatch( + [ + env.l1Wallet_2.address, + env.l1Wallet_2.address, + env.l1Wallet_2.address, + ], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + const [postL2Balance1, postL2Balance2, postL2Balance3] = + await L2ERC1155.balanceOfBatch( + [env.l2Wallet.address, env.l2Wallet.address, env.l2Wallet.address], + [DUMMY_TOKEN_ID_1, DUMMY_TOKEN_ID_2, DUMMY_TOKEN_ID_3] + ) + expect(postL2Balance1).to.deep.eq(preL2Balance1.add(DUMMY_TOKEN_AMOUNT_1)) + expect(postL2Balance2).to.deep.eq(preL2Balance2.add(DUMMY_TOKEN_AMOUNT_2)) + expect(postL2Balance3).to.deep.eq(preL2Balance3.add(DUMMY_TOKEN_AMOUNT_3)) + expect(postL1Balance1).to.deep.eq(preL1Balance1.sub(DUMMY_TOKEN_AMOUNT_1)) + expect(postL1Balance2).to.deep.eq(preL1Balance2.sub(DUMMY_TOKEN_AMOUNT_2)) + expect(postL1Balance3).to.deep.eq(preL1Balance3.sub(DUMMY_TOKEN_AMOUNT_3)) + }) + }) + + describe('L1 native token - failing mint on L2', async () => { + before(async () => { + Factory__L1ERC1155 = new ContractFactory( + ERC1155Json.abi, + ERC1155Json.bytecode, + env.l1Wallet + ) + + Factory__L2ERC1155 = new ContractFactory( + L2ERC1155FailingMintJson.abi, + L2ERC1155FailingMintJson.bytecode, + env.l2Wallet + ) + + // deploy a L1 native token token each time if existing contracts are used for tests + L1ERC1155 = await Factory__L1ERC1155.deploy('uri') + await L1ERC1155.deployTransaction.wait() + + L2ERC1155 = await Factory__L2ERC1155.deploy( + L2Bridge.address, + L1ERC1155.address, + 'uri' + ) + await L2ERC1155.deployTransaction.wait() + + // register token + const registerL1BridgeTx = await L1Bridge.registerPair( + L1ERC1155.address, + L2ERC1155.address, + 'L1' + ) + await registerL1BridgeTx.wait() + + const registerL2BridgeTx = await L2Bridge.registerPair( + L1ERC1155.address, + L2ERC1155.address, + 'L1' + ) + await registerL2BridgeTx.wait() + }) + + it('should try deposit token to L2', async () => { + // mint token on L1 + const mintTx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1 + ) + await mintTx.wait() + + const approveTx = await L1ERC1155.setApprovalForAll( + L1Bridge.address, + true + ) + await approveTx.wait() + + const depositTx = await env.waitForXDomainTransaction( + await L1Bridge.deposit( + L1ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 999999 + ) + ) + + // submit a random l2 tx, so the relayer is unstuck for the tests + await env.l2Wallet_2.sendTransaction({ + to: env.l2Wallet_2.address, + value: utils.parseEther('0.01'), + gasLimit: 1000000, + }) + + const backTx = await env.messenger.l2Provider.getTransaction( + depositTx.remoteReceipt.transactionHash + ) + await env.waitForXDomainTransaction(backTx) + + // check event DepositFailed is emittted + const returnedlogIndex = await getFilteredLogIndex( + depositTx.remoteReceipt, + L2ERC1155BridgeJson.abi, + L2Bridge.address, + 'DepositFailed' + ) + const ifaceL2Bridge = new ethers.utils.Interface(L2ERC1155BridgeJson.abi) + const log = ifaceL2Bridge.parseLog( + depositTx.remoteReceipt.logs[returnedlogIndex] + ) + expect(log.args._tokenId).to.deep.eq(DUMMY_TOKEN_ID_1) + + const balanceL1 = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const balanceL2 = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + expect(balanceL1).to.deep.eq(DUMMY_TOKEN_AMOUNT_1) + expect(balanceL2).to.deep.eq(0) + }).timeout(100000) + }) + + describe('L2 native token - failing mint on L1', async () => { + before(async () => { + Factory__L1ERC1155 = new ContractFactory( + L1ERC1155FailingMintJson.abi, + L1ERC1155FailingMintJson.bytecode, + env.l1Wallet + ) + + Factory__L2ERC1155 = new ContractFactory( + ERC1155Json.abi, + ERC1155Json.bytecode, + env.l2Wallet + ) + + // deploy a L2 native token token each time if existing contracts are used for tests + L2ERC1155 = await Factory__L2ERC1155.deploy('uri') + await L2ERC1155.deployTransaction.wait() + + L1ERC1155 = await Factory__L1ERC1155.deploy( + L1Bridge.address, + L2ERC1155.address, + 'uri' + ) + await L1ERC1155.deployTransaction.wait() + + // register token + const registerL1BridgeTx = await L1Bridge.registerPair( + L1ERC1155.address, + L2ERC1155.address, + 'L2' + ) + await registerL1BridgeTx.wait() + + const registerL2BridgeTx = await L2Bridge.registerPair( + L1ERC1155.address, + L2ERC1155.address, + 'L2' + ) + await registerL2BridgeTx.wait() + }) + + it('should try withdraw token to L1', async () => { + // mint token on L1 + const mintTx = await L2ERC1155.mint( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1 + ) + await mintTx.wait() + + const approveTx = await L2ERC1155.setApprovalForAll( + L2Bridge.address, + true + ) + await approveTx.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.approve(L2Bridge.address, exitFee) + await approveBOBATX.wait() + + await env.waitForRevertXDomainTransactionL1( + L2Bridge.withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 9999999 + ) + ) + + const balanceL1 = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const balanceL2 = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + expect(balanceL1).to.deep.eq(0) + expect(balanceL2).to.deep.eq(DUMMY_TOKEN_AMOUNT_1) + }).timeout(100000) + }) + + describe('Bridges pause tests', async () => { + before(async () => { + Factory__L1ERC1155 = new ContractFactory( + ERC1155Json.abi, + ERC1155Json.bytecode, + env.l1Wallet + ) + + Factory__L2ERC1155 = new ContractFactory( + L2StandardERC1155Json.abi, + L2StandardERC1155Json.bytecode, + env.l2Wallet + ) + + // deploy a L1 native token token each time if existing contracts are used for tests + L1ERC1155 = await Factory__L1ERC1155.deploy('uri') + + await L1ERC1155.deployTransaction.wait() + + L2ERC1155 = await Factory__L2ERC1155.deploy( + L2Bridge.address, + L1ERC1155.address, + 'uri' + ) + + await L2ERC1155.deployTransaction.wait() + + // register token + const registerL1BridgeTx = await L1Bridge.registerPair( + L1ERC1155.address, + L2ERC1155.address, + 'L1' + ) + await registerL1BridgeTx.wait() + + const registerL2BridgeTx = await L2Bridge.registerPair( + L1ERC1155.address, + L2ERC1155.address, + 'L1' + ) + await registerL2BridgeTx.wait() + }) + + it('should pause and unpause L1 bridge', async () => { + const mintTx = await L1ERC1155.mint( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1 + ) + await mintTx.wait() + const approveTx = await L1ERC1155.setApprovalForAll( + L1Bridge.address, + true + ) + await approveTx.wait() + + const pauseL1Tx = await L1Bridge.pause() + await pauseL1Tx.wait() + + await expect( + L1Bridge.deposit( + L1ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 9999999 + ) + ).to.be.revertedWith('Pausable: paused') + + await expect( + L1Bridge.depositTo( + L1ERC1155.address, + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 9999999 + ) + ).to.be.revertedWith('Pausable: paused') + + const unpauseL1Tx = await L1Bridge.unpause() + await unpauseL1Tx.wait() + + await env.waitForXDomainTransaction( + L1Bridge.deposit( + L1ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 9999999 + ) + ) + + const balanceL1 = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const balanceL2 = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + + expect(balanceL1).to.deep.eq(0) + expect(balanceL2).to.deep.eq(DUMMY_TOKEN_AMOUNT_1) + }) + + it('should pause and unpause L2 bridge', async () => { + const approveTx = await L2ERC1155.setApprovalForAll( + L2Bridge.address, + true + ) + await approveTx.wait() + + const pauseL2Tx = await L2Bridge.pause() + await pauseL2Tx.wait() + + await expect( + L2Bridge.withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 9999999 + ) + ).to.be.revertedWith('Pausable: paused') + + await expect( + L2Bridge.withdrawTo( + L2ERC1155.address, + env.l1Wallet_2.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 9999999 + ) + ).to.be.revertedWith('Pausable: paused') + + const unpauseL2Tx = await L2Bridge.unpause() + await unpauseL2Tx.wait() + + // Approve BOBA + const exitFee = await BOBABillingContract.exitFee() + const approveBOBATX = await L2BOBAToken.connect(env.l2Wallet).approve( + L2Bridge.address, + exitFee + ) + await approveBOBATX.wait() + + await env.waitForXDomainTransaction( + L2Bridge.withdraw( + L2ERC1155.address, + DUMMY_TOKEN_ID_1, + DUMMY_TOKEN_AMOUNT_1, + '0x', + 9999999 + ) + ) + + const balanceL1 = await L1ERC1155.balanceOf( + env.l1Wallet.address, + DUMMY_TOKEN_ID_1 + ) + const balanceL2 = await L2ERC1155.balanceOf( + env.l2Wallet.address, + DUMMY_TOKEN_ID_1 + ) + expect(balanceL1).to.be.eq(DUMMY_TOKEN_AMOUNT_1) + expect(balanceL2).to.be.eq(0) + }) + + it('should not allow to pause bridges for non-owner', async () => { + await expect(L1Bridge.connect(env.l1Wallet_2).pause()).to.be.revertedWith( + 'Caller is not the owner' + ) + await expect(L2Bridge.connect(env.l2Wallet_2).pause()).to.be.revertedWith( + 'Caller is not the owner' + ) + }) + + it('should not allow to unpause bridges for non-owner', async () => { + await expect( + L1Bridge.connect(env.l1Wallet_2).unpause() + ).to.be.revertedWith('Caller is not the owner') + await expect( + L2Bridge.connect(env.l2Wallet_2).unpause() + ).to.be.revertedWith('Caller is not the owner') + }) + }) + + describe('Configuration tests', async () => { + it('should not allow to configure billing contract address for non-owner', async () => { + await expect( + L2Bridge.connect(env.l2Wallet_2).configureBillingContractAddress( + env.addressesBOBA.Proxy__BobaBillingContract + ) + ).to.be.revertedWith('Caller is not the owner') + }) + + it('should not allow to configure billing contract address to zero address', async () => { + await expect( + L2Bridge.connect(env.l2Wallet).configureBillingContractAddress( + ethers.constants.AddressZero + ) + ).to.be.revertedWith('Billing contract address cannot be zero') + }) + }) +}) diff --git a/integration-tests/test/nft_bridge.spec.ts b/integration-tests/test/nft_bridge.spec.ts index bb78dddbc8..97969fd5a3 100644 --- a/integration-tests/test/nft_bridge.spec.ts +++ b/integration-tests/test/nft_bridge.spec.ts @@ -5,8 +5,8 @@ import { Contract, ContractFactory, utils, BigNumber } from 'ethers' import { getFilteredLogIndex } from './shared/utils' -import L1NFTBridge from '@boba/contracts/artifacts/contracts/bridges/L1NFTBridge.sol/L1NFTBridge.json' -import L2NFTBridge from '@boba/contracts/artifacts/contracts/bridges/L2NFTBridge.sol/L2NFTBridge.json' +import L1NFTBridge from '@boba/contracts/artifacts/contracts/ERC721Bridges/L1NFTBridge.sol/L1NFTBridge.json' +import L2NFTBridge from '@boba/contracts/artifacts/contracts/ERC721Bridges/L2NFTBridge.sol/L2NFTBridge.json' import L1ERC721Json from '@boba/contracts/artifacts/contracts/standards/L1StandardERC721.sol/L1StandardERC721.json' import L2ERC721Json from '@boba/contracts/artifacts/contracts/standards/L2StandardERC721.sol/L2StandardERC721.json' import ERC721Json from '@boba/contracts/artifacts/contracts/test-helpers/L1ERC721.sol/L1ERC721.json' diff --git a/packages/boba/contracts/contracts/ERC1155Bridges/L1ERC1155Bridge.sol b/packages/boba/contracts/contracts/ERC1155Bridges/L1ERC1155Bridge.sol new file mode 100644 index 0000000000..4fe44e0eba --- /dev/null +++ b/packages/boba/contracts/contracts/ERC1155Bridges/L1ERC1155Bridge.sol @@ -0,0 +1,631 @@ +// SPDX-License-Identifier: MIT +// @unsupported: ovm + +/** + Note: This contract has not been audited, exercise caution when using this on mainnet + */ +pragma solidity >0.7.5; +pragma experimental ABIEncoderV2; + +/* Interface Imports */ +import { iL1ERC1155Bridge } from "./interfaces/iL1ERC1155Bridge.sol"; +import { iL2ERC1155Bridge } from "./interfaces/iL2ERC1155Bridge.sol"; +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import { IERC1155MetadataURI } from "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; + +/* Library Imports */ +import { CrossDomainEnabled } from "@eth-optimism/contracts/contracts/libraries/bridge/CrossDomainEnabled.sol"; +import { Lib_PredeployAddresses } from "@eth-optimism/contracts/contracts/libraries/constants/Lib_PredeployAddresses.sol"; +import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { ERC1155Holder } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; + +/* Contract Imports */ +import { IL1StandardERC1155 } from "../standards/IL1StandardERC1155.sol"; + +/* External Imports */ +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; + +/** + * @title L1ERC1155Bridge + * @dev The L1 ERC1155 Bridge is a contract which stores deposited L1 ERC1155 + * tokens that are in use on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits + * and listening to it for newly finalized withdrawals. + * + * Compiler used: solc + * Runtime target: EVM + */ +contract L1ERC1155Bridge is iL1ERC1155Bridge, CrossDomainEnabled, ERC1155Holder, ReentrancyGuardUpgradeable, PausableUpgradeable { + using SafeMath for uint; + + /******************************** + * External Contract References * + ********************************/ + + address public owner; + address public l2Bridge; + // Default gas value which can be overridden if more complex logic runs on L2. + uint32 public depositL2Gas; + + enum Network { L1, L2 } + + // Info of each token + struct PairTokenInfo { + address l1Contract; + address l2Contract; + Network baseNetwork; // L1 or L2 + } + + // Maps L1 token to tokenId to L2 token contract deposited for the native L1 token + mapping(address => mapping (uint256 => uint256)) public deposits; + // Maps L1 token address to tokenInfo + mapping(address => PairTokenInfo) public pairTokenInfo; + + /*************** + * Constructor * + ***************/ + + // This contract lives behind a proxy, so the constructor parameters will go unused. + constructor() + CrossDomainEnabled(address(0)) + {} + + /********************** + * Function Modifiers * + **********************/ + + modifier onlyOwner() { + require(msg.sender == owner, 'Caller is not the owner'); + _; + } + + modifier onlyInitialized() { + require(address(messenger) != address(0), "Contract has not yet been initialized"); + _; + } + + /****************** + * Initialization * + ******************/ + + /** + * @dev transfer ownership + * + * @param _newOwner new owner of this contract + */ + function transferOwnership( + address _newOwner + ) + public + onlyOwner() + onlyInitialized() + { + owner = _newOwner; + } + + /** + * @dev Configure gas. + * + * @param _depositL2Gas default finalized deposit L2 Gas + */ + function configureGas( + uint32 _depositL2Gas + ) + public + onlyOwner() + onlyInitialized() + { + depositL2Gas = _depositL2Gas; + } + + /** + * @param _l1messenger L1 Messenger address being used for cross-chain communications. + * @param _l2Bridge L2 bridge address. + */ + function initialize( + address _l1messenger, + address _l2Bridge + ) + public + initializer() + { + require(_l1messenger != address(0) && _l2Bridge != address(0), "zero address not allowed"); + messenger = _l1messenger; + l2Bridge = _l2Bridge; + owner = msg.sender; + configureGas(1400000); + + __Context_init_unchained(); + __Pausable_init_unchained(); + __ReentrancyGuard_init_unchained(); + } + + /*** + * @dev Add the new token pair to the pool + * DO NOT add the same token token more than once. + * + * @param _l1Contract L1 token contract address + * @param _l2Contract L2 token contract address + * @param _baseNetwork Network where the token contract was created + * + */ + function registerPair( + address _l1Contract, + address _l2Contract, + string memory _baseNetwork + ) + public + onlyOwner() + { + //create2 would prevent this check + //require(_l1Contract != _l2Contract, "Contracts should not be the same"); + bytes4 erc1155 = 0xd9b67a26; + require(ERC165Checker.supportsInterface(_l1Contract, erc1155), "L1 token is not ERC1155 compatible"); + bytes32 bn = keccak256(abi.encodePacked(_baseNetwork)); + bytes32 l1 = keccak256(abi.encodePacked("L1")); + bytes32 l2 = keccak256(abi.encodePacked("L2")); + // l2 token address equal to zero, then pair is not registered yet. + // use with caution, can register only once + PairTokenInfo storage pairToken = pairTokenInfo[_l1Contract]; + require(pairToken.l2Contract == address(0), "L2 token address already registered"); + // _baseNetwork can only be L1 or L2 + require(bn == l1 || bn == l2, "Invalid Network"); + Network baseNetwork; + if (bn == l1) { + baseNetwork = Network.L1; + } + else { + require(ERC165Checker.supportsInterface(_l1Contract, 0xc8a973c4), "L1 contract is not bridgable"); + baseNetwork = Network.L2; + } + + pairTokenInfo[_l1Contract] = + PairTokenInfo({ + l1Contract: _l1Contract, + l2Contract: _l2Contract, + baseNetwork: baseNetwork + }); + } + + /************** + * Depositing * + **************/ + + // /** + // * @inheritdoc iL1ERC1155Bridge + // */ + function deposit( + address _l1Contract, + uint256 _tokenId, + uint256 _amount, + bytes memory _data, + uint32 _l2Gas + ) + external + virtual + override + nonReentrant() + whenNotPaused() + { + _initiateDeposit(_l1Contract, msg.sender, msg.sender, _tokenId, _amount, _data, _l2Gas); + } + + // /** + // * @inheritdoc iL1ERC1155Bridge + // */ + function depositBatch( + address _l1Contract, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes memory _data, + uint32 _l2Gas + ) + external + virtual + override + nonReentrant() + whenNotPaused() + { + _initiateDepositBatch(_l1Contract, msg.sender, msg.sender, _tokenIds, _amounts, _data, _l2Gas); + } + + // /** + // * @inheritdoc iL1ERC1155Bridge + // */ + function depositTo( + address _l1Contract, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes memory _data, + uint32 _l2Gas + ) + external + virtual + override + nonReentrant() + whenNotPaused() + { + _initiateDeposit(_l1Contract, msg.sender, _to, _tokenId, _amount, _data, _l2Gas); + } + + // /** + // * @inheritdoc iL1ERC1155Bridge + // */ + function depositBatchTo( + address _l1Contract, + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes memory _data, + uint32 _l2Gas + ) + external + virtual + override + nonReentrant() + whenNotPaused() + { + _initiateDepositBatch(_l1Contract, msg.sender, _to, _tokenIds, _amounts, _data, _l2Gas); + } + + /** + * @dev Performs the logic for deposits by informing the L2 Deposited Token + * contract of the deposit and calling a handler to lock the L1 token. (e.g. transferFrom) + * + * @param _l1Contract Address of the L1 token contract we are depositing + * @param _from Account to pull the deposit from on L1 + * @param _to Account to give the deposit to on L2 + * @param _tokenId token Id to deposit. + * @param _amount Amount of token Id to deposit. + * @param _data Optional data for events + * @param _l2Gas Gas limit required to complete the deposit on L2. + * or encoded tokenURI, in this order of priority if user choses to send, is empty otherwise + */ + function _initiateDeposit( + address _l1Contract, + address _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes memory _data, + uint32 _l2Gas + ) + internal + { + PairTokenInfo storage pairToken = pairTokenInfo[_l1Contract]; + require(pairToken.l2Contract != address(0), "Can't Find L2 token Contract"); + + require(_amount > 0, "Amount should be greater than 0"); + + if (pairToken.baseNetwork == Network.L1) { + // This check could be bypassed by a malicious contract via initcode, + // but it takes care of the user error we want to avoid. + require(!Address.isContract(msg.sender), "Account not EOA"); + // When a deposit is initiated on L1, the L1 Bridge transfers the funds to itself for future + // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if + // _from is an EOA or address(0). + IERC1155(_l1Contract).safeTransferFrom( + _from, + address(this), + _tokenId, + _amount, + _data + ); + + // Construct calldata for _l2Contract.finalizeDeposit(_to, _amount) + bytes memory message = abi.encodeWithSelector( + iL2ERC1155Bridge.finalizeDeposit.selector, + _l1Contract, + pairToken.l2Contract, + _from, + _to, + _tokenId, + _amount, + _data + ); + + // Send calldata into L2 + sendCrossDomainMessage( + l2Bridge, + _l2Gas, + message + ); + + deposits[_l1Contract][_tokenId] += _amount; + } else { + address l2Contract = IL1StandardERC1155(_l1Contract).l2Contract(); + require(pairToken.l2Contract == l2Contract, "L2 token Contract Address Error"); + + // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L2 + // usage + uint256 balance = IL1StandardERC1155(_l1Contract).balanceOf(msg.sender, _tokenId); + require(_amount <= balance, "Amount exceeds balance"); + + IL1StandardERC1155(_l1Contract).burn(msg.sender, _tokenId, _amount); + + // Construct calldata for l2ERC1155Bridge.finalizeDeposit(_to, _amount) + bytes memory message; + + message = abi.encodeWithSelector( + iL2ERC1155Bridge.finalizeDeposit.selector, + _l1Contract, + l2Contract, + _from, + _to, + _tokenId, + _amount, + _data + ); + + // Send calldata into L2 + sendCrossDomainMessage( + l2Bridge, + _l2Gas, + message + ); + } + + emit DepositInitiated(_l1Contract, pairToken.l2Contract, _from, _to, _tokenId, _amount, _data); + } + + /** + * @dev Performs the logic for deposits by informing the L2 Deposited Token + * contract of the deposit and calling a handler to lock the L1 token. (e.g. transferFrom) + * + * @param _l1Contract Address of the L1 token contract we are depositing + * @param _from Account to pull the deposit from on L1 + * @param _to Account to give the deposit to on L2 + * @param _tokenIds token Ids to deposit. + * @param _amounts Amounts of token Id to deposit. + * @param _data Optional data for events + * @param _l2Gas Gas limit required to complete the deposit on L2. + * or encoded tokenURI, in this order of priority if user choses to send, is empty otherwise + */ + function _initiateDepositBatch( + address _l1Contract, + address _from, + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes memory _data, + uint32 _l2Gas + ) + internal + { + PairTokenInfo storage pairToken = pairTokenInfo[_l1Contract]; + require(pairToken.l2Contract != address(0), "Can't Find L2 token Contract"); + + for (uint256 i = 0; i < _amounts.length; i++) { + require(_amounts[i] > 0, "Amount should be greater than 0"); + } + + if (pairToken.baseNetwork == Network.L1) { + // This check could be bypassed by a malicious contract via initcode, + // but it takes care of the user error we want to avoid. + require(!Address.isContract(msg.sender), "Account not EOA"); + // When a deposit is initiated on L1, the L1 Bridge transfers the funds to itself for future + // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if + // _from is an EOA or address(0). + IERC1155(_l1Contract).safeBatchTransferFrom( + _from, + address(this), + _tokenIds, + _amounts, + _data + ); + + // Construct calldata for _l2Contract.finalizeDeposit(_to, _amount) + bytes memory message = abi.encodeWithSelector( + iL2ERC1155Bridge.finalizeDepositBatch.selector, + _l1Contract, + pairToken.l2Contract, + _from, + _to, + _tokenIds, + _amounts, + _data + ); + + // Send calldata into L2 + sendCrossDomainMessage( + l2Bridge, + _l2Gas, + message + ); + + for (uint256 i = 0; i < _tokenIds.length; i++) { + deposits[_l1Contract][_tokenIds[i]] += _amounts[i]; + } + } else { + address l2Contract = IL1StandardERC1155(_l1Contract).l2Contract(); + require(pairToken.l2Contract == l2Contract, "L2 token Contract Address Error"); + + IL1StandardERC1155(_l1Contract).burnBatch(msg.sender, _tokenIds, _amounts); + + // Construct calldata for l2ERC1155Bridge.finalizeDepositBatch(_to, _amount) + bytes memory message; + + message = abi.encodeWithSelector( + iL2ERC1155Bridge.finalizeDepositBatch.selector, + _l1Contract, + l2Contract, + _from, + _to, + _tokenIds, + _amounts, + _data + ); + + // Send calldata into L2 + sendCrossDomainMessage( + l2Bridge, + _l2Gas, + message + ); + } + + emit DepositBatchInitiated(_l1Contract, pairToken.l2Contract, _from, _to, _tokenIds, _amounts, _data); + } + + // /** + // * @inheritdoc iL1ERC1155Bridge + // */ + function finalizeWithdrawal( + address _l1Contract, + address _l2Contract, + address _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes memory _data + ) + external + override + onlyFromCrossDomainAccount(l2Bridge) + { + PairTokenInfo storage pairToken = pairTokenInfo[_l1Contract]; + + if (pairToken.baseNetwork == Network.L1) { + // When a withdrawal is finalized on L1, the L1 Bridge transfers the funds to the withdrawer + IERC1155(_l1Contract).safeTransferFrom(address(this), _to, _tokenId, _amount, _data); + + deposits[_l1Contract][_tokenId] -= _amount; + + emit WithdrawalFinalized(_l1Contract, _l2Contract, _from, _to, _tokenId, _amount, _data); + } else { + // replyNeeded helps store the status if a message needs to be sent back to the other layer + bool replyNeeded = false; + // Check the target token is compliant and + // verify the deposited token on L2 matches the L1 deposited token representation here + if ( + // check with interface of IL1StandardERC1155 + ERC165Checker.supportsInterface(_l1Contract, 0xc8a973c4) && + _l2Contract == IL1StandardERC1155(_l1Contract).l2Contract() + ) { + // When a deposit is finalized, we credit the account on L2 with the same amount of + // tokens. + try IL1StandardERC1155(_l1Contract).mint(_to, _tokenId, _amount, _data) { + emit WithdrawalFinalized(_l1Contract, _l2Contract, _from, _to, _tokenId, _amount, _data); + } catch { + replyNeeded = true; + } + } else { + replyNeeded = true; + } + + if (replyNeeded) { + bytes memory message = abi.encodeWithSelector( + iL2ERC1155Bridge.finalizeDeposit.selector, + _l1Contract, + _l2Contract, + _to, // switched the _to and _from here to bounce back the deposit to the sender + _from, + _tokenId, + _amount, + _data + ); + + // Send message up to L1 bridge + sendCrossDomainMessage( + l2Bridge, + depositL2Gas, + message + ); + emit WithdrawalFailed(_l1Contract, _l2Contract, _from, _to, _tokenId, _amount, _data); + } + } + } + + // /** + // * @inheritdoc iL1ERC1155Bridge + // */ + function finalizeWithdrawalBatch( + address _l1Contract, + address _l2Contract, + address _from, + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes memory _data + ) + external + override + onlyFromCrossDomainAccount(l2Bridge) + { + PairTokenInfo storage pairToken = pairTokenInfo[_l1Contract]; + + if (pairToken.baseNetwork == Network.L1) { + // remove the amount from the deposits + for (uint256 i = 0; i < _tokenIds.length; i++) { + deposits[_l1Contract][_tokenIds[i]] -= _amounts[i]; + } + + // When a withdrawal is finalized on L1, the L1 Bridge transfers the funds to the withdrawer + IERC1155(_l1Contract).safeBatchTransferFrom(address(this), _to, _tokenIds, _amounts, _data); + + emit WithdrawalBatchFinalized(_l1Contract, _l2Contract, _from, _to, _tokenIds, _amounts, _data); + } else { + // replyNeeded helps store the status if a message needs to be sent back to the other layer + bool replyNeeded = false; + // Check the target token is compliant and + // verify the deposited token on L2 matches the L1 deposited token representation here + if ( + // check with interface of IL1StandardERC1155 + ERC165Checker.supportsInterface(_l1Contract, 0xc8a973c4) && + _l2Contract == IL1StandardERC1155(_l1Contract).l2Contract() + ) { + // When a deposit is finalized, we credit the account on L2 with the same amount of + // tokens. + try IL1StandardERC1155(_l1Contract).mintBatch(_to, _tokenIds, _amounts, _data) { + emit WithdrawalBatchFinalized(_l1Contract, _l2Contract, _from, _to, _tokenIds, _amounts, _data); + } catch { + replyNeeded = true; + } + } else { + replyNeeded = true; + } + + if (replyNeeded) { + bytes memory message = abi.encodeWithSelector( + iL2ERC1155Bridge.finalizeDeposit.selector, + _l1Contract, + _l2Contract, + _to, // switched the _to and _from here to bounce back the deposit to the sender + _from, + _tokenIds, + _amounts, + _data + ); + + // Send message up to L1 bridge + sendCrossDomainMessage( + l2Bridge, + depositL2Gas, + message + ); + emit WithdrawalBatchFailed(_l1Contract, _l2Contract, _from, _to, _tokenIds, _amounts, _data); + } + } + } + + /****************** + * Pause * + ******************/ + + /** + * Pause contract + */ + function pause() external onlyOwner() { + _pause(); + } + + /** + * UnPause contract + */ + function unpause() external onlyOwner() { + _unpause(); + } +} diff --git a/packages/boba/contracts/contracts/ERC1155Bridges/L2ERC1155Bridge.sol b/packages/boba/contracts/contracts/ERC1155Bridges/L2ERC1155Bridge.sol new file mode 100644 index 0000000000..d5c9df960c --- /dev/null +++ b/packages/boba/contracts/contracts/ERC1155Bridges/L2ERC1155Bridge.sol @@ -0,0 +1,727 @@ +// SPDX-License-Identifier: MIT + +/** + Note: This contract has not been audited, exercise caution when using this on mainnet + */ +pragma solidity >0.7.5; +pragma experimental ABIEncoderV2; + +/* Interface Imports */ +import { iL1ERC1155Bridge } from "./interfaces/iL1ERC1155Bridge.sol"; +import { iL2ERC1155Bridge } from "./interfaces/iL2ERC1155Bridge.sol"; +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import { IERC1155MetadataURI } from "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; + +/* Library Imports */ +import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import { CrossDomainEnabled } from "@eth-optimism/contracts/contracts/libraries/bridge/CrossDomainEnabled.sol"; +import { ERC1155Holder } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import "@eth-optimism/contracts/contracts/libraries/constants/Lib_PredeployAddresses.sol"; + +/* Contract Imports */ +import { IL2StandardERC1155 } from "../standards/IL2StandardERC1155.sol"; + +/* External Imports */ +import '@openzeppelin/contracts/utils/math/SafeMath.sol'; +import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import "@eth-optimism/contracts/contracts/L2/predeploys/OVM_GasPriceOracle.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/* External Imports */ +import { L2BillingContract } from "../L2BillingContract.sol"; + +/** + * @title L2ERC1155Bridge + * @dev The L2 ERC1155 bridge is a contract which works together with the L1 Standard bridge to + * enable ERC1155 transitions between L1 and L2. + * This contract acts as a minter for new tokens when it hears about deposits into the L1 Standard + * bridge. + * This contract also acts as a burner of the tokens intended for withdrawal, informing the L1 + * bridge to release L1 funds. + * + * Compiler used: optimistic-solc + * Runtime target: OVM + */ + // add is interface +contract L2ERC1155Bridge is iL2ERC1155Bridge, CrossDomainEnabled, ERC1155Holder, ReentrancyGuardUpgradeable, PausableUpgradeable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + /******************************** + * External Contract References * + ********************************/ + + address public owner; + address public l1Bridge; + uint256 public extraGasRelay; + uint32 public exitL1Gas; + + enum Network { L1, L2 } + + // Info of each token + struct PairTokenInfo { + address l1Contract; + address l2Contract; + Network baseNetwork; // L1 or L2 + } + + // Maps L2 token to tokenId to L1 token contract deposited for the native L2 token + mapping(address => mapping (uint256 => uint256)) public exits; + // Maps L2 token address to tokenInfo + mapping(address => PairTokenInfo) public pairTokenInfo; + + // billing contract address + address public billingContractAddress; + /*************** + * Constructor * + ***************/ + + constructor() + CrossDomainEnabled(address(0)) + {} + + /********************** + * Function Modifiers * + **********************/ + modifier onlyOwner() { + require(msg.sender == owner || owner == address(0), 'Caller is not the owner'); + _; + } + + modifier onlyInitialized() { + require(address(messenger) != address(0), "Contract has not yet been initialized"); + _; + } + + modifier onlyWithBillingContract() { + require(billingContractAddress != address(0), "Billing contract address is not set"); + _; + } + + /** + * @dev transfer ownership + * + * @param _newOwner new owner of this contract + */ + function transferOwnership( + address _newOwner + ) + public + onlyOwner() + { + owner = _newOwner; + } + + /** + * @param _l2CrossDomainMessenger Cross-domain messenger used by this contract. + * @param _l1Bridge Address of the L1 bridge deployed to the main chain. + */ + function initialize( + address _l2CrossDomainMessenger, + address _l1Bridge + ) + public + onlyOwner() + initializer() + { + require(messenger == address(0), "Contract has already been initialized."); + require(_l2CrossDomainMessenger != address(0) && _l1Bridge != address(0), "zero address not allowed"); + messenger = _l2CrossDomainMessenger; + l1Bridge = _l1Bridge; + owner = msg.sender; + configureGas(100000); + + __Context_init_unchained(); + __Pausable_init_unchained(); + __ReentrancyGuard_init_unchained(); + } + + /** + * Configure gas. + * + * @param _exitL1Gas default finalized withdraw L1 Gas + */ + function configureGas( + uint32 _exitL1Gas + ) + public + onlyOwner() + onlyInitialized() + { + exitL1Gas = _exitL1Gas; + } + + /** + * @dev Configure billing contract address. + * + * @param _billingContractAddress billing contract address + */ + function configureBillingContractAddress( + address _billingContractAddress + ) + public + onlyOwner() + { + require(_billingContractAddress != address(0), "Billing contract address cannot be zero"); + billingContractAddress = _billingContractAddress; + } + + /*** + * @dev Add the new token pair to the pool + * DO NOT add the same token token more than once. + * + * @param _l1Contract L1 token contract address + * @param _l2Contract L2 token contract address + * @param _baseNetwork Network where the token contract was created + * + */ + function registerPair( + address _l1Contract, + address _l2Contract, + string memory _baseNetwork + ) + public + onlyOwner() + { + //create2 would prevent this check + //require(_l1Contract != _l2Contract, "Contracts should not be the same"); + bytes4 erc1155 = 0xd9b67a26; + require(ERC165Checker.supportsInterface(_l2Contract, erc1155), "L2 token is not ERC1155 compatible"); + bytes32 bn = keccak256(abi.encodePacked(_baseNetwork)); + bytes32 l1 = keccak256(abi.encodePacked("L1")); + bytes32 l2 = keccak256(abi.encodePacked("L2")); + // l1 token address equal to zero, then pair is not registered yet. + // use with caution, can register only once + PairTokenInfo storage pairToken = pairTokenInfo[_l2Contract]; + require(pairToken.l1Contract == address(0), "L1 token address already registered"); + // _baseNetwork can only be L1 or L2 + require(bn == l1 || bn == l2, "Invalid Network"); + Network baseNetwork; + if (bn == l1) { + require(ERC165Checker.supportsInterface(_l2Contract, 0x945d1710), "L2 contract is not bridgable"); + baseNetwork = Network.L1; + } + else { + baseNetwork = Network.L2; + } + + pairTokenInfo[_l2Contract] = + PairTokenInfo({ + l1Contract: _l1Contract, + l2Contract: _l2Contract, + baseNetwork: baseNetwork + }); + } + + /*************** + * Withdrawing * + ***************/ + + /** + * @inheritdoc iL2ERC1155Bridge + */ + function withdraw( + address _l2Contract, + uint256 _tokenId, + uint256 _amount, + bytes memory _data, + uint32 _l1Gas + ) + external + virtual + override + nonReentrant() + whenNotPaused() + { + _initiateWithdrawal( + _l2Contract, + msg.sender, + msg.sender, + _tokenId, + _amount, + _data, + _l1Gas + ); + } + + /** + * @inheritdoc iL2ERC1155Bridge + */ + function withdrawBatch( + address _l2Contract, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes memory _data, + uint32 _l1Gas + ) + external + virtual + override + nonReentrant() + whenNotPaused() + { + _initiateWithdrawalBatch( + _l2Contract, + msg.sender, + msg.sender, + _tokenIds, + _amounts, + _data, + _l1Gas + ); + } + + /** + * @inheritdoc iL2ERC1155Bridge + */ + function withdrawTo( + address _l2Contract, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes memory _data, + uint32 _l1Gas + ) + external + virtual + override + nonReentrant() + whenNotPaused() + { + _initiateWithdrawal( + _l2Contract, + msg.sender, + _to, + _tokenId, + _amount, + _data, + _l1Gas + ); + } + + /** + * @inheritdoc iL2ERC1155Bridge + */ + function withdrawBatchTo( + address _l2Contract, + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes memory _data, + uint32 _l1Gas + ) + external + virtual + override + nonReentrant() + whenNotPaused() + { + _initiateWithdrawalBatch( + _l2Contract, + msg.sender, + _to, + _tokenIds, + _amounts, + _data, + _l1Gas + ); + } + + /** + * @dev Performs the logic for withdrawals by burning the token and informing the L1 ERC721 Gateway + * of the withdrawal. + * @param _l2Contract Address of L2 ERC721 where withdrawal was initiated. + * @param _from Account to pull the deposit from on L2. + * @param _to Account to give the withdrawal to on L1. + * @param _tokenId id of token to withdraw. + * @param _amount Amount of the tokens to withdraw. + * @param _data Optional data for events. + * param _l1Gas Unused, but included for potential forward compatibility considerations. + * or encoded tokenURI, in this order of priority if user choses to send, is empty otherwise + */ + function _initiateWithdrawal( + address _l2Contract, + address _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes memory _data, + uint32 _l1Gas + ) + internal + onlyWithBillingContract() + { + // Collect the exit fee + L2BillingContract billingContract = L2BillingContract(billingContractAddress); + IERC20(billingContract.feeTokenAddress()).safeTransferFrom(msg.sender, billingContractAddress, billingContract.exitFee()); + + PairTokenInfo storage pairToken = pairTokenInfo[_l2Contract]; + require(pairToken.l1Contract != address(0), "Can't Find L1 token Contract"); + + require(_amount > 0, "Amount should be greater than 0"); + + if (pairToken.baseNetwork == Network.L1) { + address l1Contract = IL2StandardERC1155(_l2Contract).l1Contract(); + require(pairToken.l1Contract == l1Contract, "L1 token Contract Address Error"); + + // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L2 + // usage + // When a withdrawal is initiated, we burn the withdrawer's funds to prevent subsequent L2 + // usage + uint256 balance = IL2StandardERC1155(_l2Contract).balanceOf(msg.sender, _tokenId); + require(_amount <= balance, "Amount exceeds balance"); + + IL2StandardERC1155(_l2Contract).burn(msg.sender, _tokenId, _amount); + + // Construct calldata for l1ERC1155Bridge.finalizeWithdrawal(_to, _amount) + bytes memory message; + + message = abi.encodeWithSelector( + iL1ERC1155Bridge.finalizeWithdrawal.selector, + l1Contract, + _l2Contract, + _from, + _to, + _tokenId, + _amount, + _data + ); + + // Send message up to L1 bridge + sendCrossDomainMessage( + l1Bridge, + _l1Gas, + message + ); + } else { + // This check could be bypassed by a malicious contract via initcode, + // but it takes care of the user error we want to avoid. + require(!Address.isContract(msg.sender), "Account not EOA"); + // When a native token is withdrawn on L2, the L1 Bridge mints the funds to itself for future + // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if + // _from is an EOA or address(0). + IERC1155(_l2Contract).safeTransferFrom( + _from, + address(this), + _tokenId, + _amount, + _data + ); + + // Construct calldata for _l2Contract.finalizeDeposit(_to, _amount) + bytes memory message = abi.encodeWithSelector( + iL1ERC1155Bridge.finalizeWithdrawal.selector, + pairToken.l1Contract, + _l2Contract, + _from, + _to, + _tokenId, + _amount, + _data + ); + + // Send calldata into L2 + sendCrossDomainMessage( + l1Bridge, + _l1Gas, + message + ); + + exits[_l2Contract][_tokenId] += _amount; + } + emit WithdrawalInitiated(pairToken.l1Contract, _l2Contract, msg.sender, _to, _tokenId, _amount, _data); + } + + /** + * @dev Performs the logic for withdrawals by burning the token and informing the L1 ERC721 Gateway + * of the withdrawal. + * @param _l2Contract Address of L2 ERC721 where withdrawal was initiated. + * @param _from Account to pull the deposit from on L2. + * @param _to Account to give the withdrawal to on L1. + * @param _tokenIds ids of tokens to withdraw. + * @param _amounts Amounts of the tokens to withdraw. + * @param _data Optional data for events. + * param _l1Gas Unused, but included for potential forward compatibility considerations. + * or encoded tokenURI, in this order of priority if user choses to send, is empty otherwise + */ + function _initiateWithdrawalBatch( + address _l2Contract, + address _from, + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes memory _data, + uint32 _l1Gas + ) + internal + onlyWithBillingContract() + { + // Collect the exit fee + L2BillingContract billingContract = L2BillingContract(billingContractAddress); + IERC20(billingContract.feeTokenAddress()).safeTransferFrom(msg.sender, billingContractAddress, billingContract.exitFee()); + + PairTokenInfo storage pairToken = pairTokenInfo[_l2Contract]; + require(pairToken.l1Contract != address(0), "Can't Find L1 token Contract"); + + for (uint256 i = 0; i < _amounts.length; i++) { + require(_amounts[i] > 0, "Amount should be greater than 0"); + } + + if (pairToken.baseNetwork == Network.L1) { + address l1Contract = IL2StandardERC1155(_l2Contract).l1Contract(); + require(pairToken.l1Contract == l1Contract, "L1 token Contract Address Error"); + + IL2StandardERC1155(_l2Contract).burnBatch(msg.sender, _tokenIds, _amounts); + + // Construct calldata for l1ERC1155Bridge.finalizeWithdrawal(_to, _amount) + bytes memory message; + + message = abi.encodeWithSelector( + iL1ERC1155Bridge.finalizeWithdrawalBatch.selector, + l1Contract, + _l2Contract, + _from, + _to, + _tokenIds, + _amounts, + _data + ); + + // Send message up to L1 bridge + sendCrossDomainMessage( + l1Bridge, + _l1Gas, + message + ); + } else { + // This check could be bypassed by a malicious contract via initcode, + // but it takes care of the user error we want to avoid. + require(!Address.isContract(msg.sender), "Account not EOA"); + // When a native token is withdrawn on L2, the L1 Bridge mints the funds to itself for future + // withdrawals. safeTransferFrom also checks if the contract has code, so this will fail if + // _from is an EOA or address(0). + IERC1155(_l2Contract).safeBatchTransferFrom( + _from, + address(this), + _tokenIds, + _amounts, + _data + ); + + // Construct calldata for _l2Contract.finalizeDeposit(_to, _amount) + bytes memory message = abi.encodeWithSelector( + iL1ERC1155Bridge.finalizeWithdrawalBatch.selector, + pairToken.l1Contract, + _l2Contract, + _from, + _to, + _tokenIds, + _amounts, + _data + ); + + // Send calldata into L2 + sendCrossDomainMessage( + l1Bridge, + _l1Gas, + message + ); + + for (uint256 i = 0; i < _tokenIds.length; i++) { + exits[_l2Contract][_tokenIds[i]] += _amounts[i]; + } + } + emit WithdrawalBatchInitiated(pairToken.l1Contract, _l2Contract, msg.sender, _to, _tokenIds, _amounts, _data); + } + + /************************************ + * Cross-chain Function: Depositing * + ************************************/ + + // /** + // * @inheritdoc IL2ERC20Bridge + // */ + function finalizeDeposit( + address _l1Contract, + address _l2Contract, + address _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes memory _data + ) + external + virtual + override + onlyFromCrossDomainAccount(l1Bridge) + { + PairTokenInfo storage pairToken = pairTokenInfo[_l2Contract]; + + if (pairToken.baseNetwork == Network.L1) { + // replyNeeded helps store the status if a message needs to be sent back to the other layer + bool replyNeeded = false; + // Check the target token is compliant and + // verify the deposited token on L1 matches the L2 deposited token representation here + if ( + // check with interface of IL2StandardERC1155 + ERC165Checker.supportsInterface(_l2Contract, 0x945d1710) && + _l1Contract == IL2StandardERC1155(_l2Contract).l1Contract() + ) { + // When a deposit is finalized, we credit the account on L2 with the same amount of + // tokens. + try IL2StandardERC1155(_l2Contract).mint(_to, _tokenId, _amount, _data) { + emit DepositFinalized(_l1Contract, _l2Contract, _from, _to, _tokenId, _amount, _data); + } catch { + replyNeeded = true; + } + } else { + replyNeeded = true; + } + + if (replyNeeded) { + // 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, or maybe the l2 mint reverted + // 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. + // In either case, we stop the process here and construct a withdrawal + // message so that users can get their funds out in some cases. + // There is no way to prevent malicious token contracts altogether, but this does limit + // user error and mitigate some forms of malicious contract behavior. + bytes memory message = abi.encodeWithSelector( + iL1ERC1155Bridge.finalizeWithdrawal.selector, + _l1Contract, + _l2Contract, + _to, // switched the _to and _from here to bounce back the deposit to the sender + _from, + _tokenId, + _amount, + _data + ); + + // Send message up to L1 bridge + sendCrossDomainMessage( + l1Bridge, + exitL1Gas, + message + ); + emit DepositFailed(_l1Contract, _l2Contract, _from, _to, _tokenId, _amount, _data); + } + } else { + exits[_l2Contract][_tokenId] -= _amount; + // When a deposit is finalized on L2, the L2 Bridge transfers the token to the depositer + IERC1155(_l2Contract).safeTransferFrom( + address(this), + _to, + _tokenId, + _amount, + _data + ); + emit DepositFinalized(_l1Contract, _l2Contract, _from, _to, _tokenId, _amount, _data); + } + } + + // /** + // * @inheritdoc IL2ERC20Bridge + // */ + function finalizeDepositBatch( + address _l1Contract, + address _l2Contract, + address _from, + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes memory _data + ) + external + virtual + override + onlyFromCrossDomainAccount(l1Bridge) + { + PairTokenInfo storage pairToken = pairTokenInfo[_l2Contract]; + + if (pairToken.baseNetwork == Network.L1) { + // replyNeeded helps store the status if a message needs to be sent back to the other layer + bool replyNeeded = false; + // Check the target token is compliant and + // verify the deposited token on L1 matches the L2 deposited token representation here + if ( + // check with interface of IL2StandardERC1155 + ERC165Checker.supportsInterface(_l2Contract, 0x945d1710) && + _l1Contract == IL2StandardERC1155(_l2Contract).l1Contract() + ) { + // When a deposit is finalized, we credit the account on L2 with the same amount of + // tokens. + try IL2StandardERC1155(_l2Contract).mintBatch(_to, _tokenIds, _amounts, _data) { + emit DepositBatchFinalized(_l1Contract, _l2Contract, _from, _to, _tokenIds, _amounts, _data); + } catch { + replyNeeded = true; + } + } else { + replyNeeded = true; + } + + if (replyNeeded) { + // 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, or maybe the l2 mint reverted + // 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. + // In either case, we stop the process here and construct a withdrawal + // message so that users can get their funds out in some cases. + // There is no way to prevent malicious token contracts altogether, but this does limit + // user error and mitigate some forms of malicious contract behavior. + bytes memory message = abi.encodeWithSelector( + iL1ERC1155Bridge.finalizeWithdrawalBatch.selector, + _l1Contract, + _l2Contract, + _to, // switched the _to and _from here to bounce back the deposit to the sender + _from, + _tokenIds, + _amounts, + _data + ); + + // Send message up to L1 bridge + sendCrossDomainMessage( + l1Bridge, + exitL1Gas, + message + ); + emit DepositBatchFailed(_l1Contract, _l2Contract, _from, _to, _tokenIds, _amounts, _data); + } + } else { + // remove the amount from the exits + for (uint256 i = 0; i < _tokenIds.length; i++) { + exits[_l2Contract][_tokenIds[i]] -= _amounts[i]; + } + // When a deposit is finalized on L2, the L2 Bridge transfers the token to the depositer + IERC1155(_l2Contract).safeBatchTransferFrom( + address(this), + _to, + _tokenIds, + _amounts, + _data + ); + emit DepositBatchFinalized(_l1Contract, _l2Contract, _from, _to, _tokenIds, _amounts, _data); + } + } + + /****************** + * Pause * + ******************/ + + /** + * Pause contract + */ + function pause() external onlyOwner() { + _pause(); + } + + /** + * UnPause contract + */ + function unpause() external onlyOwner() { + _unpause(); + } +} diff --git a/packages/boba/contracts/contracts/ERC1155Bridges/interfaces/iL1ERC1155Bridge.sol b/packages/boba/contracts/contracts/ERC1155Bridges/interfaces/iL1ERC1155Bridge.sol new file mode 100644 index 0000000000..260030a31f --- /dev/null +++ b/packages/boba/contracts/contracts/ERC1155Bridges/interfaces/iL1ERC1155Bridge.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; +pragma experimental ABIEncoderV2; + +/** + * @title iL1ERC1155Bridge + */ +interface iL1ERC1155Bridge { + + event DepositInitiated ( + address indexed _l1Contract, + address indexed _l2Contract, + address indexed _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes _data + ); + + event DepositBatchInitiated ( + address indexed _l1Contract, + address indexed _l2Contract, + address indexed _from, + address _to, + uint256[] _tokenIds, + uint256[] _amounts, + bytes _data + ); + + event WithdrawalFinalized ( + address indexed _l1Contract, + address indexed _l2Contract, + address indexed _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes _data + ); + + event WithdrawalBatchFinalized ( + address indexed _l1Contract, + address indexed _l2Contract, + address indexed _from, + address _to, + uint256[] _tokenIds, + uint256[] _amounts, + bytes _data + ); + + event WithdrawalFailed ( + address indexed _l1Contract, + address indexed _l2Contract, + address indexed _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes _data + ); + + event WithdrawalBatchFailed ( + address indexed _l1Contract, + address indexed _l2Contract, + address indexed _from, + address _to, + uint256[] _tokenIds, + uint256[] _amounts, + bytes _data + ); + + function deposit( + address _l1Contract, + uint256 _tokenId, + uint256 _amount, + bytes calldata _data, + uint32 _l2Gas + ) + external; + + function depositBatch( + address _l1Contract, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes calldata _data, + uint32 _l2Gas + ) + external; + + function depositTo( + address _l1Contract, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes calldata _data, + uint32 _l2Gas + ) + external; + + function depositBatchTo( + address _l1Contract, + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes calldata _data, + uint32 _l2Gas + ) + external; + + function finalizeWithdrawal( + address _l1Contract, + address _l2Contract, + address _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes calldata _data + ) + external; + + function finalizeWithdrawalBatch( + address _l1Contract, + address _l2Contract, + address _from, + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes calldata _data + ) + external; +} diff --git a/packages/boba/contracts/contracts/ERC1155Bridges/interfaces/iL2ERC1155Bridge.sol b/packages/boba/contracts/contracts/ERC1155Bridges/interfaces/iL2ERC1155Bridge.sol new file mode 100644 index 0000000000..e392d4a98a --- /dev/null +++ b/packages/boba/contracts/contracts/ERC1155Bridges/interfaces/iL2ERC1155Bridge.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; +pragma experimental ABIEncoderV2; + +/** + * @title iL2ERC1155Bridge + */ +interface iL2ERC1155Bridge { + + // add events + event WithdrawalInitiated ( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes _data + ); + + event WithdrawalBatchInitiated ( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256[] _tokenIds, + uint256[] _amounts, + bytes _data + ); + + event DepositFinalized ( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes _data + ); + + event DepositBatchFinalized ( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256[] _tokenIds, + uint256[] _amounts, + bytes _data + ); + + event DepositFailed ( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes _data + ); + + event DepositBatchFailed ( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256[] _tokenIds, + uint256[] _amounts, + bytes _data + ); + + function withdraw( + address _l2Contract, + uint256 _tokenId, + uint256 _amount, + bytes calldata _data, + uint32 _l1Gas + ) + external; + + function withdrawBatch( + address _l2Contract, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes calldata _data, + uint32 _l1Gas + ) + external; + + function withdrawTo( + address _l2Contract, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes calldata _data, + uint32 _l1Gas + ) + external; + + function withdrawBatchTo( + address _l2Contract, + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes calldata _data, + uint32 _l1Gas + ) + external; + + function finalizeDeposit( + address _l1Contract, + address _l2Contract, + address _from, + address _to, + uint256 _tokenId, + uint256 _amount, + bytes calldata _data + ) + external; + + + function finalizeDepositBatch( + address _l1Contract, + address _l2Contract, + address _from, + address _to, + uint256[] memory _tokenIds, + uint256[] memory _amounts, + bytes calldata _data + ) + external; +} diff --git a/packages/boba/contracts/contracts/bridges/L1NFTBridge.sol b/packages/boba/contracts/contracts/ERC721Bridges/L1NFTBridge.sol similarity index 100% rename from packages/boba/contracts/contracts/bridges/L1NFTBridge.sol rename to packages/boba/contracts/contracts/ERC721Bridges/L1NFTBridge.sol diff --git a/packages/boba/contracts/contracts/bridges/L2NFTBridge.sol b/packages/boba/contracts/contracts/ERC721Bridges/L2NFTBridge.sol similarity index 100% rename from packages/boba/contracts/contracts/bridges/L2NFTBridge.sol rename to packages/boba/contracts/contracts/ERC721Bridges/L2NFTBridge.sol diff --git a/packages/boba/contracts/contracts/bridges/README.md b/packages/boba/contracts/contracts/ERC721Bridges/README.md similarity index 100% rename from packages/boba/contracts/contracts/bridges/README.md rename to packages/boba/contracts/contracts/ERC721Bridges/README.md diff --git a/packages/boba/contracts/contracts/bridges/interfaces/iL1NFTBridge.sol b/packages/boba/contracts/contracts/ERC721Bridges/interfaces/iL1NFTBridge.sol similarity index 100% rename from packages/boba/contracts/contracts/bridges/interfaces/iL1NFTBridge.sol rename to packages/boba/contracts/contracts/ERC721Bridges/interfaces/iL1NFTBridge.sol diff --git a/packages/boba/contracts/contracts/bridges/interfaces/iL2NFTBridge.sol b/packages/boba/contracts/contracts/ERC721Bridges/interfaces/iL2NFTBridge.sol similarity index 100% rename from packages/boba/contracts/contracts/bridges/interfaces/iL2NFTBridge.sol rename to packages/boba/contracts/contracts/ERC721Bridges/interfaces/iL2NFTBridge.sol diff --git a/packages/boba/contracts/contracts/bridges/interfaces/iSupportBridgeExtraData.sol b/packages/boba/contracts/contracts/ERC721Bridges/interfaces/iSupportBridgeExtraData.sol similarity index 100% rename from packages/boba/contracts/contracts/bridges/interfaces/iSupportBridgeExtraData.sol rename to packages/boba/contracts/contracts/ERC721Bridges/interfaces/iSupportBridgeExtraData.sol diff --git a/packages/boba/contracts/contracts/standards/IL1StandardERC1155.sol b/packages/boba/contracts/contracts/standards/IL1StandardERC1155.sol new file mode 100644 index 0000000000..639b5ce8c5 --- /dev/null +++ b/packages/boba/contracts/contracts/standards/IL1StandardERC1155.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; + +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +interface IL1StandardERC1155 is IERC165, IERC1155 { + function l2Contract() external returns (address); + + function mint(address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) external; + + function burn(address _from, uint256 _tokenId, uint256 _amount) external; + + function mintBatch(address _to, uint256[] memory _tokenIds, uint256[] memory _amounts, bytes memory _data) external; + + function burnBatch(address _from, uint256[] memory _tokenIds, uint256[] memory _amounts) external; + + event Mint(address indexed _account, uint256 _tokenId, uint256 _amount); + event Burn(address indexed _from, uint256 _tokenId, uint256 _amount); + + event MintBatch(address indexed _account, uint256[] _tokenIds, uint256[] _amounts); + event BurnBatch(address indexed _from, uint256[] _tokenIds, uint256[] _amounts); +} diff --git a/packages/boba/contracts/contracts/standards/IL2StandardERC1155.sol b/packages/boba/contracts/contracts/standards/IL2StandardERC1155.sol new file mode 100644 index 0000000000..606ef08933 --- /dev/null +++ b/packages/boba/contracts/contracts/standards/IL2StandardERC1155.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; + +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +interface IL2StandardERC1155 is IERC165, IERC1155 { + function l1Contract() external returns (address); + + function mint(address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) external; + + function burn(address _from, uint256 _tokenId, uint256 _amount) external; + + function mintBatch(address _to, uint256[] memory _tokenIds, uint256[] memory _amounts, bytes memory _data) external; + + function burnBatch(address _from, uint256[] memory _tokenIds, uint256[] memory _amounts) external; + + event Mint(address indexed _account, uint256 _tokenId, uint256 _amount); + event Burn(address indexed _from, uint256 _tokenId, uint256 _amount); + + event MintBatch(address indexed _account, uint256[] _tokenIds, uint256[] _amounts); + event BurnBatch(address indexed _from, uint256[] _tokenIds, uint256[] _amounts); +} diff --git a/packages/boba/contracts/contracts/standards/L1StandardERC1155.sol b/packages/boba/contracts/contracts/standards/L1StandardERC1155.sol new file mode 100644 index 0000000000..2fb04267e3 --- /dev/null +++ b/packages/boba/contracts/contracts/standards/L1StandardERC1155.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +import "./IL1StandardERC1155.sol"; + +contract L1StandardERC1155 is IL1StandardERC1155, ERC1155 { + address public override l2Contract; + address public l1Bridge; + + /** + * @param _l1Bridge Address of the L1 standard bridge. + * @param _l2Contract Address of the corresponding L2 NFT contract. + * @param _uri URI for all token types + */ + constructor( + address _l1Bridge, + address _l2Contract, + string memory _uri + ) + ERC1155(_uri) { + l2Contract = _l2Contract; + l1Bridge = _l1Bridge; + } + + modifier onlyL1Bridge { + require(msg.sender == l1Bridge, "Only L1 Bridge can mint and burn"); + _; + } + + function supportsInterface(bytes4 _interfaceId) public view override(IERC165, ERC1155) returns (bool) { + bytes4 bridgingSupportedInterface = IL1StandardERC1155.l2Contract.selector + ^ IL1StandardERC1155.mint.selector + ^ IL1StandardERC1155.burn.selector + ^ IL1StandardERC1155.mintBatch.selector + ^ IL1StandardERC1155.burnBatch.selector; + return _interfaceId == bridgingSupportedInterface || super.supportsInterface(_interfaceId); + } + + function mint(address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) public virtual override onlyL1Bridge { + _mint(_to, _tokenId, _amount, _data); + + emit Mint(_to, _tokenId, _amount); + } + + + function mintBatch(address _to, uint256[] memory _tokenIds, uint256[] memory _amounts, bytes memory _data) public virtual override onlyL1Bridge { + _mintBatch(_to, _tokenIds, _amounts, _data); + + emit MintBatch(_to, _tokenIds, _amounts); + } + + function burn(address _from, uint256 _tokenId, uint256 _amount) public virtual override onlyL1Bridge { + _burn(_from, _tokenId, _amount); + + emit Burn(_from, _tokenId, _amount); + } + + function burnBatch(address _from, uint256[] memory _tokenIds, uint256[] memory _amounts) public virtual override onlyL1Bridge { + _burnBatch(_from, _tokenIds, _amounts); + + emit BurnBatch(_from, _tokenIds, _amounts); + } +} diff --git a/packages/boba/contracts/contracts/standards/L2StandardERC1155.sol b/packages/boba/contracts/contracts/standards/L2StandardERC1155.sol new file mode 100644 index 0000000000..58ed613cfc --- /dev/null +++ b/packages/boba/contracts/contracts/standards/L2StandardERC1155.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.7.5; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +import "./IL2StandardERC1155.sol"; + +contract L2StandardERC1155 is IL2StandardERC1155, ERC1155 { + address public override l1Contract; + address public l2Bridge; + + /** + * @param _l2Bridge Address of the L2 standard bridge. + * @param _l1Contract Address of the corresponding L1 NFT contract. + * @param _uri URI for all token types + */ + constructor( + address _l2Bridge, + address _l1Contract, + string memory _uri + ) + ERC1155(_uri) { + l1Contract = _l1Contract; + l2Bridge = _l2Bridge; + } + + modifier onlyL2Bridge { + require(msg.sender == l2Bridge, "Only L2 Bridge can mint and burn"); + _; + } + + function supportsInterface(bytes4 _interfaceId) public view override(IERC165, ERC1155) returns (bool) { + bytes4 bridgingSupportedInterface = IL2StandardERC1155.l1Contract.selector + ^ IL2StandardERC1155.mint.selector + ^ IL2StandardERC1155.burn.selector + ^ IL2StandardERC1155.mintBatch.selector + ^ IL2StandardERC1155.burnBatch.selector; + return _interfaceId == bridgingSupportedInterface || super.supportsInterface(_interfaceId); + } + + function mint(address _to, uint256 _tokenId, uint256 _amount, bytes memory _data) public virtual override onlyL2Bridge { + _mint(_to, _tokenId, _amount, _data); + + emit Mint(_to, _tokenId, _amount); + } + + function mintBatch(address _to, uint256[] memory _tokenIds, uint256[] memory _amounts, bytes memory _data) public virtual override onlyL2Bridge { + _mintBatch(_to, _tokenIds, _amounts, _data); + + emit MintBatch(_to, _tokenIds, _amounts); + } + + function burn(address _from, uint256 _tokenId, uint256 _amount) public virtual override onlyL2Bridge { + _burn(_from, _tokenId, _amount); + + emit Burn(_from, _tokenId, _amount); + } + + function burnBatch(address _from, uint256[] memory _tokenIds, uint256[] memory _amounts) public virtual override onlyL2Bridge { + _burnBatch(_from, _tokenIds, _amounts); + + emit BurnBatch(_from, _tokenIds, _amounts); + } +} diff --git a/packages/boba/contracts/contracts/test-helpers/L1ERC1155.sol b/packages/boba/contracts/contracts/test-helpers/L1ERC1155.sol new file mode 100644 index 0000000000..9f4b52ff5d --- /dev/null +++ b/packages/boba/contracts/contracts/test-helpers/L1ERC1155.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.8.0; + +import '@openzeppelin/contracts/token/ERC1155/ERC1155.sol'; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title ERC1155 + * @dev A super simple ERC1155 implementation! + */ +contract L1ERC1155 is Ownable, ERC1155 { + + constructor( + string memory _uri + ) + public + ERC1155(_uri) + {} + + function mint(address _to, uint256 _tokenId, uint256 _amount) public onlyOwner { + _mint(_to, _tokenId, _amount, ""); + } + + function mintBatch(address _to, uint256[] memory _tokenIds, uint256[] memory _amounts) public onlyOwner { + _mintBatch(_to, _tokenIds, _amounts, ""); + } +} diff --git a/packages/boba/contracts/deploy/009-NFTBridges.deploy.ts b/packages/boba/contracts/deploy/009-NFTBridges.deploy.ts index 74ecb0eebd..a8fb4e5f5b 100644 --- a/packages/boba/contracts/deploy/009-NFTBridges.deploy.ts +++ b/packages/boba/contracts/deploy/009-NFTBridges.deploy.ts @@ -4,8 +4,8 @@ import { Contract, ContractFactory } from 'ethers' import { getContractFactory } from '@eth-optimism/contracts' import { registerBobaAddress } from './000-Messenger.deploy' -import L1NFTBridgeJson from '../artifacts/contracts/bridges/L1NFTBridge.sol/L1NFTBridge.json' -import L2NFTBridgeJson from '../artifacts/contracts/bridges/L2NFTBridge.sol/L2NFTBridge.json' +import L1NFTBridgeJson from '../artifacts/contracts/ERC721Bridges/L1NFTBridge.sol/L1NFTBridge.json' +import L2NFTBridgeJson from '../artifacts/contracts/ERC721Bridges/L2NFTBridge.sol/L2NFTBridge.json' let Factory__L1NFTBridge: ContractFactory let Factory__L2NFTBridge: ContractFactory diff --git a/packages/boba/contracts/deploy/010-Proxy_NFTBridges.deploy.ts b/packages/boba/contracts/deploy/010-Proxy_NFTBridges.deploy.ts index ed998db37a..ff330f2224 100644 --- a/packages/boba/contracts/deploy/010-Proxy_NFTBridges.deploy.ts +++ b/packages/boba/contracts/deploy/010-Proxy_NFTBridges.deploy.ts @@ -5,8 +5,8 @@ import { getContractFactory } from '@eth-optimism/contracts' import { registerBobaAddress } from './000-Messenger.deploy' import ProxyJson from '../artifacts/contracts/libraries/Lib_ResolvedDelegateProxy.sol/Lib_ResolvedDelegateProxy.json' -import L1NFTBridgeJson from '../artifacts/contracts/bridges/L1NFTBridge.sol/L1NFTBridge.json' -import L2NFTBridgeJson from '../artifacts/contracts/bridges/L2NFTBridge.sol/L2NFTBridge.json' +import L1NFTBridgeJson from '../artifacts/contracts/ERC721Bridges/L1NFTBridge.sol/L1NFTBridge.json' +import L2NFTBridgeJson from '../artifacts/contracts/ERC721Bridges/L2NFTBridge.sol/L2NFTBridge.json' let Factory__Proxy__L1NFTBridge: ContractFactory let Factory__Proxy__L2NFTBridge: ContractFactory diff --git a/packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts b/packages/boba/contracts/deploy/020-L2BillingContract.deploy.ts similarity index 98% rename from packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts rename to packages/boba/contracts/deploy/020-L2BillingContract.deploy.ts index bd0c60d4ed..5e2e47eef9 100644 --- a/packages/boba/contracts/deploy/019-L2BillingContract.deploy.ts +++ b/packages/boba/contracts/deploy/020-L2BillingContract.deploy.ts @@ -8,7 +8,7 @@ import ProxyJson from '../artifacts/contracts/libraries/Lib_ResolvedDelegateProx import L2LiquidityPoolJson from '../artifacts/contracts/LP/L2LiquidityPool.sol/L2LiquidityPool.json' import L2BillingContractJson from '../artifacts/contracts/L2BillingContract.sol/L2BillingContract.json' import DiscretionaryExitFeeJson from '../artifacts/contracts/DiscretionaryExitFee.sol/DiscretionaryExitFee.json' -import L2NFTBridgeJson from '../artifacts/contracts/bridges/L2NFTBridge.sol/L2NFTBridge.json' +import L2NFTBridgeJson from '../artifacts/contracts/ERC721Bridges/L2NFTBridge.sol/L2NFTBridge.json' let Factory__Proxy__L2BillingContract: ContractFactory let Factory__L2BillingContract: ContractFactory diff --git a/packages/boba/contracts/deploy/021-ERC1155Bridges.deploy.ts b/packages/boba/contracts/deploy/021-ERC1155Bridges.deploy.ts new file mode 100644 index 0000000000..3010da85f2 --- /dev/null +++ b/packages/boba/contracts/deploy/021-ERC1155Bridges.deploy.ts @@ -0,0 +1,77 @@ +/* Imports: External */ +import { DeployFunction, DeploymentSubmission } from 'hardhat-deploy/dist/types' +import { Contract, ContractFactory } from 'ethers' +import { getContractFactory } from '@eth-optimism/contracts' +import { registerBobaAddress } from './000-Messenger.deploy' + +import L1ERC1155BridgeJson from '../artifacts/contracts/ERC1155Bridges/L1ERC1155Bridge.sol/L1ERC1155Bridge.json' +import L2ERC1155BridgeJson from '../artifacts/contracts/ERC1155Bridges/L2ERC1155Bridge.sol/L2ERC1155Bridge.json' + +let Factory__L1ERC1155Bridge: ContractFactory +let Factory__L2ERC1155Bridge: ContractFactory + +let L1ERC1155Bridge: Contract +let L2ERC1155Bridge: Contract + +const deployFn: DeployFunction = async (hre) => { + const addressManager = getContractFactory('Lib_AddressManager') + .connect((hre as any).deployConfig.deployer_l1) + .attach(process.env.ADDRESS_MANAGER_ADDRESS) as any + + Factory__L1ERC1155Bridge = new ContractFactory( + L1ERC1155BridgeJson.abi, + L1ERC1155BridgeJson.bytecode, + (hre as any).deployConfig.deployer_l1 + ) + + Factory__L2ERC1155Bridge = new ContractFactory( + L2ERC1155BridgeJson.abi, + L2ERC1155BridgeJson.bytecode, + (hre as any).deployConfig.deployer_l2 + ) + + console.log('Deploying...') + + // Deploy L1 token Bridge + L1ERC1155Bridge = await Factory__L1ERC1155Bridge.deploy() + await L1ERC1155Bridge.deployTransaction.wait() + const L1ERC1155BridgeDeploymentSubmission: DeploymentSubmission = { + ...L1ERC1155Bridge, + receipt: L1ERC1155Bridge.receipt, + address: L1ERC1155Bridge.address, + abi: L1ERC1155Bridge.abi, + } + + await registerBobaAddress( + addressManager, + 'L1ERC1155Bridge', + L1ERC1155Bridge.address + ) + await hre.deployments.save( + 'L1ERC1155Bridge', + L1ERC1155BridgeDeploymentSubmission + ) + console.log(`L1ERC1155Bridge deployed to: ${L1ERC1155Bridge.address}`) + + L2ERC1155Bridge = await Factory__L2ERC1155Bridge.deploy() + await L2ERC1155Bridge.deployTransaction.wait() + const L2ERC1155BridgeDeploymentSubmission: DeploymentSubmission = { + ...L2ERC1155Bridge, + receipt: L2ERC1155Bridge.receipt, + address: L2ERC1155Bridge.address, + abi: L2ERC1155Bridge.abi, + } + await registerBobaAddress( + addressManager, + 'L2ERC1155Bridge', + L2ERC1155Bridge.address + ) + await hre.deployments.save( + 'L2ERC1155Bridge', + L2ERC1155BridgeDeploymentSubmission + ) + console.log(`L2ERC1155Bridge deployed to: ${L2ERC1155Bridge.address}`) +} + +deployFn.tags = ['L1ERC1155Bridge', 'L2ERC1155Bridge', 'required'] +export default deployFn diff --git a/packages/boba/contracts/deploy/021-Proxy__ERC1155Bridges.deploy.ts b/packages/boba/contracts/deploy/021-Proxy__ERC1155Bridges.deploy.ts new file mode 100644 index 0000000000..95b932a1e8 --- /dev/null +++ b/packages/boba/contracts/deploy/021-Proxy__ERC1155Bridges.deploy.ts @@ -0,0 +1,133 @@ +/* Imports: External */ +import { DeployFunction, DeploymentSubmission } from 'hardhat-deploy/dist/types' +import { Contract, ContractFactory, ethers } from 'ethers' +import { getContractFactory } from '@eth-optimism/contracts' +import { registerBobaAddress } from './000-Messenger.deploy' + +import ProxyJson from '../artifacts/contracts/libraries/Lib_ResolvedDelegateProxy.sol/Lib_ResolvedDelegateProxy.json' +import L1ERC1155BridgeJson from '../artifacts/contracts/ERC1155Bridges/L1ERC1155Bridge.sol/L1ERC1155Bridge.json' +import L2ERC1155BridgeJson from '../artifacts/contracts/ERC1155Bridges/L2ERC1155Bridge.sol/L2ERC1155Bridge.json' + +let Factory__Proxy__L1ERC1155Bridge: ContractFactory +let Factory__Proxy__L2ERC1155Bridge: ContractFactory + +let Proxy__L1ERC1155Bridge: Contract +let Proxy__L2ERC1155Bridge: Contract + +const deployFn: DeployFunction = async (hre) => { + const addressManager = getContractFactory('Lib_AddressManager') + .connect((hre as any).deployConfig.deployer_l1) + .attach(process.env.ADDRESS_MANAGER_ADDRESS) as any + + Factory__Proxy__L1ERC1155Bridge = new ContractFactory( + ProxyJson.abi, + ProxyJson.bytecode, + (hre as any).deployConfig.deployer_l1 + ) + + Factory__Proxy__L2ERC1155Bridge = new ContractFactory( + ProxyJson.abi, + ProxyJson.bytecode, + (hre as any).deployConfig.deployer_l2 + ) + + // Deploy proxy contracts + console.log(`'Deploying LP Proxy...`) + + const L1ERC1155Bridge = await (hre as any).deployments.get('L1ERC1155Bridge') + const L2ERC1155Bridge = await (hre as any).deployments.get('L2ERC1155Bridge') + const L1CrossDomainMessengerFastAddress = await ( + hre as any + ).deployConfig.addressManager.getAddress('Proxy__L1CrossDomainMessengerFast') + + Proxy__L1ERC1155Bridge = await Factory__Proxy__L1ERC1155Bridge.deploy( + L1ERC1155Bridge.address + ) + await Proxy__L1ERC1155Bridge.deployTransaction.wait() + const Proxy__L1ERC1155BridgeDeploymentSubmission: DeploymentSubmission = { + ...Proxy__L1ERC1155Bridge, + receipt: Proxy__L1ERC1155Bridge.receipt, + address: Proxy__L1ERC1155Bridge.address, + abi: Proxy__L1ERC1155Bridge.abi, + } + + console.log( + `Proxy__L1ERC1155Bridge deployed to: ${Proxy__L1ERC1155Bridge.address}` + ) + + Proxy__L2ERC1155Bridge = await Factory__Proxy__L2ERC1155Bridge.deploy( + L2ERC1155Bridge.address + ) + await Proxy__L2ERC1155Bridge.deployTransaction.wait() + const Proxy__L2ERC1155BridgeDeploymentSubmission: DeploymentSubmission = { + ...Proxy__L2ERC1155Bridge, + receipt: Proxy__L2ERC1155Bridge.receipt, + address: Proxy__L2ERC1155Bridge.address, + abi: Proxy__L2ERC1155Bridge.abi, + } + console.log( + `Proxy__L2ERC1155Bridge deployed to: ${Proxy__L2ERC1155Bridge.address}` + ) + + Proxy__L1ERC1155Bridge = new ethers.Contract( + Proxy__L1ERC1155Bridge.address, + L1ERC1155BridgeJson.abi, + (hre as any).deployConfig.deployer_l1 + ) + + const initL1BridgeTX = await Proxy__L1ERC1155Bridge.initialize( + (hre as any).deployConfig.l1MessengerAddress, + Proxy__L2ERC1155Bridge.address + ) + await initL1BridgeTX.wait() + console.log(`Proxy__L1ERC1155Bridge initialized: ${initL1BridgeTX.hash}`) + + Proxy__L2ERC1155Bridge = new ethers.Contract( + Proxy__L2ERC1155Bridge.address, + L2ERC1155BridgeJson.abi, + (hre as any).deployConfig.deployer_l2 + ) + + const initL2BridgeTX = await Proxy__L2ERC1155Bridge.initialize( + (hre as any).deployConfig.l2MessengerAddress, + Proxy__L1ERC1155Bridge.address + ) + await initL2BridgeTX.wait() + console.log(`Proxy__L2ERC1155Bridge initialized: ${initL2BridgeTX.hash}`) + + const Proxy__BobaBillingContractDeployment = await hre.deployments.getOrNull( + 'Proxy__BobaBillingContract' + ) + + const configureBillingAddrTx = + await Proxy__L2ERC1155Bridge.configureBillingContractAddress( + Proxy__BobaBillingContractDeployment.address + ) + await configureBillingAddrTx.wait() + console.log( + `Proxy__L2ERC1155Bridge configured the billing address: ${configureBillingAddrTx.hash}` + ) + + await hre.deployments.save( + 'Proxy__L1ERC1155Bridge', + Proxy__L1ERC1155BridgeDeploymentSubmission + ) + await hre.deployments.save( + 'Proxy__L2ERC1155Bridge', + Proxy__L2ERC1155BridgeDeploymentSubmission + ) + await registerBobaAddress( + addressManager, + 'Proxy__L1ERC1155Bridge', + Proxy__L1ERC1155Bridge.address + ) + await registerBobaAddress( + addressManager, + 'Proxy__L2ERC1155Bridge', + Proxy__L2ERC1155Bridge.address + ) +} + +deployFn.tags = ['Proxy__L1ERC1155Bridge', 'Proxy__L2ERC1155Bridge', 'required'] + +export default deployFn diff --git a/packages/boba/contracts/deploy/020-Dump.deploy.ts b/packages/boba/contracts/deploy/022-Dump.deploy.ts similarity index 100% rename from packages/boba/contracts/deploy/020-Dump.deploy.ts rename to packages/boba/contracts/deploy/022-Dump.deploy.ts diff --git a/packages/boba/contracts/test/contracts/ERC1155Bridges/l1ERC1155Bridges.spec.ts b/packages/boba/contracts/test/contracts/ERC1155Bridges/l1ERC1155Bridges.spec.ts new file mode 100644 index 0000000000..2962b14d01 --- /dev/null +++ b/packages/boba/contracts/test/contracts/ERC1155Bridges/l1ERC1155Bridges.spec.ts @@ -0,0 +1,206 @@ +/* External Imports */ +import { ethers } from 'hardhat' +import { Signer, Contract } from 'ethers' +import { getContractFactory } from '@eth-optimism/contracts' +import { expect } from '../../setup' + +let L1ERC1155Bridge: Contract +let L2ERC1155Bridge: Contract +let L1CrossDomainMessenger: Contract +let L1StandardERC1155: Contract +let L2StandardERC1155: Contract +let ERC1155: Contract +const deployERC1155 = async (uri): Promise => { + return (await ethers.getContractFactory('ERC1155')).deploy(uri) +} +// eslint-disable-next-line prettier/prettier +const deployL2StandardERC11555 = async (l2Bridge, l1Contract, uri): Promise => { + // eslint-disable-next-line prettier/prettier + return (await ethers.getContractFactory('L2StandardERC1155')).deploy(l2Bridge, l1Contract, uri) +} +// eslint-disable-next-line prettier/prettier +const deployL1StandardERC1155 = async (l1Bridge, l2Contract, uri): Promise => { + // eslint-disable-next-line prettier/prettier + return (await ethers.getContractFactory('L1StandardERC1155')).deploy(l1Bridge, l2Contract, uri) +} +const deployL1ERC1155Bridge = async (): Promise => { + return (await ethers.getContractFactory('L1ERC1155Bridge')).deploy() +} +const deployL2ERC1155Bridge = async (): Promise => { + return (await ethers.getContractFactory('L2ERC1155Bridge')).deploy() +} +const deployL1CrossDomainMessenger = async (): Promise => { + const signer: Signer = (await ethers.getSigners())[0] + return ( + await getContractFactory('L1CrossDomainMessenger').connect(signer) + ).deploy() +} + +describe('L1ERC1155Bridge Tests', () => { + describe('L1ERC1155Bridge ownership', () => { + beforeEach(async () => { + L1ERC1155Bridge = await deployL1ERC1155Bridge() + L2ERC1155Bridge = await deployL2ERC1155Bridge() + L1CrossDomainMessenger = await deployL1CrossDomainMessenger() + }) + it('should NOT be able to change the owner', async () => { + const oldOwner = '0x0000000000000000000000000000000000000000' + const newOwner = '0x0000000000000000000000000000000000000001' + expect(await L1ERC1155Bridge.owner()).to.be.equal(oldOwner) + await expect( + L1ERC1155Bridge.transferOwnership(newOwner) + ).to.be.revertedWith('Caller is not the owner') + }) + it('changing gas reverts on not initialized', async () => { + const newGas = 1 + await expect(L1ERC1155Bridge.configureGas(newGas)).to.be.revertedWith( + 'Caller is not the owner' + ) + }) + }) + + describe('L1ERC1155Bridge tests initialized', () => { + beforeEach(async () => { + L1ERC1155Bridge = await deployL1ERC1155Bridge() + L2ERC1155Bridge = await deployL2ERC1155Bridge() + L1CrossDomainMessenger = await deployL1CrossDomainMessenger() + }) + + it('should be able to initialize and change the gas', async () => { + const magicGas = 1400000 + await L1ERC1155Bridge.initialize( + L1CrossDomainMessenger.address, + L2ERC1155Bridge.address + ) + expect(await L1ERC1155Bridge.l2Bridge()).to.be.equal( + L2ERC1155Bridge.address + ) + expect(await L1ERC1155Bridge.messenger()).to.be.equal( + L1CrossDomainMessenger.address + ) + const signer: Signer = (await ethers.getSigners())[0] + expect(await L1ERC1155Bridge.owner()).to.be.equal( + await signer.getAddress() + ) + expect(await L1ERC1155Bridge.depositL2Gas()).to.be.equal(magicGas) + // now test gas change + expect(await L1ERC1155Bridge.configureGas(magicGas + 1)) + expect(await L1ERC1155Bridge.depositL2Gas()).to.be.equal(magicGas + 1) + }) + + it('should not be able to init twice', async () => { + const signer: Signer = (await ethers.getSigners())[1] + await expect( + L1ERC1155Bridge.initialize( + L2ERC1155Bridge.address, + L1CrossDomainMessenger.address + ) + ) + await expect( + L1ERC1155Bridge.connect(signer).initialize( + L2ERC1155Bridge.address, + L1CrossDomainMessenger.address, + { from: await signer.getAddress() } + ) + ).to.be.revertedWith('Initializable: contract is already initialized') + }) + + it('should not be able to init with zero address messenger', async () => { + await expect( + L1ERC1155Bridge.initialize( + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000001' + ) + ).to.be.revertedWith('zero address not allowed') + }) + it('should not be able to init with zero address L2ERC1155Bridge', async () => { + await expect( + L1ERC1155Bridge.initialize( + '0x0000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000' + ) + ).to.be.revertedWith('zero address not allowed') + }) + }) + + describe('cover registerPair', () => { + beforeEach(async () => { + L1ERC1155Bridge = await deployL1ERC1155Bridge() + L2ERC1155Bridge = await deployL2ERC1155Bridge() + L1CrossDomainMessenger = await deployL1CrossDomainMessenger() + ERC1155 = await deployERC1155('uri') + L2StandardERC1155 = await deployL2StandardERC11555( + L2ERC1155Bridge.address, + ERC1155.address, + 'uri' + ) + L1StandardERC1155 = await deployL1StandardERC1155( + L1ERC1155Bridge.address, + ERC1155.address, + 'uri' + ) + await L1ERC1155Bridge.initialize( + L1CrossDomainMessenger.address, + L2ERC1155Bridge.address + ) + }) + it('can register a token pair with L1 creation', async () => { + const l1 = 0 + await L1ERC1155Bridge.registerPair( + ERC1155.address, + L2StandardERC1155.address, + 'L1' + ) + const pairInfo = await L1ERC1155Bridge.pairTokenInfo(ERC1155.address) + expect(pairInfo.l1Contract).eq(ERC1155.address) + expect(pairInfo.l2Contract).eq(L2StandardERC1155.address) + expect(pairInfo.baseNetwork).eq(l1) + }) + it('can not register a NFT twice', async () => { + await L1ERC1155Bridge.registerPair( + ERC1155.address, + L2StandardERC1155.address, + 'L1' + ) + await expect( + L1ERC1155Bridge.registerPair( + ERC1155.address, + L2StandardERC1155.address, + 'L1' + ) + ).to.be.revertedWith('L2 token address already registered') + }) + it('can register a token with L2 creation', async () => { + const l2 = 1 + await L1ERC1155Bridge.registerPair( + L1StandardERC1155.address, + ERC1155.address, + 'L2' + ) + const pairInfo = await L1ERC1155Bridge.pairTokenInfo( + L1StandardERC1155.address + ) + expect(pairInfo.l1Contract).eq(L1StandardERC1155.address) + expect(pairInfo.l2Contract).eq(ERC1155.address) + expect(pairInfo.baseNetwork).eq(l2) + }) + it('cant register a token with faulty base network', async () => { + await expect( + L1ERC1155Bridge.registerPair(ERC1155.address, ERC1155.address, 'L211') + ).to.be.revertedWith('Invalid Network') + }) + it('cant register if not owner', async () => { + const signer: Signer = (await ethers.getSigners())[1] + await expect( + L1ERC1155Bridge.connect(signer).registerPair( + ERC1155.address, + ERC1155.address, + 'L2', + { + from: await signer.getAddress(), + } + ) + ).to.be.revertedWith('Caller is not the owner') + }) + }) +}) diff --git a/packages/boba/contracts/test/contracts/ERC1155Bridges/l2ERC1155Bridges.spec.ts b/packages/boba/contracts/test/contracts/ERC1155Bridges/l2ERC1155Bridges.spec.ts new file mode 100644 index 0000000000..aa99c0e3d2 --- /dev/null +++ b/packages/boba/contracts/test/contracts/ERC1155Bridges/l2ERC1155Bridges.spec.ts @@ -0,0 +1,228 @@ +/* External Imports */ +import { ethers } from 'hardhat' +import { Signer, Contract } from 'ethers' +import { getContractFactory } from '@eth-optimism/contracts' +import { expect } from '../../setup' + +let L1ERC1155Bridge: Contract +let L2ERC1155Bridge: Contract +let L1CrossDomainMessenger: Contract +let L2CrossDomainMessenger: Contract +let L1StandardERC1155: Contract +let L2StandardERC1155: Contract +let ERC1155: Contract +const deployERC1155 = async (uri): Promise => { + return (await ethers.getContractFactory('ERC1155')).deploy(uri) +} +// eslint-disable-next-line prettier/prettier +const deployL2StandardERC11555 = async (l2Bridge, l1Contract, uri): Promise => { + // eslint-disable-next-line prettier/prettier + return (await ethers.getContractFactory('L2StandardERC1155')).deploy(l2Bridge, l1Contract, uri) +} +// eslint-disable-next-line prettier/prettier +const deployL1StandardERC1155 = async (l1Bridge, l2Contract, uri): Promise => { + // eslint-disable-next-line prettier/prettier + return (await ethers.getContractFactory('L1StandardERC1155')).deploy(l1Bridge, l2Contract, uri) +} +const deployL1ERC1155Bridge = async (): Promise => { + return (await ethers.getContractFactory('L1ERC1155Bridge')).deploy() +} +const deployL2ERC1155Bridge = async (): Promise => { + return (await ethers.getContractFactory('L2ERC1155Bridge')).deploy() +} +const deployL1CrossDomainMessenger = async (): Promise => { + const signer: Signer = (await ethers.getSigners())[0] + return ( + await getContractFactory('L1CrossDomainMessenger').connect(signer) + ).deploy() +} +const deployL2CrossDomainMessenger = async ( + l1CrossDomainMessenger +): Promise => { + const signer: Signer = (await ethers.getSigners())[0] + return ( + await getContractFactory('L2CrossDomainMessenger').connect(signer) + ).deploy(l1CrossDomainMessenger) +} + +describe('L2ERC1155Bridge Tests', () => { + describe('L2ERC1155Bridge ownership', () => { + beforeEach(async () => { + L1ERC1155Bridge = await deployL1ERC1155Bridge() + L2ERC1155Bridge = await deployL2ERC1155Bridge() + L1CrossDomainMessenger = await deployL1CrossDomainMessenger() + L2CrossDomainMessenger = await deployL2CrossDomainMessenger( + L1CrossDomainMessenger.address + ) + await L2ERC1155Bridge.initialize( + L2CrossDomainMessenger.address, + L2ERC1155Bridge.address + ) + }) + it('should NOT be able to change the owner', async () => { + const oldOwner = (await ethers.getSigners())[0] + const newOwner = (await ethers.getSigners())[1] + expect(await L2ERC1155Bridge.owner()).to.be.equal(oldOwner.address) + await expect( + L2ERC1155Bridge.connect(newOwner).transferOwnership(newOwner.address) + ).to.be.revertedWith('Caller is not the owner') + }) + it('changing gas reverts on not initialized', async () => { + const newGas = 1 + await expect( + L2ERC1155Bridge.connect((await ethers.getSigners())[1]).configureGas( + newGas + ) + ).to.be.revertedWith('Caller is not the owner') + }) + }) + + describe('L2ERC1155Bridge tests initialized', () => { + beforeEach(async () => { + L1ERC1155Bridge = await deployL1ERC1155Bridge() + L2ERC1155Bridge = await deployL2ERC1155Bridge() + L1CrossDomainMessenger = await deployL1CrossDomainMessenger() + L2CrossDomainMessenger = await deployL2CrossDomainMessenger( + L1CrossDomainMessenger.address + ) + }) + + it('should be able to initialize and change the gas', async () => { + const magicGas = 100000 + await L2ERC1155Bridge.initialize( + L2CrossDomainMessenger.address, + L1ERC1155Bridge.address + ) + expect(await L2ERC1155Bridge.l1Bridge()).to.be.equal( + L1ERC1155Bridge.address + ) + expect(await L2ERC1155Bridge.messenger()).to.be.equal( + L2CrossDomainMessenger.address + ) + const signer: Signer = (await ethers.getSigners())[0] + expect(await L2ERC1155Bridge.owner()).to.be.equal( + await signer.getAddress() + ) + expect(await L2ERC1155Bridge.exitL1Gas()).to.be.equal(magicGas) + // now test gas change + expect(await L2ERC1155Bridge.configureGas(magicGas + 1)) + expect(await L2ERC1155Bridge.exitL1Gas()).to.be.equal(magicGas + 1) + }) + + it('should not be able to init twice', async () => { + await expect( + L2ERC1155Bridge.initialize( + L2CrossDomainMessenger.address, + L2ERC1155Bridge.address + ) + ) + await expect( + L2ERC1155Bridge.initialize( + L2CrossDomainMessenger.address, + L2ERC1155Bridge.address + ) + ).to.be.revertedWith('Initializable: contract is already initialized') + }) + + it('should not be able to init with zero address messenger', async () => { + await expect( + L2ERC1155Bridge.initialize( + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000001' + ) + ).to.be.revertedWith('zero address not allowed') + }) + it('should not be able to init with zero address L2ERC1155Bridge', async () => { + await expect( + L2ERC1155Bridge.initialize( + '0x0000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000' + ) + ).to.be.revertedWith('zero address not allowed') + }) + }) + + describe('cover registerPair', () => { + beforeEach(async () => { + L1ERC1155Bridge = await deployL1ERC1155Bridge() + L2ERC1155Bridge = await deployL2ERC1155Bridge() + L1CrossDomainMessenger = await deployL1CrossDomainMessenger() + L2CrossDomainMessenger = await deployL2CrossDomainMessenger( + L1CrossDomainMessenger.address + ) + ERC1155 = await deployERC1155('uri') + L2StandardERC1155 = await deployL2StandardERC11555( + L2ERC1155Bridge.address, + ERC1155.address, + 'uri' + ) + L1StandardERC1155 = await deployL1StandardERC1155( + L1ERC1155Bridge.address, + ERC1155.address, + 'uri' + ) + await L2ERC1155Bridge.initialize( + L2CrossDomainMessenger.address, + L2ERC1155Bridge.address + ) + }) + it('can register a token with L1 creation', async () => { + const l1 = 0 + await L2ERC1155Bridge.registerPair( + ERC1155.address, + L2StandardERC1155.address, + 'L1' + ) + const pairTokenInfo = await L2ERC1155Bridge.pairTokenInfo( + L2StandardERC1155.address + ) + expect(pairTokenInfo.l1Contract).eq(ERC1155.address) + expect(pairTokenInfo.l2Contract).eq(L2StandardERC1155.address) + expect(pairTokenInfo.baseNetwork).eq(l1) + }) + it('can not register a token twice', async () => { + await L2ERC1155Bridge.registerPair( + ERC1155.address, + L2StandardERC1155.address, + 'L1' + ) + await expect( + L2ERC1155Bridge.registerPair( + ERC1155.address, + L2StandardERC1155.address, + 'L1' + ) + ).to.be.revertedWith('L1 token address already registered') + }) + it('can register a token with L2 creation', async () => { + const l2 = 1 + await L2ERC1155Bridge.registerPair( + L1StandardERC1155.address, + ERC1155.address, + 'L2' + ) + const pairTokenInfo = await L2ERC1155Bridge.pairTokenInfo(ERC1155.address) + expect(pairTokenInfo.l1Contract).eq(L1StandardERC1155.address) + expect(pairTokenInfo.l2Contract).eq(ERC1155.address) + expect(pairTokenInfo.baseNetwork).eq(l2) + }) + it('cant register a token with faulty base network', async () => { + await expect( + L2ERC1155Bridge.registerPair(ERC1155.address, ERC1155.address, 'L211') + ).to.be.revertedWith('Invalid Network') + }) + it('cant register if not owner', async () => { + const signer: Signer = (await ethers.getSigners())[1] + await expect( + L2ERC1155Bridge.connect(signer).registerPair( + ERC1155.address, + ERC1155.address, + 'L2', + { + from: await signer.getAddress(), + } + ) + ).to.be.revertedWith('Caller is not the owner') + }) + }) +}) diff --git a/packages/boba/contracts/test/contracts/bridges/l1nftbridge.spec.ts b/packages/boba/contracts/test/contracts/ERC721Bridges/l1nftbridge.spec.ts similarity index 100% rename from packages/boba/contracts/test/contracts/bridges/l1nftbridge.spec.ts rename to packages/boba/contracts/test/contracts/ERC721Bridges/l1nftbridge.spec.ts diff --git a/packages/boba/contracts/test/contracts/bridges/l2nftbridge.spec.ts b/packages/boba/contracts/test/contracts/ERC721Bridges/l2nftbridge.spec.ts similarity index 100% rename from packages/boba/contracts/test/contracts/bridges/l2nftbridge.spec.ts rename to packages/boba/contracts/test/contracts/ERC721Bridges/l2nftbridge.spec.ts diff --git a/packages/message-relayer/src/service.ts b/packages/message-relayer/src/service.ts index 24d370926a..61d3b58f65 100644 --- a/packages/message-relayer/src/service.ts +++ b/packages/message-relayer/src/service.ts @@ -536,6 +536,7 @@ export class MessageRelayerService extends BaseService { const relayerFilterSelect = [ filter.Proxy__L1StandardBridge, filter.Proxy__L1NFTBridge, + filter.Proxy__L1ERC1155Bridge, ] this.state.lastFilterPollingTimestamp = new Date().getTime()