This example demonstrates how to deploy and interact with ERC721 contract using Optimistic Rollup, including the following operations:
- Deposit ERC721 from L1 to L2
- Transfer ERC721 in L2
- Withdraw ERC721 from L2 to L1
Here are three other great examples:
Everything we need to build and run Optimistic Rollup is in Optimistic Ethereum:
$ git clone https://github.com/ethereum-optimism/optimism.git
$ cd optimism
$ yarn
$ yarn build
Run it when the build is done:
$ cd ops
$ docker-compose build
$ docker-compose up
A few services will be up, including:
- L1 Ethereum Node (EVM)
- L2 Optimistic Ethereum Node (OVM)
- Batch Submitter
- Data Transport Layer
- Deployer
- Relayer
- Verifier
The environment variable
FRAUD_PROOF_WINDOW_SECONDS
in the deployer service defines how much long the user has to wait when withdrawing. Its default value is0
second.
Clean the docker volume when you need to restart the service. Ex:
docker-compose down -v
Before we continue, we need to make sure that the Optimistic Ethereum operates normally, Especially the relayer. Use integration test to check its functionality:
$ cd optimism/integration-tests
$ yarn build:integration
$ yarn test:integration
Make sure all the tests related to L1 <--> L2 Communication
passed before you continue.
It might take a while (~ 120s) for Optimistic Ethereum to be fully operational. If you fail all the test, try again later or rebuild Optimistic Ethereum from the source again.
Next, let's deploy the contract:
$ git clone https://github.com/ccniuj/optimistic-rollup-example-erc721.git
$ cd optimistic-rollup-example-erc721
$ yarn install
$ yarn compile
There are 3 contracts to be deployed:
ExamoleToken
(ERC721), L1L2DepositedEERC721
, L2OVM_L1ERC721Gateway
, L1
OVM_L1ERC721Gateway
is depoyed on L1 only. As its name suggests, it works as a "Gateway" which providesdeposit
andwithdraw
functions. User needs to use a gateway to move funds.
At the time of writing, ERC721 gateway contract hasn't been implemented by the Optimism team. The contracts used in this example are from this pull requrest proposed by @azf20.
Next, deploy these contracts by the script:
$ node ./deploy.js
Deploying L1 ERC721...
L1 ERC721 Contract Address: 0xFD471836031dc5108809D173A067e8486B9047A3
Deploying L2 ERC721...
L2 ERC721 Contract Address: 0x09635F643e140090A9A8Dcd712eD6285858ceBef
Deploying L1 ERC721 Gateway...
L1 ERC721 Gateway Contract Address: 0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc
Initializing L2 ERC721...
In the begining, none of the accounts has any ERC721 token:
L1/L2 | Account | IDs of Owned Token |
---|---|---|
L1 | Deployer | - |
L1 | User | - |
L2 | Deployer | - |
L2 | User | - |
Next, We will mint 2 tokens to get started. Let's enter hardhat ETH (L1) console:
$ npx hardhat console --network eth
Welcome to Node.js v16.1.0.
Type ".help" for more information.
>
Initialize deployer and user:
// In Hardhat ETH Console
> let accounts = await ethers.getSigners()
> let deployer = accounts[0]
> let user = accounts[1]
Instantiate ExampleToken
(ERC721) and OVM_L1ERC721Gateway
contracts. Their contract addresses are in the outputs of the deploy script:
// In Hardhat ETH Console
> let ERC721_abi = await artifacts.readArtifact("ExampleToken").then(c => c.abi)
> let ERC721 = new ethers.Contract("0xFD471836031dc5108809D173A067e8486B9047A3", ERC721_abi)
> let Gateway_abi = await artifacts.readArtifact("OVM_L1ERC721Gateway").then(c => c.abi)
> let Gateway = new ethers.Contract("0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc", Gateway_abi)
Use deployer to mint 2 tokens:
// In Hardhat ETH Console
> await ERC721.connect(deployer).mintToken(deployer.address, "foo")
{
hash: "...",
...
}
> await ERC721.connect(deployer).mintToken(deployer.address, "bar")
{
hash: "...",
...
}
Only the owner of the ExampleToken contract can mint.
Confirm the balance of the owner of tokens:
> await ERC721.connect(deployer).balanceOf(deployer.address)
BigNumber { _hex: '0x02', _isBigNumber: true } // 2
> await ERC721.connect(deployer).ownerOf(1)
'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' // deployer
> await ERC721.connect(deployer).ownerOf(2)
'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' // deployer
Here are the current balances:
L1/L2 | Account | IDs of Owned Token |
---|---|---|
L1 | Deployer | 1, 2 |
L1 | User | - |
L2 | Deployer | - |
L2 | User | - |
Next, approve OVM_L1ERC721Gateway
to transfer the token which has 2
as TokenID:
// In Hardhat ETH Console
> await ERC721.connect(deployer).approve("0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc", 2)
{
hash: "...",
...
}
Call deposit
at OVM_L1ERC721Gateway
contract to deposit this token:
// In Hardhat ETH Console
> await Gateway.connect(deployer).deposit(2)
{
hash: "...",
...
}
Confirm if the deposit is successful from Optimistic Ethereum (L2) console:
$ npx hardhat console --network optimism
Welcome to Node.js v16.1.0.
Type ".help" for more information.
>
Initialize Deployer and User:
// In Hardhat Optimism Console
> let accounts = await ethers.getSigners()
> let deployer = accounts[0]
> let user = accounts[1]
Instantiate L2DepositedERC721
contract. Its contract address is in the outputs of the deploy script:
// In Hardhat Optimism Console
> let L2ERC721_abi = await artifacts.readArtifact("OVM_L2DepositedERC721").then(c => c.abi)
> let L2DepositedERC721 = new ethers.Contract("0x09635F643e140090A9A8Dcd712eD6285858ceBef", L2ERC721_abi)
Confirm if the deposit is successful:
// In Hardhat Optimism Console
> await L2DepositedERC721.connect(deployer).balanceOf(deployer.address)
BigNumber { _hex: '0x01', _isBigNumber: true } // 1
> await L2DepositedERC721.connect(deployer).ownerOf(2)
'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' // deployer
Here are the current balances:
L1/L2 | Account | IDs of Owned Token |
---|---|---|
L1 | Deployer | 1 |
L1 | User | - |
L2 | Deployer | 2 |
L2 | User | - |
Next, let's transfer some funds from deployer to user:
// In Hardhat Optimism Console
> await L2DepositedERC721.connect(user).balanceOf(user.address)
BigNumber { _hex: '0x00', _isBigNumber: true } // 0
> await L2DepositedERC721.connect(deployer).transferFrom(depoyer.address, user.address, 2)
{
hash: "..."
...
}
> await L2DepositedERC721.connect(user).balanceOf(user.address)
BigNumber { _hex: '0x01', _isBigNumber: true } // 1
> await L2DepositedERC721.connect(user).ownerOf(2)
'0x70997970C51812dc3A010C7d01b50e0d17dc79C8' // user
Here are the current balances:
L1/L2 | Account | IDs of Owned Token |
---|---|---|
L1 | Deployer | 1 |
L1 | User | - |
L2 | Deployer | - |
L2 | User | 2 |
Next, let's withdraw the funds via account user. Call withdraw
at L2DepositedERC721
contract:
// In Hardhat Optimism Console
> await L2DepositedERC721.connect(user).withdraw(2)
{
hash: "..."
...
}
> await L2DepositedERC721.connect(user).balanceOf(user.address)
BigNumber { _hex: '0x00', _isBigNumber: true }
Finally, let's confirm if the withdrawal is successful on L1:
// In Hardhat ETH Console
> await ERC721.connect(user).balanceOf(user.address)
BigNumber { _hex: '0x01', _isBigNumber: true } // 1
> await ERC721.connect(deployer).balanceOf(deployer.address)
BigNumber { _hex: '0x01', _isBigNumber: true } // 1
> await ERC721.connect(user).ownerOf(2)
'0x70997970C51812dc3A010C7d01b50e0d17dc79C8' // user
Since the
FRAUD_PROOF_WINDOW_SECONDS
is set to be0
second, you don't need to wait too long before the fund is withdrawn back to L1.
After all the operations, here are the final balances:
L1/L2 | Account | IDs of Owned Token |
---|---|---|
L1 | Deployer | 1 |
L1 | User | 2 |
L2 | Deployer | - |
L2 | User | - |
- OVM Deep Dive
- (Almost) Everything you need to know about Optimistic Rollup
- How does Optimism's Rollup really work?
- Optimistic Rollup Official Documentation
- Ethers Documentation (v5)
- Optimistic Rollup Example: ERC20(Github)
- Optimism (Github)
- optimism-tutorial (Github)
- l1-l2-deposit-withdrawal (Github)
- Proof-of-concept ERC721 Bridge Implementation (Github)