BOBA NFT bridges consists of two bridge contracts. The L1NFTBridge contract is deployed on L1 and the L2NFTBridge contract is deployed on L2. It supports native L1 NFTs and native L2 NFTs to be moved back and forth.
Native L1 NFT: the original NFT contract was deployed on L1
Native L2 NFT: the original NFT contract was deployed on L2
Bridging an NFT to Boba takes several minutes, and bridging an NFT from Boba to Ethereum takes 7 days. Not all NFTs are bridgeable - developers must use specialized NFT contracts (e.g. L2StandardERC721.sol) to enable this functionality.
When deploying your L2StandardERC721, please take caution if you extend the contract with more features, as an incorrect implementation may result in loss of tokens. For instance, do not add a method that would allow updating the corresponding 'l1Contract' address for an L2StandardERC721. An update in between operation would deem the previous tokens to be locked on the bridge. Furthermore, The NFTBridge contracts use the information at the time of registration to obtain the l1Token information and send messages between the bridges.
Assuming you have already deployed an NFT contract on L1, and you wish to transfer those NFTs to L2, please make sure that your L1 NFT contract is ERC721 compatible. Your contract must implement ERC165 and ERC721 interfaces. We will check the interface before registering your NFT contracts to our bridges.
bytes4 erc721 =0x80ac58cd;require(ERC165Checker.supportsInterface(_l1Contract, erc721),"L1 NFT is not ERC721 compatible");
After verifying the interface, please deploy L2StandardERC721 on Boba. The L1_NFT_CONTRACT_ADDRESS is the address of your NFT on Ethereum.
constFactory__L2StandardERC721=newethers.ContractFactory(L2StandardERC721.abi,L2StandardERC721.bytecode, L2Wallet)constL2StandardERC721=awaitFactory__L2StandardERC721.deploy(L2_NFT_BRIDGE_ADDRESS,// L2 NFT Bridge AddressL1_NFT_CONTRACT_ADDRESS,// Your L1 NFT AddressNFT_NAME,NFT_SYMBOL,BASE_URI)awaitL2StandardERC721.deployTransaction.wait()
If you want to deploy your own L2 NFT contract, please follow requirements:
Your L2 NFT contract must be ERC721 compatible and implemented ERC165 and ERC721 interfaces.
The mint function in your L2 NFT contract should be overriden by.
The input must be address _to, uint256 _tokenId, bytes memory _data, even though you might need them all.
In your L2 NFT contract, you must add the following code to bypass our interface check in our NFT bridge
pragmasolidity >0.7.5;import"@openzeppelin/contracts/token/ERC721/ERC721.sol";import"./IL2StandardERC721.sol";contractL2StandardERC721isIL2StandardERC721, ERC721 {// [This is mandatory]// This is your L1 NFT contract address. Only one pair of your NFT contracts can be registered in the NFT bridges.addresspublicoverride l1Contract;// [This is not mandatory] You can use other names or other ways to only allow l2 NFT bridge to mint and burn tokens// This is L2 NFT brigde contract addressaddresspublic l2Bridge;// [This is not mandatory]// Only l2Brigde (L2 NFT bridge) can mint or burn NFTsmodifieronlyL2Bridge {require(msg.sender == l2Bridge,"Only L2 Bridge can mint and burn"); _; }// [This is mandatory]// You must export this interface `IL2StandardERC721.l1Contract.selector`functionsupportsInterface(bytes4_interfaceId) publicviewoverride(IERC165,ERC721) returns (bool) {bytes4 bridgingSupportedInterface = IL2StandardERC721.l1Contract.selector^ IL2StandardERC721.mint.selector^ IL2StandardERC721.burn.selector;return _interfaceId == bridgingSupportedInterface || super.supportsInterface(_interfaceId); }// [The input is mandatory] The input must be `address _to, uint256 _tokenId, bytes memory _data`// [SECURITY] Make sure that only L2 NFT bridge can mint tokensfunctionmint(address_to,uint256_tokenId,bytesmemory_data) publicvirtualoverrideonlyL2Bridge {_mint(_to, _tokenId);emitMint(_to, _tokenId); }// [SECURITY] Make sure that only L2 NFT bridge can burn tokensfunctionburn(uint256_tokenId) publicvirtualoverrideonlyL2Bridge {_burn(_tokenId);emitBurn(_tokenId); }}
NOTE: Once you have your L2 NFT contract address, please contact us so we can register that address in the L1 and L2 NFT bridges.
Deploy your NFT on Boba and then deploy L1StandardERC721 on Ethereum. The L2_NFT_CONTRACT_ADDRESS is the address of your NFT on Boba.
constFactory__L1StandardERC721=newethers.ContractFactory(L1StandardERC721.abi,L1StandardERC721.bytecode, L1Wallet)constL1StandardERC721=awaitFactory__L1StandardERC721.deploy(L1_NFT_BRIDGE_ADDRESS,// L1 NFT Bridge AddressL2_NFT_CONTRACT_ADDRESS,// Your L2 NFT AddressNFT_NAME,NFT_SYMBOL,BASE_URI)awaitL2StandardERC721.deployTransaction.wait()
If you want to deploy your own L1 NFT contract, please follow requirements:
Your L1 NFT contract must be ERC721 compatible and implemented ERC165 and ERC721 interfaces.
The mint function in your L1 NFT contract should be overriden by.
The input must be address _to, uint256 _tokenId, bytes memory _data, even though you might need them all.
In your L1 NFT contract, you must add the following code to bypass our interface check in our NFT bridge
pragmasolidity >0.7.5;import"@openzeppelin/contracts/token/ERC721/ERC721.sol";import"./IL1StandardERC721.sol";contractL1StandardERC721isIL1StandardERC721, ERC721 {// [This is mandatory]// This is your L1 NFT contract address. Only one pair of your NFT contracts can be registered in the NFT bridges.addresspublicoverride l2Contract;// [This is not mandatory] You can use other names or other ways to only allow l1 NFT bridge to mint and burn tokens// This is L1 NFT brigde contract addressaddresspublic l1Bridge;// [This is not mandatory]// Only l1Brigde (L1 NFT bridge) can mint or burn NFTsmodifieronlyL1Bridge {require(msg.sender == l1Bridge,"Only L1 Bridge can mint and burn"); _; }// [This is mandatory]// You must export this interface `IL1StandardERC721.l2Contract.selector`functionsupportsInterface(bytes4_interfaceId) publicviewoverride(IERC165,ERC721) returns (bool) {bytes4 bridgingSupportedInterface = IL1StandardERC721.l2Contract.selector^ IL1StandardERC721.mint.selector^ IL1StandardERC721.burn.selector;return _interfaceId == bridgingSupportedInterface || super.supportsInterface(_interfaceId); }// [SECURITY] Make sure that only L1 NFT bridge can mint tokens// The input must be `address _to, uint256 _tokenId, bytes memory _data`functionmint(address_to,uint256_tokenId,bytesmemory_data) publicvirtualoverrideonlyL1Bridge {_mint(_to, _tokenId);emitMint(_to, _tokenId); }// [The input is mandatory] The input must be `address _to, uint256 _tokenId, bytes memory _data`// [SECURITY] Make sure that only L1 NFT bridge can mint tokensfunctionburn(uint256_tokenId) publicvirtualoverrideonlyL1Bridge {_burn(_tokenId);emitBurn(_tokenId); }}
NOTE: Once you have your L1 NFT contract address, please contact us so we can register that address in the L1 and L2 NFT bridges.
CASE 1 - Native L1 NFT - Bridge NFTs from Ethereum to Boba
First, users transfer their NFT to the L1 NFT Bridge, starting with an approval.
Users have to approve the Boba for the exit fee next. They then call the withdraw or withdrawTo function to exit the NFT from Boba to Ethereum. The NFT will arrive on L1 after the seven days.
Users have to approve the Boba for the exit fee next. They then call the withdraw or withdrawTo function to exit NFT from L2. The NFT will arrive on L1 after the seven days.
Attempting to categorize ERC721s on the basis of metadata, we have:
ERC721 with derivable metaData (more common)
The general ERC721(like the one in the example) has the tokenURI in the form = 'baseURI' + 'tokenId' or is completely derivable on-chain from the tokenId
In this case, you don't really need to worry about transporting metadata between layers and hence you are already at best.
ERC721 with no metadata (non- ERC721Metadata)
Some ERC721 do not have metadata associated, in which case you surely do not need to worry about transporting metadata
ERC721 with non-derivable metaData (unrecoverable context)
These are the "special NFTs" that require some form of transportation of metadata between layers
The NFT Bridge provides with a special method 'withdrawWithExtraData' in comparision to 'withdraw' for the usual bridging to allow transporting the metadata when bridging the NFT to the other layer
What metadata are actually bridged?
When you chose to withdraw through the aforementioned method - the tokenURI() data will be encoded and passed on to the L1StandardERC721 for it to receive and handle it
Optimisations
Bridging the tokenURI data as a whole might not be ecnomical always and depends on the size of the tokenURI. For example, bridging the tokenURI data for on-chain NFTs could be very costly. The NFTBridge, asks for a special method for this - bridgeExtraData, if this method is implemented on your native ERC721 contracts and returns some data that you would need to bridge to the other layer, the bridge will prioritize this over tokenURI potentially allowing to bridge seed data for generation on the L1 side.
Making your ERC721 bridge extra data
To enable the bridge to pick up the exposed extra data that you would want to bridge
expose the method bridgeExtraData() on your contract. This can encode one/many unique seed data to be transported over to the other layer while bridging. For example, an on-chain contract that requires three unique integers to be transported for each tokenId can expose the data in a way similar to:
so your NFT contract should decode _data and write into the smart contract.
To bridge the NFT with the extra data, you need to call these functions of NFT bridges:
// This example is for the L1 native NFT. L2 native NFT is similar to this one.// Approve L1 NFT contract firstconstapproveL1Tx=awaitL1NFT.approve(L1_NFT_BRIDGE_ADDRESS,TOKEN_ID)awaitapproveL1Tx.wait()// Deposit with extra dataconstdepositTx=awaitL1NFTBrige.depositNFTWithExtraData(L1_NFT_CONTRACT_ADDRESS,TOKEN_ID,9999999// L2 gas)awaitdepositTx.wait()// Or deposit to another walletconstdepositToTx=awaitL1NFTBrige.depositNFTWithExtraDataTo(L1_NFT_CONTRACT_ADDRESS,TARGET_WALLET_ADDRESS,TOKEN_ID,9999999// L2 gas)awaitdepositToTx.wait()// Approve L2 NFT contract firstconstapproveL2Tx=awaitL2NFT.approve(L2_NFT_BRIDGE_ADDRESS,TOKEN_ID)awaitapproveL2Tx.wait()// You can use the standard function to withdrawconstwithdrawTx=awaitL2NFTBrige.withdraw(L2_NFT_CONTRACT_ADDRESS,TOKEN_ID,9999999// L2 gas)awaitwithdrawTx.wait()// Or withdraw to another walletconstwithdrawToTx=awaitL2NFTBrige.withdrawTo(L2_NFT_CONTRACT_ADDRESS,TARGET_WALLET_ADDRESS,TOKEN_ID,9999999// L2 gas)awaitwithdrawToTx.wait()