@hyperlane-xyz/core
Version:
Core solidity contracts for Hyperlane
214 lines (185 loc) • 6.5 kB
text/typescript
/* Imports: External */
import { BigNumber, Contract, ContractFactory, utils, Wallet } from 'ethers'
import { ethers } from 'hardhat'
import { getContractFactory } from '@eth-optimism/contracts'
import { MessageStatus } from '@eth-optimism/sdk'
/* Imports: Internal */
import { expect } from './shared/setup'
import { OptimismEnv } from './shared/env'
import { withdrawalTest } from './shared/utils'
describe('Bridged tokens', () => {
let env: OptimismEnv
before(async () => {
env = await OptimismEnv.new()
})
let otherWalletL1: Wallet
let otherWalletL2: Wallet
before(async () => {
const other = Wallet.createRandom()
otherWalletL1 = other.connect(env.l1Wallet.provider)
otherWalletL2 = other.connect(env.l2Wallet.provider)
const tx1 = await env.l1Wallet.sendTransaction({
to: otherWalletL1.address,
value: utils.parseEther('0.01'),
})
await tx1.wait()
const tx2 = await env.l2Wallet.sendTransaction({
to: otherWalletL2.address,
value: utils.parseEther('0.01'),
})
await tx2.wait()
})
let L1Factory__ERC20: ContractFactory
let L2Factory__ERC20: ContractFactory
before(async () => {
L1Factory__ERC20 = await ethers.getContractFactory('ERC20', env.l1Wallet)
L2Factory__ERC20 = getContractFactory('L2StandardERC20', env.l2Wallet)
})
// This is one of the only stateful integration tests in which we don't set up a new contract
// before each test. We do this because the test is more of an "actor-based" test where we're
// going through a series of actions and confirming that the actions are performed correctly at
// every step.
let L1__ERC20: Contract
let L2__ERC20: Contract
before(async () => {
// Deploy the L1 ERC20
L1__ERC20 = await L1Factory__ERC20.deploy(1000000, 'OVM Test', 8, 'OVM')
await L1__ERC20.deployed()
// Deploy the L2 ERC20
L2__ERC20 = await L2Factory__ERC20.deploy(
'0x4200000000000000000000000000000000000010',
L1__ERC20.address,
'OVM Test',
'OVM'
)
await L2__ERC20.deployed()
// Approve the L1 ERC20 to spend our money
const tx = await L1__ERC20.approve(
env.messenger.contracts.l1.L1StandardBridge.address,
1000000
)
await tx.wait()
})
it('should deposit tokens into L2', async () => {
await env.messenger.waitForMessageReceipt(
await env.messenger.depositERC20(
L1__ERC20.address,
L2__ERC20.address,
1000
)
)
expect(await L1__ERC20.balanceOf(env.l1Wallet.address)).to.deep.equal(
BigNumber.from(999000)
)
expect(await L2__ERC20.balanceOf(env.l2Wallet.address)).to.deep.equal(
BigNumber.from(1000)
)
})
it('should transfer tokens on L2', async () => {
const tx = await L2__ERC20.transfer(otherWalletL1.address, 500)
await tx.wait()
expect(await L2__ERC20.balanceOf(env.l2Wallet.address)).to.deep.equal(
BigNumber.from(500)
)
expect(await L2__ERC20.balanceOf(otherWalletL2.address)).to.deep.equal(
BigNumber.from(500)
)
})
withdrawalTest(
'should withdraw tokens from L2 to the depositor',
async () => {
const tx = await env.messenger.withdrawERC20(
L1__ERC20.address,
L2__ERC20.address,
500
)
await env.messenger.waitForMessageStatus(
tx,
MessageStatus.READY_FOR_RELAY
)
await env.messenger.finalizeMessage(tx)
await env.messenger.waitForMessageReceipt(tx)
expect(await L1__ERC20.balanceOf(env.l1Wallet.address)).to.deep.equal(
BigNumber.from(999500)
)
expect(await L2__ERC20.balanceOf(env.l2Wallet.address)).to.deep.equal(
BigNumber.from(0)
)
}
)
withdrawalTest(
'should withdraw tokens from L2 to the transfer recipient',
async () => {
const tx = await env.messenger.withdrawERC20(
L1__ERC20.address,
L2__ERC20.address,
500,
{
signer: otherWalletL2,
}
)
await env.messenger.waitForMessageStatus(
tx,
MessageStatus.READY_FOR_RELAY
)
await env.messenger.finalizeMessage(tx)
await env.messenger.waitForMessageReceipt(tx)
expect(await L1__ERC20.balanceOf(otherWalletL1.address)).to.deep.equal(
BigNumber.from(500)
)
expect(await L2__ERC20.balanceOf(otherWalletL2.address)).to.deep.equal(
BigNumber.from(0)
)
}
)
// This test demonstrates that an apparent withdrawal bug is in fact non-existent.
// Specifically, the L2 bridge does not check that the L2 token being burned corresponds
// with the L1 token which is specified for the withdrawal.
withdrawalTest(
'should not allow an arbitrary L2 token to be withdrawn in exchange for a legitimate L1 token',
async () => {
// First deposit some of the L1 token to L2, so that there is something which could be stolen.
await env.messenger.waitForMessageReceipt(
await env.messenger.depositERC20(
L1__ERC20.address,
L2__ERC20.address,
1000
)
)
expect(await L2__ERC20.balanceOf(env.l2Wallet.address)).to.deep.equal(
BigNumber.from(1000)
)
// Deploy a Fake L2 token, which:
// - returns the address of a legitimate L1 token from its l1Token() getter.
// - allows the L2 bridge to call its burn() function.
const fakeToken = await (
await ethers.getContractFactory('FakeL2StandardERC20', env.l2Wallet)
).deploy(
L1__ERC20.address,
env.messenger.contracts.l2.L2StandardBridge.address
)
await fakeToken.deployed()
const balBefore = await L1__ERC20.balanceOf(otherWalletL1.address)
// Withdraw some of the Fake L2 token, hoping to receive the same amount of the legitimate
// token on L1.
const withdrawalTx = await env.messenger.withdrawERC20(
L1__ERC20.address,
fakeToken.address,
500,
{
signer: otherWalletL2,
}
)
await env.messenger.waitForMessageStatus(
withdrawalTx,
MessageStatus.READY_FOR_RELAY
)
await env.messenger.finalizeMessage(withdrawalTx)
await env.messenger.waitForMessageReceipt(withdrawalTx)
// Ensure that the L1 recipient address has not received any additional L1 token balance.
expect(await L1__ERC20.balanceOf(otherWalletL1.address)).to.deep.equal(
balBefore
)
}
)
})