UNPKG

@hyperlane-xyz/core

Version:

Core solidity contracts for Hyperlane

1,509 lines (1,322 loc) 47 kB
/* * 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' ) }) })