@hyperlane-xyz/core
Version:
Core solidity contracts for Hyperlane
1,509 lines (1,322 loc) • 47 kB
text/typescript
/*
* Copyright 2019-2020, Offchain Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-env node, mocha */
import { ethers, network } from 'hardhat'
import { Signer } from '@ethersproject/abstract-signer'
import { BigNumberish, BigNumber } from '@ethersproject/bignumber'
import { BytesLike } from '@ethersproject/bytes'
import { ContractTransaction } from '@ethersproject/contracts'
import { assert, expect } from 'chai'
import {
Bridge__factory,
Inbox__factory,
RollupEventInbox__factory,
Outbox__factory,
ERC20Bridge__factory,
ERC20Inbox__factory,
ERC20RollupEventInbox__factory,
ERC20Outbox__factory,
BridgeCreator__factory,
ChallengeManager,
ChallengeManager__factory,
DeployHelper__factory,
OneStepProofEntry__factory,
OneStepProver0__factory,
OneStepProverHostIo__factory,
OneStepProverMath__factory,
OneStepProverMemory__factory,
RollupAdminLogic,
RollupAdminLogic__factory,
RollupCreator__factory,
RollupUserLogic,
RollupUserLogic__factory,
SequencerInbox,
SequencerInbox__factory,
Bridge,
} from '../../build/types'
import {
abi as UpgradeExecutorABI,
bytecode as UpgradeExecutorBytecode,
} from '@offchainlabs/upgrade-executor/build/contracts/src/UpgradeExecutor.sol/UpgradeExecutor.json'
import { initializeAccounts } from './utils'
import {
Node,
RollupContract,
forceCreateNode,
assertionEquals,
} from './common/rolluplib'
import { AssertionStruct } from '../../build/types/src/rollup/RollupCore'
import { ExecutionStateStruct } from '../../build/types/src/rollup/RollupCore'
import { keccak256 } from 'ethers/lib/utils'
import {
ConfigStruct,
RollupCreatedEvent,
} from '../../build/types/src/rollup/RollupCreator'
import { constants, providers } from 'ethers'
import { blockStateHash, MachineStatus } from './common/challengeLib'
import * as globalStateLib from './common/globalStateLib'
import { RollupChallengeStartedEvent } from '../../build/types/src/rollup/IRollupCore'
const zerobytes32 = ethers.constants.HashZero
const stakeRequirement = 10
const stakeToken = ethers.constants.AddressZero
const confirmationPeriodBlocks = 100
const minimumAssertionPeriod = 75
const ZERO_ADDR = ethers.constants.AddressZero
const extraChallengeTimeBlocks = 20
const wasmModuleRoot =
'0x9900000000000000000000000000000000000000000000000000000000000010'
const dummy4844Reader = '0x0000000000000000000000000000000000000089'
// let rollup: RollupContract
let rollup: RollupContract
let batchPosterManager: Signer
let rollupUser: RollupUserLogic
let rollupAdmin: RollupAdminLogic
let bridge: Bridge
let accounts: Signer[]
let validators: Signer[]
let sequencerInbox: SequencerInbox
let admin: Signer
let sequencer: Signer
let challengeManager: ChallengeManager
let upgradeExecutor: string
// let adminproxy: string
async function getDefaultConfig(
_confirmPeriodBlocks = confirmationPeriodBlocks
): Promise<ConfigStruct> {
return {
baseStake: stakeRequirement,
chainId: stakeToken,
chainConfig: '{}', // TODO
confirmPeriodBlocks: _confirmPeriodBlocks,
extraChallengeTimeBlocks: extraChallengeTimeBlocks,
owner: await accounts[0].getAddress(),
sequencerInboxMaxTimeVariation: {
delayBlocks: (60 * 60 * 24) / 15,
futureBlocks: 12,
delaySeconds: 60 * 60 * 24,
futureSeconds: 60 * 60,
},
stakeToken: stakeToken,
wasmModuleRoot: wasmModuleRoot,
loserStakeEscrow: ZERO_ADDR,
genesisBlockNum: 0,
}
}
const setup = async () => {
accounts = await initializeAccounts()
admin = accounts[0]
const user = accounts[1]
const val1 = accounts[2]
const val2 = accounts[3]
const val3 = accounts[4]
const val4 = accounts[5]
sequencer = accounts[6]
const batchPosterManager = accounts[7]
const oneStep0Fac = (await ethers.getContractFactory(
'OneStepProver0'
)) as OneStepProver0__factory
const oneStep0 = await oneStep0Fac.deploy()
const oneStepMemoryFac = (await ethers.getContractFactory(
'OneStepProverMemory'
)) as OneStepProverMemory__factory
const oneStepMemory = await oneStepMemoryFac.deploy()
const oneStepMathFac = (await ethers.getContractFactory(
'OneStepProverMath'
)) as OneStepProverMath__factory
const oneStepMath = await oneStepMathFac.deploy()
const oneStepHostIoFac = (await ethers.getContractFactory(
'OneStepProverHostIo'
)) as OneStepProverHostIo__factory
const oneStepHostIo = await oneStepHostIoFac.deploy()
const oneStepProofEntryFac = (await ethers.getContractFactory(
'OneStepProofEntry'
)) as OneStepProofEntry__factory
const oneStepProofEntry = await oneStepProofEntryFac.deploy(
oneStep0.address,
oneStepMemory.address,
oneStepMath.address,
oneStepHostIo.address
)
const challengeManagerTemplateFac = (await ethers.getContractFactory(
'ChallengeManager'
)) as ChallengeManager__factory
const challengeManagerTemplate = await challengeManagerTemplateFac.deploy()
const rollupAdminLogicFac = (await ethers.getContractFactory(
'RollupAdminLogic'
)) as RollupAdminLogic__factory
const rollupAdminLogicTemplate = await rollupAdminLogicFac.deploy()
const rollupUserLogicFac = (await ethers.getContractFactory(
'RollupUserLogic'
)) as RollupUserLogic__factory
const rollupUserLogicTemplate = await rollupUserLogicFac.deploy()
const upgradeExecutorLogicFac = await ethers.getContractFactory(
UpgradeExecutorABI,
UpgradeExecutorBytecode
)
const upgradeExecutorLogic = await upgradeExecutorLogicFac.deploy()
const ethBridgeFac = (await ethers.getContractFactory(
'Bridge'
)) as Bridge__factory
const ethBridge = await ethBridgeFac.deploy()
const ethSequencerInboxFac = (await ethers.getContractFactory(
'SequencerInbox'
)) as SequencerInbox__factory
const ethSequencerInbox = await ethSequencerInboxFac.deploy(
117964,
dummy4844Reader,
false
)
const ethInboxFac = (await ethers.getContractFactory(
'Inbox'
)) as Inbox__factory
const ethInbox = await ethInboxFac.deploy(117964)
const ethRollupEventInboxFac = (await ethers.getContractFactory(
'RollupEventInbox'
)) as RollupEventInbox__factory
const ethRollupEventInbox = await ethRollupEventInboxFac.deploy()
const ethOutboxFac = (await ethers.getContractFactory(
'Outbox'
)) as Outbox__factory
const ethOutbox = await ethOutboxFac.deploy()
const erc20BridgeFac = (await ethers.getContractFactory(
'ERC20Bridge'
)) as ERC20Bridge__factory
const erc20Bridge = await erc20BridgeFac.deploy()
const erc20SequencerInboxFac = (await ethers.getContractFactory(
'SequencerInbox'
)) as SequencerInbox__factory
const erc20SequencerInbox = await erc20SequencerInboxFac.deploy(
117964,
dummy4844Reader,
true
)
const erc20InboxFac = (await ethers.getContractFactory(
'ERC20Inbox'
)) as ERC20Inbox__factory
const erc20Inbox = await erc20InboxFac.deploy(117964)
const erc20RollupEventInboxFac = (await ethers.getContractFactory(
'ERC20RollupEventInbox'
)) as ERC20RollupEventInbox__factory
const erc20RollupEventInbox = await erc20RollupEventInboxFac.deploy()
const erc20OutboxFac = (await ethers.getContractFactory(
'ERC20Outbox'
)) as ERC20Outbox__factory
const erc20Outbox = await erc20OutboxFac.deploy()
const bridgeCreatorFac = (await ethers.getContractFactory(
'BridgeCreator'
)) as BridgeCreator__factory
const bridgeCreator = await bridgeCreatorFac.deploy(
{
bridge: ethBridge.address,
sequencerInbox: ethSequencerInbox.address,
inbox: ethInbox.address,
rollupEventInbox: ethRollupEventInbox.address,
outbox: ethOutbox.address,
},
{
bridge: erc20Bridge.address,
sequencerInbox: erc20SequencerInbox.address,
inbox: erc20Inbox.address,
rollupEventInbox: erc20RollupEventInbox.address,
outbox: erc20Outbox.address,
}
)
const rollupCreatorFac = (await ethers.getContractFactory(
'RollupCreator'
)) as RollupCreator__factory
const rollupCreator = await rollupCreatorFac.deploy()
const deployHelperFac = (await ethers.getContractFactory(
'DeployHelper'
)) as DeployHelper__factory
const deployHelper = await deployHelperFac.deploy()
await rollupCreator.setTemplates(
bridgeCreator.address,
oneStepProofEntry.address,
challengeManagerTemplate.address,
rollupAdminLogicTemplate.address,
rollupUserLogicTemplate.address,
upgradeExecutorLogic.address,
ethers.constants.AddressZero,
ethers.constants.AddressZero,
deployHelper.address
)
const maxFeePerGas = BigNumber.from('1000000000')
const deployParams = {
config: await getDefaultConfig(),
batchPosters: [await sequencer.getAddress()],
validators: [
await val1.getAddress(),
await val2.getAddress(),
await val3.getAddress(),
await val4.getAddress(),
],
maxDataSize: 117964,
nativeToken: ethers.constants.AddressZero,
deployFactoriesToL2: true,
maxFeePerGasForRetryables: maxFeePerGas,
batchPosterManager: await batchPosterManager.getAddress(),
}
const response = await rollupCreator.createRollup(deployParams, {
value: ethers.utils.parseEther('0.2'),
})
const rec = await response.wait()
const rollupCreatedEvent = rollupCreator.interface.parseLog(
rec.logs[rec.logs.length - 1]
).args as RollupCreatedEvent['args']
const rollupAdmin = rollupAdminLogicFac
.attach(rollupCreatedEvent.rollupAddress)
.connect(rollupCreator.signer)
const rollupUser = rollupUserLogicFac
.attach(rollupCreatedEvent.rollupAddress)
.connect(user)
const bridge = ethBridgeFac.attach(rollupCreatedEvent.bridge).connect(user)
sequencerInbox = (
(await ethers.getContractFactory(
'SequencerInbox'
)) as SequencerInbox__factory
).attach(rollupCreatedEvent.sequencerInbox)
await sequencerInbox
.connect(await impersonateAccount(rollupCreatedEvent.upgradeExecutor))
.setBatchPosterManager(await batchPosterManager.getAddress())
challengeManager = (
(await ethers.getContractFactory(
'ChallengeManager'
)) as ChallengeManager__factory
).attach(await rollupUser.challengeManager())
return {
admin,
user,
rollupAdmin,
rollupUser,
validators: [val1, val2, val3, val4],
rollupAdminLogicTemplate,
rollupUserLogicTemplate,
blockChallengeFactory: challengeManagerTemplateFac,
rollupEventBridge: await rollupAdmin.rollupEventInbox(),
outbox: rollupCreatedEvent.outbox,
sequencerInbox: rollupCreatedEvent.sequencerInbox,
delayedBridge: rollupCreatedEvent.bridge,
delayedInbox: rollupCreatedEvent.inboxAddress,
bridge,
batchPosterManager,
upgradeExecutorAddress: rollupCreatedEvent.upgradeExecutor,
adminproxy: rollupCreatedEvent.adminProxy,
}
}
async function tryAdvanceChain(blocks: number, time?: number): Promise<void> {
try {
if (time === undefined) {
time = blocks * 12
}
if (blocks <= 0) {
blocks = 1
}
if (time > 0) {
await ethers.provider.send('evm_increaseTime', [time])
}
for (let i = 0; i < blocks; i++) {
await ethers.provider.send('evm_mine', [])
}
} catch (e) {
// EVM mine failed. Try advancing the chain by sending txes if the node
// is in dev mode and mints blocks when txes are sent
for (let i = 0; i < blocks; i++) {
const tx = await accounts[0].sendTransaction({
value: 0,
to: await accounts[0].getAddress(),
})
await tx.wait()
}
}
}
async function advancePastAssertion(
blockProposed: number,
confBlocks?: number
): Promise<void> {
if (confBlocks === undefined) {
confBlocks = confirmationPeriodBlocks
}
const blockProposedBlock = await ethers.provider.getBlock(blockProposed)
const latestBlock = await ethers.provider.getBlock('latest')
const passedBlocks = latestBlock.number - blockProposed
const passedTime = latestBlock.timestamp - blockProposedBlock.timestamp
await tryAdvanceChain(confBlocks - passedBlocks, confBlocks * 12 - passedTime)
}
function newRandomExecutionState() {
const blockHash = keccak256(ethers.utils.randomBytes(32))
const sendRoot = keccak256(ethers.utils.randomBytes(32))
const machineStatus = 1
return newExecutionState(blockHash, sendRoot, 1, 0, machineStatus)
}
function newExecutionState(
blockHash: string,
sendRoot: string,
inboxPosition: BigNumberish,
positionInMessage: BigNumberish,
machineStatus: BigNumberish
): ExecutionStateStruct {
return {
globalState: {
bytes32Vals: [blockHash, sendRoot],
u64Vals: [inboxPosition, positionInMessage],
},
machineStatus,
}
}
function newRandomAssertion(
prevExecutionState: ExecutionStateStruct
): AssertionStruct {
return {
beforeState: prevExecutionState,
afterState: newRandomExecutionState(),
numBlocks: 10,
}
}
async function makeSimpleNode(
rollup: RollupContract,
sequencerInbox: SequencerInbox,
parentNode: {
assertion: { afterState: ExecutionStateStruct }
nodeNum: number
nodeHash: BytesLike
inboxMaxCount: BigNumber
},
siblingNode?: Node,
prevNode?: Node,
stakeToAdd?: BigNumber
): Promise<{ tx: ContractTransaction; node: Node }> {
const staker = await rollup.rollup.getStaker(
await rollup.rollup.signer.getAddress()
)
const assertion = newRandomAssertion(parentNode.assertion.afterState)
const { tx, node, expectedNewNodeHash } = await rollup.stakeOnNewNode(
sequencerInbox,
parentNode,
assertion,
siblingNode,
stakeToAdd
)
expect(assertionEquals(assertion, node.assertion), 'unexpected assertion').to
.be.true
assert.equal(
node.nodeNum,
(prevNode || siblingNode || parentNode).nodeNum + 1
)
assert.equal(node.nodeHash, expectedNewNodeHash)
if (stakeToAdd) {
const stakerAfter = await rollup.rollup.getStaker(
await rollup.rollup.signer.getAddress()
)
expect(stakerAfter.latestStakedNode.toNumber()).to.eq(node.nodeNum)
expect(stakerAfter.amountStaked.toString()).to.eq(
staker.amountStaked.add(stakeToAdd).toString()
)
}
return { tx, node }
}
let prevNode: Node
const prevNodes: Node[] = []
function updatePrevNode(node: Node) {
prevNode = node
prevNodes.push(node)
}
const _IMPLEMENTATION_PRIMARY_SLOT =
'0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'
const _IMPLEMENTATION_SECONDARY_SLOT =
'0x2b1dbce74324248c222f0ec2d5ed7bd323cfc425b336f0253c5ccfda7265546d'
const getDoubleLogicUUPSTarget = async (
slot: 'user' | 'admin',
provider: providers.Provider
): Promise<string> => {
return `0x${(
await provider.getStorageAt(
rollupAdmin.address,
slot === 'admin'
? _IMPLEMENTATION_PRIMARY_SLOT
: _IMPLEMENTATION_SECONDARY_SLOT
)
)
.substring(26)
.toLowerCase()}`
}
const impersonateAccount = (address: string) =>
network.provider
.request({
// Fund inboxMock to send transaction
method: 'hardhat_setBalance',
params: [address, '0xffffffffffffffffffff'],
})
.then(() =>
network.provider.request({
method: 'hardhat_impersonateAccount',
params: [address],
})
)
.then(() => ethers.getSigner(address))
describe('ArbRollup', () => {
it('should initialize', async function () {
const {
rollupAdmin: rollupAdminContract,
rollupUser: rollupUserContract,
bridge: bridgeContract,
admin: adminI,
validators: validatorsI,
batchPosterManager: batchPosterManagerI,
upgradeExecutorAddress,
} = await setup()
rollupAdmin = rollupAdminContract
rollupUser = rollupUserContract
bridge = bridgeContract
admin = adminI
validators = validatorsI
upgradeExecutor = upgradeExecutorAddress
// adminproxy = adminproxyAddress
rollup = new RollupContract(rollupUser.connect(validators[0]))
batchPosterManager = batchPosterManagerI
})
it('should only initialize once', async function () {
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.initialize(await getDefaultConfig(), {
challengeManager: constants.AddressZero,
bridge: constants.AddressZero,
inbox: constants.AddressZero,
outbox: constants.AddressZero,
rollupAdminLogic: constants.AddressZero,
rollupEventInbox: constants.AddressZero,
rollupUserLogic: constants.AddressZero,
sequencerInbox: constants.AddressZero,
validatorUtils: constants.AddressZero,
validatorWalletCreator: constants.AddressZero,
})
).to.be.revertedWith('Initializable: contract is already initialized')
})
it('should place stake on new node', async function () {
await tryAdvanceChain(minimumAssertionPeriod)
const initNode: {
assertion: { afterState: ExecutionStateStruct }
nodeNum: number
nodeHash: BytesLike
inboxMaxCount: BigNumber
} = {
assertion: {
afterState: {
globalState: {
bytes32Vals: [zerobytes32, zerobytes32],
u64Vals: [0, 0],
},
machineStatus: MachineStatus.FINISHED,
},
},
inboxMaxCount: BigNumber.from(1),
nodeHash: zerobytes32,
nodeNum: 0,
}
const stake = await rollup.currentRequiredStake()
const { node } = await makeSimpleNode(
rollup,
sequencerInbox,
initNode,
undefined,
undefined,
stake
)
updatePrevNode(node)
})
it('should let a new staker place on existing node', async function () {
const stake = await rollup.currentRequiredStake()
await rollupUser
.connect(validators[2])
.newStakeOnExistingNode(1, prevNode.nodeHash, { value: stake })
await rollupUser
.connect(validators[3])
.newStakeOnExistingNode(1, prevNode.nodeHash, { value: stake })
})
it('should move stake to a new node', async function () {
await tryAdvanceChain(minimumAssertionPeriod)
const { node } = await makeSimpleNode(rollup, sequencerInbox, prevNode)
updatePrevNode(node)
})
it('should let the second staker place on the new node', async function () {
await rollup
.connect(validators[2])
.stakeOnExistingNode(2, prevNode.nodeHash)
})
it('should confirm node', async function () {
await tryAdvanceChain(confirmationPeriodBlocks * 2)
await rollup.confirmNextNode(prevNodes[0])
})
it('should confirm next node', async function () {
await tryAdvanceChain(minimumAssertionPeriod)
await rollup.confirmNextNode(prevNodes[1])
})
let challengedNode: Node
let validNode: Node
it('should let the first staker make another node', async function () {
await tryAdvanceChain(minimumAssertionPeriod)
const { node } = await makeSimpleNode(rollup, sequencerInbox, prevNode)
challengedNode = node
validNode = node
})
let challengerNode: Node
it('should let the second staker make a conflicting node', async function () {
await tryAdvanceChain(minimumAssertionPeriod)
const { node } = await makeSimpleNode(
rollup.connect(validators[2]),
sequencerInbox,
prevNode,
validNode
)
challengerNode = node
})
it('should fail to confirm first staker node', async function () {
await advancePastAssertion(challengerNode.proposedBlock)
await expect(rollup.confirmNextNode(validNode)).to.be.revertedWith(
'NOT_ALL_STAKED'
)
})
let challengeIndex: number
let challengeCreatedAt: number
it('should initiate a challenge', async function () {
const tx = rollup.createChallenge(
await validators[0].getAddress(),
await validators[2].getAddress(),
challengedNode,
challengerNode
)
const receipt = await (await tx).wait()
const ev = rollup.rollup.interface.parseLog(
receipt.logs![receipt.logs!.length - 1]
)
expect(ev.name).to.equal('RollupChallengeStarted')
const parsedEv = ev.args as RollupChallengeStartedEvent['args']
challengeIndex = parsedEv.challengeIndex.toNumber()
challengeCreatedAt = receipt.blockNumber
})
it('should make a new node', async function () {
const { node } = await makeSimpleNode(
rollup,
sequencerInbox,
validNode,
undefined,
challengerNode
)
challengedNode = node
})
it('new staker should make a conflicting node', async function () {
const stake = await rollup.currentRequiredStake()
await rollup.rollup
.connect(validators[1])
.newStakeOnExistingNode(3, validNode.nodeHash, {
value: stake.add(50),
})
const { node } = await makeSimpleNode(
rollup.connect(validators[1]),
sequencerInbox,
validNode,
challengedNode
)
challengerNode = node
})
it('timeout should not occur early', async function () {
const challengeCreatedAtTime = (
await ethers.provider.getBlock(challengeCreatedAt)
).timestamp
// This is missing the extraChallengeTimeBlocks
const notQuiteChallengeDuration =
challengedNode.proposedBlock -
validNode.proposedBlock +
confirmationPeriodBlocks
const elapsedTime =
(await ethers.provider.getBlock('latest')).timestamp -
challengeCreatedAtTime
await tryAdvanceChain(1, notQuiteChallengeDuration - elapsedTime)
const isTimedOut = await challengeManager
.connect(validators[0])
.isTimedOut(challengeIndex)
expect(isTimedOut).to.be.false
})
it('asserter should win via timeout', async function () {
await tryAdvanceChain(extraChallengeTimeBlocks)
await challengeManager.connect(validators[0]).timeout(challengeIndex)
})
it('confirm first staker node', async function () {
await rollup.confirmNextNode(validNode)
})
it('should reject out of order second node', async function () {
await rollup.rejectNextNode(stakeToken)
})
it('should initiate another challenge', async function () {
const tx = rollup.createChallenge(
await validators[0].getAddress(),
await validators[1].getAddress(),
challengedNode,
challengerNode
)
const receipt = await (await tx).wait()
const ev = rollup.rollup.interface.parseLog(
receipt.logs![receipt.logs!.length - 1]
)
expect(ev.name).to.equal('RollupChallengeStarted')
const parsedEv = ev.args as RollupChallengeStartedEvent['args']
challengeIndex = parsedEv.challengeIndex.toNumber()
await expect(
rollup.rollup.completeChallenge(
challengeIndex,
await sequencer.getAddress(),
await validators[3].getAddress()
)
).to.be.revertedWith('WRONG_SENDER')
})
it('challenger should reply in challenge', async function () {
const seg0 = blockStateHash(
BigNumber.from(challengerNode.assertion.beforeState.machineStatus),
globalStateLib.hash(challengerNode.assertion.beforeState.globalState)
)
const seg1 = blockStateHash(
BigNumber.from(challengedNode.assertion.afterState.machineStatus),
globalStateLib.hash(challengedNode.assertion.afterState.globalState)
)
await challengeManager.connect(validators[1]).bisectExecution(
challengeIndex,
{
challengePosition: BigNumber.from(0),
oldSegments: [seg0, seg1],
oldSegmentsLength: BigNumber.from(challengedNode.assertion.numBlocks),
oldSegmentsStart: 0,
},
[
seg0,
zerobytes32,
zerobytes32,
zerobytes32,
zerobytes32,
zerobytes32,
zerobytes32,
zerobytes32,
zerobytes32,
zerobytes32,
zerobytes32,
]
)
})
it('challenger should win via timeout', async function () {
const challengeDuration =
confirmationPeriodBlocks +
extraChallengeTimeBlocks +
(challengerNode.proposedBlock - validNode.proposedBlock)
await advancePastAssertion(challengerNode.proposedBlock, challengeDuration)
await challengeManager.timeout(challengeIndex)
})
it('should reject out of order second node', async function () {
await rollup.rejectNextNode(await validators[1].getAddress())
})
it('confirm next node', async function () {
await tryAdvanceChain(confirmationPeriodBlocks)
await rollup.confirmNextNode(challengerNode)
})
it('allow force refund staker with pending node', async function () {
await rollupAdmin.connect(await impersonateAccount(upgradeExecutor)).pause()
await (
await rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.forceRefundStaker([await validators[3].getAddress()])
).wait()
await expect(
rollup.rollup.connect(validators[3]).withdrawStakerFunds()
).to.be.revertedWith('PAUSED_AND_ACTIVE')
// staker can only withdraw if rollup address changed when paused
await bridge
.connect(await impersonateAccount(rollup.rollup.address))
.updateRollupAddress(ethers.constants.AddressZero)
await (
await rollup.rollup.connect(validators[3]).withdrawStakerFunds()
).wait()
await rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.resume()
// restore rollup address
await bridge
.connect(await impersonateAccount(ethers.constants.AddressZero))
.updateRollupAddress(rollupUser.address)
const postWithdrawablefunds = await rollup.rollup.withdrawableFunds(
await validators[3].getAddress()
)
expect(postWithdrawablefunds, 'withdrawable funds').to.equal(0)
const stake = await rollup.rollup.amountStaked(
await validators[3].getAddress()
)
expect(stake, 'amount staked').to.equal(0)
})
it('should add and remove stakes correctly', async function () {
/*
RollupUser functions that alter stake and their respective Core logic
user: newStake
core: createNewStake
user: addToDeposit
core: increaseStakeBy
user: reduceDeposit
core: reduceStakeTo
user: returnOldDeposit
core: withdrawStaker
user: withdrawStakerFunds
core: withdrawFunds
*/
const initialStake = await rollup.rollup.amountStaked(
await validators[1].getAddress()
)
await rollup.connect(validators[1]).reduceDeposit(initialStake)
await expect(
rollup.connect(validators[1]).reduceDeposit(initialStake.add(1))
).to.be.revertedWith('TOO_LITTLE_STAKE')
await rollup
.connect(validators[1])
.addToDeposit(await validators[1].getAddress(), { value: 5 })
await rollup.connect(validators[1]).reduceDeposit(5)
const prevBalance = await validators[1].getBalance()
const prevWithdrawablefunds = await rollup.rollup.withdrawableFunds(
await validators[1].getAddress()
)
const tx = await rollup.rollup.connect(validators[1]).withdrawStakerFunds()
const receipt = await tx.wait()
const gasPaid = receipt.gasUsed.mul(receipt.effectiveGasPrice)
const postBalance = await validators[1].getBalance()
const postWithdrawablefunds = await rollup.rollup.withdrawableFunds(
await validators[1].getAddress()
)
expect(postWithdrawablefunds).to.equal(0)
expect(postBalance.add(gasPaid)).to.equal(
prevBalance.add(prevWithdrawablefunds)
)
// this gets deposit and removes staker
await rollup.rollup
.connect(validators[1])
.returnOldDeposit(await validators[1].getAddress())
// all stake is now removed
})
it('should allow removing zombies', async function () {
const zombieCount = (
await rollup.rollup.connect(validators[2]).zombieCount()
).toNumber()
for (let i = 0; i < zombieCount; i++) {
await rollup.rollup.connect(validators[2]).removeZombie(0, 1024)
}
})
it('should pause the contracts then resume', async function () {
const prevIsPaused = await rollup.rollup.paused()
expect(prevIsPaused).to.equal(false)
await rollupAdmin.connect(await impersonateAccount(upgradeExecutor)).pause()
const postIsPaused = await rollup.rollup.paused()
expect(postIsPaused).to.equal(true)
await expect(
rollup
.connect(validators[1])
.addToDeposit(await validators[1].getAddress(), { value: 5 })
).to.be.revertedWith('Pausable: paused')
await rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.resume()
})
it('should allow admin to alter rollup while paused', async function () {
const prevLatestConfirmed = await rollup.rollup.latestConfirmed()
expect(prevLatestConfirmed.toNumber()).to.equal(6)
// prevNode is prevLatestConfirmed
prevNode = challengerNode
const stake = await rollup.currentRequiredStake()
const { node: node1 } = await makeSimpleNode(
rollup,
sequencerInbox,
prevNode,
undefined,
undefined,
stake
)
const node1Num = await rollup.rollup.latestNodeCreated()
expect(node1Num.toNumber(), 'node1num').to.eq(node1.nodeNum)
await tryAdvanceChain(minimumAssertionPeriod)
const { node: node2 } = await makeSimpleNode(
rollup.connect(validators[2]),
sequencerInbox,
prevNode,
node1,
undefined,
stake
)
const node2Num = await rollup.rollup.latestNodeCreated()
expect(node2Num.toNumber(), 'node2num').to.eq(node2.nodeNum)
const tx = await rollup.createChallenge(
await validators[0].getAddress(),
await validators[2].getAddress(),
node1,
node2
)
const receipt = await tx.wait()
const ev = rollup.rollup.interface.parseLog(
receipt.logs![receipt.logs!.length - 1]
)
expect(ev.name).to.equal('RollupChallengeStarted')
const parsedEv = ev.args as RollupChallengeStartedEvent['args']
challengeIndex = parsedEv.challengeIndex.toNumber()
expect(
await challengeManager.currentResponder(challengeIndex),
'turn challenger'
).to.eq(await validators[2].getAddress())
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.forceResolveChallenge(
[await validators[0].getAddress()],
[await validators[2].getAddress()]
),
'force resolve'
).to.be.revertedWith('Pausable: not paused')
await expect(
rollup.createChallenge(
await validators[0].getAddress(),
await validators[2].getAddress(),
node1,
node2
),
'create challenge'
).to.be.revertedWith('IN_CHAL')
await rollupAdmin.connect(await impersonateAccount(upgradeExecutor)).pause()
await rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.forceResolveChallenge(
[await validators[0].getAddress()],
[await validators[2].getAddress()]
)
// challenge should have been destroyed
expect(
await challengeManager.currentResponder(challengeIndex),
'turn reset'
).to.equal(constants.AddressZero)
const challengeA = await rollupAdmin.currentChallenge(
await validators[0].getAddress()
)
const challengeB = await rollupAdmin.currentChallenge(
await validators[2].getAddress()
)
expect(challengeA).to.equal(ZERO_ADDR)
expect(challengeB).to.equal(ZERO_ADDR)
await rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.forceRefundStaker([
await validators[0].getAddress(),
await validators[2].getAddress(),
])
const adminAssertion = newRandomAssertion(prevNode.assertion.afterState)
const { node: forceCreatedNode1 } = await forceCreateNode(
rollupAdmin.connect(await impersonateAccount(upgradeExecutor)),
sequencerInbox,
prevNode,
adminAssertion,
node2
)
expect(
assertionEquals(forceCreatedNode1.assertion, adminAssertion),
'assertion error'
).to.be.true
const adminNodeNum = await rollup.rollup.latestNodeCreated()
const midLatestConfirmed = await rollup.rollup.latestConfirmed()
expect(midLatestConfirmed.toNumber()).to.equal(6)
expect(adminNodeNum.toNumber()).to.equal(node2Num.toNumber() + 1)
const adminAssertion2 = newRandomAssertion(prevNode.assertion.afterState)
const { node: forceCreatedNode2 } = await forceCreateNode(
rollupAdmin.connect(await impersonateAccount(upgradeExecutor)),
sequencerInbox,
prevNode,
adminAssertion2,
forceCreatedNode1
)
const postLatestCreated = await rollup.rollup.latestNodeCreated()
await rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.forceConfirmNode(
adminNodeNum,
adminAssertion.afterState.globalState.bytes32Vals[0],
adminAssertion.afterState.globalState.bytes32Vals[1]
)
const postLatestConfirmed = await rollup.rollup.latestConfirmed()
expect(postLatestCreated).to.equal(adminNodeNum.add(1))
expect(postLatestConfirmed).to.equal(adminNodeNum)
await rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.resume()
// should create node after resuming
prevNode = forceCreatedNode1
await tryAdvanceChain(minimumAssertionPeriod)
await expect(
makeSimpleNode(
rollup.connect(validators[2]),
sequencerInbox,
prevNode,
undefined,
forceCreatedNode2,
stake
)
).to.be.revertedWith('STAKER_IS_ZOMBIE')
await expect(
makeSimpleNode(rollup.connect(validators[2]), sequencerInbox, prevNode)
).to.be.revertedWith('NOT_STAKED')
await rollup.rollup.connect(validators[2]).removeOldZombies(0)
await makeSimpleNode(
rollup.connect(validators[2]),
sequencerInbox,
prevNode,
undefined,
forceCreatedNode2,
stake
)
})
it('should initialize a fresh rollup', async function () {
const {
rollupAdmin: rollupAdminContract,
rollupUser: rollupUserContract,
admin: adminI,
validators: validatorsI,
batchPosterManager: batchPosterManagerI,
upgradeExecutorAddress,
} = await setup()
rollupAdmin = rollupAdminContract
rollupUser = rollupUserContract
admin = adminI
validators = validatorsI
upgradeExecutor = upgradeExecutorAddress
rollup = new RollupContract(rollupUser.connect(validators[0]))
batchPosterManager = batchPosterManagerI
})
it('should stake on initial node again', async function () {
await tryAdvanceChain(minimumAssertionPeriod)
const initNode: {
assertion: { afterState: ExecutionStateStruct }
nodeNum: number
nodeHash: BytesLike
inboxMaxCount: BigNumber
} = {
assertion: {
afterState: {
globalState: {
bytes32Vals: [zerobytes32, zerobytes32],
u64Vals: [0, 0],
},
machineStatus: MachineStatus.FINISHED,
},
},
inboxMaxCount: BigNumber.from(1),
nodeHash: zerobytes32,
nodeNum: 0,
}
const stake = await rollup.currentRequiredStake()
const { node } = await makeSimpleNode(
rollup,
sequencerInbox,
initNode,
undefined,
undefined,
stake
)
updatePrevNode(node)
})
it('should only allow admin to upgrade primary logic', async function () {
const user = rollupUser.signer
// store the current implementation addresses
const proxyPrimaryTarget0 = await getDoubleLogicUUPSTarget(
'admin',
user.provider!
)
const proxySecondaryTarget0 = await getDoubleLogicUUPSTarget(
'user',
user.provider!
)
// deploy a new admin logic
const rollupAdminLogicFac = (await ethers.getContractFactory(
'RollupAdminLogic'
)) as RollupAdminLogic__factory
const newAdminLogicImpl = await rollupAdminLogicFac.deploy()
// attempt to upgrade as user, should revert
await expect(rollupAdmin.connect(user).upgradeTo(newAdminLogicImpl.address))
.to.be.reverted
// upgrade as admin
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.upgradeTo(newAdminLogicImpl.address)
).to.emit(rollupAdmin, 'Upgraded')
// check the new implementation address is set
const proxyPrimaryTarget = await getDoubleLogicUUPSTarget(
'admin',
user.provider!
)
await expect(proxyPrimaryTarget).to.not.eq(proxyPrimaryTarget0)
await expect(proxyPrimaryTarget).to.eq(
newAdminLogicImpl.address.toLowerCase()
)
// check the other implementation address is unchanged
const proxySecondaryTarget = await getDoubleLogicUUPSTarget(
'user',
user.provider!
)
await expect(proxySecondaryTarget).to.eq(proxySecondaryTarget0)
})
it('should only allow admin to upgrade secondary logic', async function () {
const user = rollupUser.signer
// store the current implementation addresses
const proxyPrimaryTarget0 = await getDoubleLogicUUPSTarget(
'admin',
user.provider!
)
const proxySecondaryTarget0 = await getDoubleLogicUUPSTarget(
'user',
user.provider!
)
// deploy a new user logic
const rollupUserLogicFac = (await ethers.getContractFactory(
'RollupUserLogic'
)) as RollupUserLogic__factory
const newUserLogicImpl = await rollupUserLogicFac.deploy()
// attempt to upgrade as user, should revert
await expect(
rollupAdmin.connect(user).upgradeSecondaryTo(newUserLogicImpl.address)
).to.be.reverted
// upgrade as admin
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.upgradeSecondaryTo(newUserLogicImpl.address)
).to.emit(rollupAdmin, 'UpgradedSecondary')
// check the new implementation address is set
const proxySecondaryTarget = await getDoubleLogicUUPSTarget(
'user',
user.provider!
)
await expect(proxySecondaryTarget).to.not.eq(proxySecondaryTarget0)
await expect(proxySecondaryTarget).to.eq(
newUserLogicImpl.address.toLowerCase()
)
// check the other implementation address is unchanged
const proxyPrimaryTarget = await getDoubleLogicUUPSTarget(
'admin',
user.provider!
)
await expect(proxyPrimaryTarget).to.eq(proxyPrimaryTarget0)
})
it('should allow admin to upgrade primary logic and call', async function () {
const rollupAdminLogicFac = (await ethers.getContractFactory(
'RollupAdminLogic'
)) as RollupAdminLogic__factory
const newAdminLogicImpl = await rollupAdminLogicFac.deploy()
// first pause the contract so we can unpause after upgrade
await rollupAdmin.connect(await impersonateAccount(upgradeExecutor)).pause()
// 0x046f7da2 - resume()
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.upgradeToAndCall(newAdminLogicImpl.address, '0x046f7da2')
).to.emit(rollupAdmin, 'Unpaused')
})
it('should allow admin to upgrade secondary logic and call', async function () {
const rollupUserLogicFac = (await ethers.getContractFactory(
'RollupUserLogic'
)) as RollupUserLogic__factory
const newUserLogicImpl = await rollupUserLogicFac.deploy()
// this call should revert since the user logic don't have a fallback
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.upgradeSecondaryToAndCall(newUserLogicImpl.address, '0x')
).to.revertedWith('Address: low-level delegate call failed')
// 0x8da5cb5b - owner() (some random function that will not revert)
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.upgradeSecondaryToAndCall(newUserLogicImpl.address, '0x8da5cb5b')
).to.emit(rollupAdmin, 'UpgradedSecondary')
})
it('should fail upgrade to unsafe primary logic', async function () {
const rollupUserLogicFac = (await ethers.getContractFactory(
'RollupUserLogic'
)) as RollupUserLogic__factory
const newUserLogicImpl = await rollupUserLogicFac.deploy()
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.upgradeTo(newUserLogicImpl.address)
).to.revertedWith('ERC1967Upgrade: unsupported proxiableUUID')
})
it('should fail upgrade to unsafe secondary logic', async function () {
const rollupAdminLogicFac = (await ethers.getContractFactory(
'RollupAdminLogic'
)) as RollupAdminLogic__factory
const newAdminLogicImpl = await rollupAdminLogicFac.deploy()
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.upgradeSecondaryTo(newAdminLogicImpl.address)
).to.revertedWith('ERC1967Upgrade: unsupported secondary proxiableUUID')
})
it('should fail upgrade to proxy primary logic', async function () {
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.upgradeTo(rollupAdmin.address)
).to.revertedWith('ERC1967Upgrade: new implementation is not UUPS')
})
it('should fail upgrade to proxy secondary logic', async function () {
await expect(
rollupAdmin
.connect(await impersonateAccount(upgradeExecutor))
.upgradeSecondaryTo(rollupAdmin.address)
).to.revertedWith(
'ERC1967Upgrade: new secondary implementation is not UUPS'
)
})
it('should fail to init rollupAdminLogic without proxy', async function () {
const user = rollupUser.signer
const rollupAdminLogicFac = (await ethers.getContractFactory(
'RollupAdminLogic'
)) as RollupAdminLogic__factory
const proxyPrimaryTarget = await getDoubleLogicUUPSTarget(
'admin',
user.provider!
)
const proxyPrimaryImpl = rollupAdminLogicFac.attach(proxyPrimaryTarget)
await expect(
proxyPrimaryImpl.initialize(await getDefaultConfig(), {
challengeManager: constants.AddressZero,
bridge: constants.AddressZero,
inbox: constants.AddressZero,
outbox: constants.AddressZero,
rollupAdminLogic: constants.AddressZero,
rollupEventInbox: constants.AddressZero,
rollupUserLogic: constants.AddressZero,
sequencerInbox: constants.AddressZero,
validatorUtils: constants.AddressZero,
validatorWalletCreator: constants.AddressZero,
})
).to.be.revertedWith('Function must be called through delegatecall')
})
it('should fail to init rollupUserLogic without proxy', async function () {
const user = rollupUser.signer
const rollupUserLogicFac = (await ethers.getContractFactory(
'RollupUserLogic'
)) as RollupUserLogic__factory
const proxySecondaryTarget = await getDoubleLogicUUPSTarget(
'user',
user.provider!
)
const proxySecondaryImpl = rollupUserLogicFac.attach(proxySecondaryTarget)
await expect(
proxySecondaryImpl.interface.functions['initialize(address)']
.stateMutability
).to.eq('view')
})
it('can set is sequencer', async function () {
const testAddress = await accounts[9].getAddress()
expect(await sequencerInbox.isSequencer(testAddress)).to.be.false
await expect(
sequencerInbox.setIsSequencer(testAddress, true)
).to.revertedWith(
`NotBatchPosterManager("${await sequencerInbox.signer.getAddress()}")`
)
expect(await sequencerInbox.isSequencer(testAddress)).to.be.false
await (
await sequencerInbox
.connect(batchPosterManager)
.setIsSequencer(testAddress, true)
).wait()
expect(await sequencerInbox.isSequencer(testAddress)).to.be.true
await (
await sequencerInbox
.connect(batchPosterManager)
.setIsSequencer(testAddress, false)
).wait()
expect(await sequencerInbox.isSequencer(testAddress)).to.be.false
})
it('can set a batch poster', async function () {
const testAddress = await accounts[9].getAddress()
expect(await sequencerInbox.isBatchPoster(testAddress)).to.be.false
await expect(
sequencerInbox.setIsBatchPoster(testAddress, true)
).to.revertedWith(
`NotBatchPosterManager("${await sequencerInbox.signer.getAddress()}")`
)
expect(await sequencerInbox.isBatchPoster(testAddress)).to.be.false
await (
await sequencerInbox
.connect(batchPosterManager)
.setIsBatchPoster(testAddress, true)
).wait()
expect(await sequencerInbox.isBatchPoster(testAddress)).to.be.true
await (
await sequencerInbox
.connect(batchPosterManager)
.setIsBatchPoster(testAddress, false)
).wait()
expect(await sequencerInbox.isBatchPoster(testAddress)).to.be.false
})
it('can set batch poster manager', async function () {
const testManager = await accounts[8].getAddress()
expect(await sequencerInbox.batchPosterManager()).to.eq(
await batchPosterManager.getAddress()
)
await expect(
sequencerInbox.connect(accounts[8]).setBatchPosterManager(testManager)
).to.revertedWith(`NotOwner("${testManager}", "${upgradeExecutor}")`)
expect(await sequencerInbox.batchPosterManager()).to.eq(
await batchPosterManager.getAddress()
)
await (
await sequencerInbox
.connect(await impersonateAccount(upgradeExecutor))
.setBatchPosterManager(testManager)
).wait()
expect(await sequencerInbox.batchPosterManager()).to.eq(testManager)
})
it('should fail the chainid fork check', async function () {
await expect(sequencerInbox.removeDelayAfterFork()).to.revertedWith(
'NotForked()'
)
})
it('should fail the batch poster check', async function () {
await expect(
sequencerInbox.addSequencerL2Batch(
0,
'0x',
0,
ethers.constants.AddressZero,
0,
0
)
).to.revertedWith('NotBatchPoster()')
})
it('should fail the onlyValidator check', async function () {
await expect(rollupUser.withdrawStakerFunds()).to.revertedWith(
'NOT_VALIDATOR'
)
})
it('should fail to call removeWhitelistAfterFork', async function () {
await expect(rollupUser.removeWhitelistAfterFork()).to.revertedWith(
'CHAIN_ID_NOT_CHANGED'
)
})
it('should fail to call removeWhitelistAfterValidatorAfk', async function () {
await expect(rollupUser.removeWhitelistAfterValidatorAfk()).to.revertedWith(
'VALIDATOR_NOT_AFK'
)
})
})