diff --git a/.changeset/polite-wasps-pay.md b/.changeset/polite-wasps-pay.md new file mode 100644 index 000000000000..ff6c6e4ef814 --- /dev/null +++ b/.changeset/polite-wasps-pay.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/contracts': patch +--- + +enables l2 upgrades to be initiated by an l1 to l2 message diff --git a/packages/contracts/contracts/chugsplash/L2ChugSplashDeployer.sol b/packages/contracts/contracts/chugsplash/L2ChugSplashDeployer.sol index e709f9d18b65..b11bc7883ad6 100644 --- a/packages/contracts/contracts/chugsplash/L2ChugSplashDeployer.sol +++ b/packages/contracts/contracts/chugsplash/L2ChugSplashDeployer.sol @@ -100,7 +100,7 @@ contract L2ChugSplashDeployer is Ownable { /*************** * Constructor * ***************/ - + /** * @param _owner Address that will initially own the L2ChugSplashDeployer. */ diff --git a/packages/contracts/contracts/chugsplash/L2ChugSplashOwner.sol b/packages/contracts/contracts/chugsplash/L2ChugSplashOwner.sol new file mode 100644 index 000000000000..933dfefaa742 --- /dev/null +++ b/packages/contracts/contracts/chugsplash/L2ChugSplashOwner.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.8.0; + +/* Library Imports */ +import { OVM_CrossDomainEnabled } from "../optimistic-ethereum/libraries/bridge/OVM_CrossDomainEnabled.sol"; + +/** + * @title L2ChugSplashOwner + * @dev This contract will be the owner of the L2ChugSplashDeployer contract on deployed networks. + * By separating this from the L2ChugSplashDeployer, we can more easily test the core ChugSplash + * logic. It's effectively just a proxy to the L2ChugSplashDeployer. + */ +contract L2ChugSplashOwner is OVM_CrossDomainEnabled { + + /********** + * Events * + **********/ + + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + + /************* + * Variables * + *************/ + + address public owner; + + + /*************** + * Constructor * + ***************/ + + /** + * @param _owner Address that will initially own the L2ChugSplashOwner. + */ + constructor( + address _owner + ) + public + OVM_CrossDomainEnabled(0x4200000000000000000000000000000000000007) + { + // Need to replicate the code from transferOwnership because transferOwnership can only be + // called via an L1 => L2 message. + require( + _owner != address(0), + "L2ChugSplashOwner: new owner is the zero address" + ); + + emit OwnershipTransferred(owner, _owner); + owner = _owner; + } + + + /******************** + * Public Functions * + ********************/ + + /** + * Leaves the contract without owner. + */ + function renounceOwnership() + public + onlyFromCrossDomainAccount(owner) + { + emit OwnershipTransferred(owner, address(0)); + owner = address(0); + } + + /** + * Transfers ownership to a new address. + * @param _newOwner Address of the new owner. + */ + function transferOwnership( + address _newOwner + ) + public + onlyFromCrossDomainAccount(owner) + { + require( + _newOwner != address(0), + "L2ChugSplashOwner: new owner is the zero address" + ); + + emit OwnershipTransferred(owner, _newOwner); + owner = _newOwner; + } + + + /********************* + * Fallback Function * + *********************/ + + fallback() + external + onlyFromCrossDomainAccount(owner) + { + (bool success, bytes memory returndata) = address( + 0x420000000000000000000000000000000000000D + ).call(msg.data); + + if (success) { + assembly { + return(add(returndata, 0x20), mload(returndata)) + } + } else { + assembly { + revert(add(returndata, 0x20), mload(returndata)) + } + } + } +} diff --git a/packages/contracts/src/contract-deployment/config.ts b/packages/contracts/src/contract-deployment/config.ts index e73b51794974..e179624caac3 100644 --- a/packages/contracts/src/contract-deployment/config.ts +++ b/packages/contracts/src/contract-deployment/config.ts @@ -5,6 +5,7 @@ import { Overrides } from '@ethersproject/contracts' /* Internal Imports */ import { getContractFactory } from '../contract-defs' +import { predeploys } from '../predeploys' export interface RollupDeployConfig { deploymentSigner: Signer @@ -260,6 +261,10 @@ export const makeContractDeployConfig = async ( ), }, L2ChugSplashDeployer: { + factory: getContractFactory('L2ChugSplashDeployer'), + params: [predeploys.L2ChugSplashOwner], + }, + L2ChugSplashOwner: { factory: getContractFactory('L2ChugSplashDeployer'), params: [config.l2ChugSplashDeployerOwner], }, diff --git a/packages/contracts/src/contract-dumps.ts b/packages/contracts/src/contract-dumps.ts index 4d0fb03b6ffa..b3b195abbf44 100644 --- a/packages/contracts/src/contract-dumps.ts +++ b/packages/contracts/src/contract-dumps.ts @@ -154,6 +154,7 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise => { 'OVM_ETH', 'OVM_ExecutionManagerWrapper', 'L2ChugSplashDeployer', + 'L2ChugSplashOwner', ], deployOverrides: {}, waitForReceipts: false, @@ -173,6 +174,7 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise => { 'OVM_ProxyEOA', 'OVM_ExecutionManagerWrapper', 'L2ChugSplashDeployer', + 'L2ChugSplashOwner', ] const deploymentResult = await deploy(config) diff --git a/packages/contracts/src/predeploys.ts b/packages/contracts/src/predeploys.ts index fbf1e0926f0b..db25ad795fdc 100644 --- a/packages/contracts/src/predeploys.ts +++ b/packages/contracts/src/predeploys.ts @@ -19,5 +19,6 @@ export const predeploys = { OVM_ProxyEOA: '0x4200000000000000000000000000000000000009', OVM_ExecutionManagerWrapper: '0x420000000000000000000000000000000000000B', L2ChugSplashDeployer: '0x420000000000000000000000000000000000000D', + L2ChugSplashOwner: '0x420000000000000000000000000000000000000E', ERC1820Registry: '0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24', } diff --git a/packages/contracts/test/contracts/chugsplash/L2ChugSplashOwner.spec.ts b/packages/contracts/test/contracts/chugsplash/L2ChugSplashOwner.spec.ts new file mode 100644 index 000000000000..bf2d90df1653 --- /dev/null +++ b/packages/contracts/test/contracts/chugsplash/L2ChugSplashOwner.spec.ts @@ -0,0 +1,234 @@ +import { expect } from '../../setup' + +/* Imports: External */ +import hre from 'hardhat' +import { ethers, Contract, Signer, ContractFactory } from 'ethers' +import { MockContract, smockit } from '@eth-optimism/smock' + +/* Imports: Internal */ +import { predeploys } from '../../../src' +import { NON_ZERO_ADDRESS } from '../../helpers' + +describe('L2ChugSplashDeployer', () => { + let signer1: Signer + let signer2: Signer + before(async () => { + ;[signer1, signer2] = await hre.ethers.getSigners() + }) + + let Mock__OVM_ExecutionManager: MockContract + let Mock__OVM_L2CrossDomainMessenger: MockContract + let Mock__OVM_L2ChugSplashDeployer: MockContract + before(async () => { + Mock__OVM_ExecutionManager = await smockit('OVM_ExecutionManager', { + address: predeploys.OVM_ExecutionManagerWrapper, + }) + Mock__OVM_L2CrossDomainMessenger = await smockit( + 'OVM_L2CrossDomainMessenger', + { + address: predeploys.OVM_L2CrossDomainMessenger, + } + ) + Mock__OVM_L2ChugSplashDeployer = await smockit('L2ChugSplashDeployer', { + address: predeploys.L2ChugSplashDeployer, + }) + }) + + let Factory__L2ChugSplashOwner: ContractFactory + before(async () => { + Factory__L2ChugSplashOwner = await hre.ethers.getContractFactory( + 'L2ChugSplashOwner' + ) + }) + + let L2ChugSplashOwner: Contract + beforeEach(async () => { + L2ChugSplashOwner = await Factory__L2ChugSplashOwner.connect( + signer1 + ).deploy( + await signer1.getAddress() // _owner + ) + }) + + describe('owner', () => { + it('should have an owner', async () => { + expect(await L2ChugSplashOwner.owner()).to.equal( + await signer1.getAddress() + ) + }) + }) + + describe('renounceOwnership', () => { + it('should revert if called directly by the owner', async () => { + await expect(L2ChugSplashOwner.connect(signer1).renounceOwnership()).to.be + .reverted + }) + + it('should revert if called directly by someone other than the owner', async () => { + await expect(L2ChugSplashOwner.connect(signer2).renounceOwnership()).to.be + .reverted + }) + + it('should revert if called by an L1 => L2 message from someone other than the owner', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + await signer2.getAddress() + ) + + await expect( + L2ChugSplashOwner.connect( + hre.ethers.provider + ).callStatic.renounceOwnership({ + from: Mock__OVM_L2CrossDomainMessenger.address, + }) + ).to.be.reverted + }) + + it('should succeed if called via an L1 => L2 message from the owner', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + await signer1.getAddress() + ) + + await expect( + L2ChugSplashOwner.connect( + hre.ethers.provider + ).callStatic.renounceOwnership({ + from: Mock__OVM_L2CrossDomainMessenger.address, + }) + ).to.not.be.reverted + }) + }) + + describe('transferOwnership', () => { + it('should revert if called directly by the owner', async () => { + await expect( + L2ChugSplashOwner.connect(signer1).transferOwnership(NON_ZERO_ADDRESS) + ).to.be.reverted + }) + + it('should revert if called directly by someone other than the owner', async () => { + await expect( + L2ChugSplashOwner.connect(signer2).transferOwnership(NON_ZERO_ADDRESS) + ).to.be.reverted + }) + + it('should revert if called by an L1 => L2 message from someone other than the owner', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + await signer2.getAddress() + ) + + await expect( + L2ChugSplashOwner.connect( + hre.ethers.provider + ).callStatic.transferOwnership(NON_ZERO_ADDRESS, { + from: Mock__OVM_L2CrossDomainMessenger.address, + }) + ).to.be.reverted + }) + + it('should succeed if called via an L1 => L2 message from the owner', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + await signer1.getAddress() + ) + + await expect( + L2ChugSplashOwner.connect( + hre.ethers.provider + ).callStatic.transferOwnership(NON_ZERO_ADDRESS, { + from: Mock__OVM_L2CrossDomainMessenger.address, + }) + ).to.not.be.reverted + }) + }) + + describe('fallback function', () => { + it('should revert if called directly by the owner', async () => { + await expect( + signer1.sendTransaction({ + to: L2ChugSplashOwner.address, + }) + ).to.be.reverted + }) + + it('should revert if called directly by someone other than the owner', async () => { + await expect( + signer2.sendTransaction({ + to: L2ChugSplashOwner.address, + }) + ).to.be.reverted + }) + + it('should revert if called by an L1 => L2 message from someone other than the owner', async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + await signer2.getAddress() + ) + + await expect( + hre.ethers.provider.call({ + to: L2ChugSplashOwner.address, + from: Mock__OVM_L2CrossDomainMessenger.address, + }) + ).to.be.reverted + }) + + describe('when called by an L1 => L2 message from the owner', async () => { + beforeEach(async () => { + Mock__OVM_L2CrossDomainMessenger.smocked.xDomainMessageSender.will.return.with( + await signer1.getAddress() + ) + }) + + it('should be able to trigger approveTransactionBundle', async () => { + await expect( + hre.ethers.provider.call({ + to: L2ChugSplashOwner.address, + from: Mock__OVM_L2CrossDomainMessenger.address, + data: Mock__OVM_L2ChugSplashDeployer.interface.encodeFunctionData( + 'approveTransactionBundle', + [ethers.constants.HashZero, ethers.BigNumber.from(0)] + ), + }) + ).to.not.be.reverted + + expect( + Mock__OVM_L2ChugSplashDeployer.smocked.approveTransactionBundle + .calls[0] + ).to.deep.equal([ethers.constants.HashZero, ethers.BigNumber.from(0)]) + }) + + it('should be able to trigger cancelTransactionBundle', async () => { + await expect( + hre.ethers.provider.call({ + to: L2ChugSplashOwner.address, + from: Mock__OVM_L2CrossDomainMessenger.address, + data: Mock__OVM_L2ChugSplashDeployer.interface.encodeFunctionData( + 'cancelTransactionBundle' + ), + }) + ).to.not.be.reverted + + expect( + Mock__OVM_L2ChugSplashDeployer.smocked.cancelTransactionBundle + .calls[0] + ).to.not.be.undefined + }) + + it('should be able to trigger overrideTransactionBundle', async () => { + await expect( + hre.ethers.provider.call({ + to: L2ChugSplashOwner.address, + from: Mock__OVM_L2CrossDomainMessenger.address, + data: Mock__OVM_L2ChugSplashDeployer.interface.encodeFunctionData( + 'overrideTransactionBundle', + [ethers.constants.HashZero, ethers.BigNumber.from(0)] + ), + }) + ).to.not.be.reverted + + expect( + Mock__OVM_L2ChugSplashDeployer.smocked.overrideTransactionBundle + .calls[0] + ).to.deep.equal([ethers.constants.HashZero, ethers.BigNumber.from(0)]) + }) + }) + }) +})