diff --git a/.changeset/pink-cameras-reflect.md b/.changeset/pink-cameras-reflect.md new file mode 100644 index 0000000000000..6790e00c1dd3f --- /dev/null +++ b/.changeset/pink-cameras-reflect.md @@ -0,0 +1,6 @@ +--- +"@eth-optimism/contracts": patch +"@eth-optimism/hardhat-ovm": patch +--- + +Use optimistic-solc to compile the SequencerEntrypoint. Also introduces a cache invalidation mechanism for hardhat-ovm so that we can push new compiler versions. diff --git a/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_SequencerEntrypoint.sol b/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_SequencerEntrypoint.sol index bba7dd59c90fc..2b2911013d877 100644 --- a/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_SequencerEntrypoint.sol +++ b/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_SequencerEntrypoint.sol @@ -1,11 +1,15 @@ // SPDX-License-Identifier: MIT +// @unsupported: evm pragma solidity >0.5.0 <0.8.0; +/* Interface Imports */ +import { iOVM_ECDSAContractAccount } from "../../iOVM/accounts/iOVM_ECDSAContractAccount.sol"; + /* Library Imports */ import { Lib_BytesUtils } from "../../libraries/utils/Lib_BytesUtils.sol"; import { Lib_OVMCodec } from "../../libraries/codec/Lib_OVMCodec.sol"; import { Lib_ECDSAUtils } from "../../libraries/utils/Lib_ECDSAUtils.sol"; -import { Lib_SafeExecutionManagerWrapper } from "../../libraries/wrappers/Lib_SafeExecutionManagerWrapper.sol"; +import { Lib_ExecutionManagerWrapper } from "../../libraries/wrappers/Lib_ExecutionManagerWrapper.sol"; /** * @title OVM_SequencerEntrypoint @@ -15,7 +19,7 @@ import { Lib_SafeExecutionManagerWrapper } from "../../libraries/wrappers/Lib_Sa * This contract is the implementation referenced by the Proxy Sequencer Entrypoint, thus enabling * the Optimism team to upgrade the decompression of calldata from the Sequencer. * - * Compiler used: solc + * Compiler used: optimistic-solc * Runtime target: OVM */ contract OVM_SequencerEntrypoint { @@ -59,43 +63,56 @@ contract OVM_SequencerEntrypoint { bytes memory compressedTx = Lib_BytesUtils.slice(msg.data, 66); bool isEthSignedMessage = transactionType == TransactionType.ETH_SIGNED_MESSAGE; + // Grab the chain ID for the current network. + uint256 chainId; + assembly { + chainId := chainid() + } + // Need to decompress and then re-encode the transaction based on the original encoding. bytes memory encodedTx = Lib_OVMCodec.encodeEIP155Transaction( - Lib_OVMCodec.decompressEIP155Transaction(compressedTx), + Lib_OVMCodec.decompressEIP155Transaction( + compressedTx, + chainId + ), isEthSignedMessage ); address target = Lib_ECDSAUtils.recover( encodedTx, isEthSignedMessage, - uint8(v), + v, r, s ); - if (Lib_SafeExecutionManagerWrapper.safeEXTCODESIZE(target) == 0) { + bool isEmptyContract; + assembly { + isEmptyContract := iszero(extcodesize(target)) + } + + if (isEmptyContract) { // ProxyEOA has not yet been deployed for this EOA. bytes32 messageHash = Lib_ECDSAUtils.getMessageHash(encodedTx, isEthSignedMessage); - Lib_SafeExecutionManagerWrapper.safeCREATEEOA(messageHash, uint8(v), r, s); + Lib_ExecutionManagerWrapper.ovmCREATEEOA(messageHash, v, r, s); } - // ProxyEOA has been deployed for this EOA, continue to CALL. - bytes memory callbytes = abi.encodeWithSignature( - "execute(bytes,uint8,uint8,bytes32,bytes32)", + Lib_OVMCodec.EOASignatureType sigtype; + if (isEthSignedMessage) { + sigtype = Lib_OVMCodec.EOASignatureType.ETH_SIGNED_MESSAGE; + } else { + sigtype = Lib_OVMCodec.EOASignatureType.EIP155_TRANSACTION; + } + + iOVM_ECDSAContractAccount(target).execute( encodedTx, - isEthSignedMessage, - uint8(v), + sigtype, + v, r, s ); - - Lib_SafeExecutionManagerWrapper.safeCALL( - gasleft(), - target, - callbytes - ); } - + /********************** * Internal Functions * @@ -119,9 +136,7 @@ contract OVM_SequencerEntrypoint { } if (_transactionType == 2) { return TransactionType.ETH_SIGNED_MESSAGE; } else { - Lib_SafeExecutionManagerWrapper.safeREVERT( - "Transaction type must be 0 or 2" - ); + revert("Transaction type must be 0 or 2"); } } } diff --git a/packages/contracts/contracts/optimistic-ethereum/libraries/codec/Lib_OVMCodec.sol b/packages/contracts/contracts/optimistic-ethereum/libraries/codec/Lib_OVMCodec.sol index 0cb7245956a5b..42c9d2e8b614d 100644 --- a/packages/contracts/contracts/optimistic-ethereum/libraries/codec/Lib_OVMCodec.sol +++ b/packages/contracts/contracts/optimistic-ethereum/libraries/codec/Lib_OVMCodec.sol @@ -155,10 +155,12 @@ library Lib_OVMCodec { /** * Decompresses a compressed EIP155 transaction. * @param _transaction Compressed EIP155 transaction bytes. + * @param _chainId Chain ID this transaction was signed with. * @return Transaction parsed into a struct. */ function decompressEIP155Transaction( - bytes memory _transaction + bytes memory _transaction, + uint256 _chainId ) internal returns ( @@ -171,7 +173,7 @@ library Lib_OVMCodec { nonce: Lib_BytesUtils.toUint24(_transaction, 6), to: Lib_BytesUtils.toAddress(_transaction, 9), data: Lib_BytesUtils.slice(_transaction, 29), - chainId: Lib_SafeExecutionManagerWrapper.safeCHAINID(), + chainId: _chainId, value: 0 }); } diff --git a/packages/contracts/contracts/optimistic-ethereum/libraries/wrappers/Lib_ExecutionManagerWrapper.sol b/packages/contracts/contracts/optimistic-ethereum/libraries/wrappers/Lib_ExecutionManagerWrapper.sol new file mode 100644 index 0000000000000..1546922a6fc32 --- /dev/null +++ b/packages/contracts/contracts/optimistic-ethereum/libraries/wrappers/Lib_ExecutionManagerWrapper.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +// @unsupported: evm +pragma solidity >0.5.0 <0.8.0; + +/* Library Imports */ +import { Lib_ErrorUtils } from "../utils/Lib_ErrorUtils.sol"; + +/** + * @title Lib_ExecutionManagerWrapper + * + * Compiler used: solc + * Runtime target: OVM + */ +library Lib_ExecutionManagerWrapper { + + /********************** + * Internal Functions * + **********************/ + + /** + * Performs a safe ovmGETNONCE call. + * @return _nonce Result of calling ovmGETNONCE. + */ + function ovmGETNONCE() + internal + returns ( + uint256 _nonce + ) + { + bytes memory returndata = _safeExecutionManagerInteraction( + abi.encodeWithSignature( + "ovmGETNONCE()" + ) + ); + + return abi.decode(returndata, (uint256)); + } + + /** + * Performs a safe ovmINCREMENTNONCE call. + */ + function ovmINCREMENTNONCE() + internal + { + _safeExecutionManagerInteraction( + abi.encodeWithSignature( + "ovmINCREMENTNONCE()" + ) + ); + } + + /** + * Performs a safe ovmCREATEEOA call. + * @param _messageHash Message hash which was signed by EOA + * @param _v v value of signature (0 or 1) + * @param _r r value of signature + * @param _s s value of signature + */ + function ovmCREATEEOA( + bytes32 _messageHash, + uint8 _v, + bytes32 _r, + bytes32 _s + ) + internal + { + _safeExecutionManagerInteraction( + abi.encodeWithSignature( + "ovmCREATEEOA(bytes32,uint8,bytes32,bytes32)", + _messageHash, + _v, + _r, + _s + ) + ); + } + + /** + * Calls the ovmL1TXORIGIN opcode. + * @return Address that sent this message from L1. + */ + function ovmL1TXORIGIN() + internal + returns ( + address + ) + { + bytes memory returndata = _safeExecutionManagerInteraction( + abi.encodeWithSignature( + "ovmL1TXORIGIN()" + ) + ); + + return abi.decode(returndata, (address)); + } + + + /********************* + * Private Functions * + *********************/ + + /** + * Performs an ovm interaction and the necessary safety checks. + * @param _calldata Data to send to the OVM_ExecutionManager (encoded with sighash). + * @return _returndata Data sent back by the OVM_ExecutionManager. + */ + function _safeExecutionManagerInteraction( + bytes memory _calldata + ) + private + returns ( + bytes memory + ) + { + bytes memory returndata; + assembly { + // kall is a custom yul builtin within optimistic-solc that allows us to directly call + // the execution manager (since `call` would be compiled). + kall(add(_calldata, 0x20), mload(_calldata), 0x0, 0x0) + let size := returndatasize() + returndata := mload(0x40) + mstore(0x40, add(returndata, and(add(add(size, 0x20), 0x1f), not(0x1f)))) + mstore(returndata, size) + returndatacopy(add(returndata, 0x20), 0x0, size) + } + return returndata; + } +} diff --git a/packages/contracts/contracts/test-libraries/codec/TestLib_OVMCodec.sol b/packages/contracts/contracts/test-libraries/codec/TestLib_OVMCodec.sol index b9046ac8a7766..d162c1216e0e0 100644 --- a/packages/contracts/contracts/test-libraries/codec/TestLib_OVMCodec.sol +++ b/packages/contracts/contracts/test-libraries/codec/TestLib_OVMCodec.sol @@ -48,13 +48,17 @@ contract TestLib_OVMCodec { } function decompressEIP155Transaction( - bytes memory _transaction + bytes memory _transaction, + uint256 _chainId ) public returns ( Lib_OVMCodec.EIP155Transaction memory _decompressed ) { - return Lib_OVMCodec.decompressEIP155Transaction(_transaction); + return Lib_OVMCodec.decompressEIP155Transaction( + _transaction, + _chainId + ); } } diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index c13241a43516b..701b3511b32e9 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -48,6 +48,9 @@ const config: HardhatUserConfig = { }, }, }, + ovm: { + solcVersion: '0.7.6-allow_kall_2', // temporary until we fix the build for 0.7.6 + }, typechain: { outDir: 'dist/types', target: 'ethers-v5', diff --git a/packages/contracts/src/contract-deployment/config.ts b/packages/contracts/src/contract-deployment/config.ts index 5ddfc87b88ef7..db5c10b6b4d34 100644 --- a/packages/contracts/src/contract-deployment/config.ts +++ b/packages/contracts/src/contract-deployment/config.ts @@ -220,7 +220,7 @@ export const makeContractDeployConfig = async ( factory: getContractFactory('OVM_ECDSAContractAccount'), }, OVM_SequencerEntrypoint: { - factory: getContractFactory('OVM_SequencerEntrypoint'), + factory: getContractFactory('OVM_SequencerEntrypoint', undefined, true), }, OVM_ProxySequencerEntrypoint: { factory: getContractFactory('OVM_ProxySequencerEntrypoint'), diff --git a/packages/contracts/src/contract-dumps.ts b/packages/contracts/src/contract-dumps.ts index 697f35f1206d5..baffa384cffd9 100644 --- a/packages/contracts/src/contract-dumps.ts +++ b/packages/contracts/src/contract-dumps.ts @@ -164,6 +164,7 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise => { const ovmCompiled = [ 'OVM_L2ToL1MessagePasser', 'OVM_L2CrossDomainMessenger', + 'OVM_SequencerEntrypoint', 'Lib_AddressManager', 'OVM_ETH', ] @@ -211,12 +212,19 @@ export const makeStateDump = async (cfg: RollupDeployConfig): Promise => { predeploys[name] || `0xdeaddeaddeaddeaddeaddeaddeaddeaddead${i.toString(16).padStart(4, '0')}` + let def: any + try { + def = getContractDefinition(name.replace('Proxy__', '')) + } catch (err) { + def = getContractDefinition(name.replace('Proxy__', ''), true) + } + dump.accounts[name] = { address: deadAddress, code, codeHash: keccak256(code), storage: await getStorageDump(cStateManager, contract.address), - abi: getContractDefinition(name.replace('Proxy__', '')).abi, + abi: def.abi, } } diff --git a/packages/contracts/test/contracts/OVM/precompiles/OVM_ProxySequencerEntrypoint.spec.ts b/packages/contracts/test/contracts/OVM/precompiles/OVM_ProxySequencerEntrypoint.spec.ts index ec1e40fac3736..4407ff157140f 100644 --- a/packages/contracts/test/contracts/OVM/precompiles/OVM_ProxySequencerEntrypoint.spec.ts +++ b/packages/contracts/test/contracts/OVM/precompiles/OVM_ProxySequencerEntrypoint.spec.ts @@ -8,6 +8,7 @@ import { remove0x } from '@eth-optimism/core-utils' /* Internal Imports */ import { decodeSolidityError } from '../../../helpers' +import { getContractFactory } from '../../../../src' const callPredeploy = async ( Helper_PredeployCaller: Contract, @@ -59,8 +60,10 @@ describe('OVM_ProxySequencerEntrypoint', () => { Helper_PredeployCaller.setTarget(Mock__OVM_ExecutionManager.address) - OVM_SequencerEntrypoint = await ( - await ethers.getContractFactory('OVM_SequencerEntrypoint') + OVM_SequencerEntrypoint = await getContractFactory( + 'OVM_SequencerEntrypoint', + wallet, + true ).deploy() }) diff --git a/packages/contracts/test/contracts/OVM/precompiles/OVM_SequencerEntrypoint.spec.ts b/packages/contracts/test/contracts/OVM/precompiles/OVM_SequencerEntrypoint.spec.ts index 2979ad3bc33eb..02f613ce3aa8f 100644 --- a/packages/contracts/test/contracts/OVM/precompiles/OVM_SequencerEntrypoint.spec.ts +++ b/packages/contracts/test/contracts/OVM/precompiles/OVM_SequencerEntrypoint.spec.ts @@ -2,11 +2,12 @@ import { expect } from '../../../setup' /* External Imports */ import { waffle, ethers } from 'hardhat' -import { ContractFactory, Wallet, Contract } from 'ethers' +import { ContractFactory, Wallet, Contract, BigNumber } from 'ethers' import { smockit, MockContract } from '@eth-optimism/smock' +import { fromHexString, toHexString } from '@eth-optimism/core-utils' /* Internal Imports */ -import { getContractInterface } from '../../../../src' +import { getContractInterface, getContractFactory } from '../../../../src' import { encodeSequencerCalldata, signNativeTransaction, @@ -31,7 +32,38 @@ describe('OVM_SequencerEntrypoint', () => { ) Mock__OVM_ExecutionManager.smocked.ovmCHAINID.will.return.with(420) - Mock__OVM_ExecutionManager.smocked.ovmCALL.will.return.with([true, '0x']) + Mock__OVM_ExecutionManager.smocked.ovmCALL.will.return.with( + (gasLimit, target, data) => { + if (target === wallet.address) { + return [ + true, + iOVM_ECDSAContractAccount.encodeFunctionResult('execute', [ + true, + '0x', + ]), + ] + } else { + return [true, '0x'] + } + } + ) + Mock__OVM_ExecutionManager.smocked.ovmSTATICCALL.will.return.with( + (gasLimit, target, data) => { + // Duplicating the behavior of the ecrecover precompile. + if (target === '0x0000000000000000000000000000000000000001') { + const databuf = fromHexString(data) + const addr = ethers.utils.recoverAddress(databuf.slice(0, 32), { + v: BigNumber.from(databuf.slice(32, 64)).toNumber(), + r: toHexString(databuf.slice(64, 96)), + s: toHexString(databuf.slice(96, 128)), + }) + const ret = ethers.utils.defaultAbiCoder.encode(['address'], [addr]) + return [true, ret] + } else { + return [true, '0x'] + } + } + ) Helper_PredeployCaller = await ( await ethers.getContractFactory('Helper_PredeployCaller') @@ -42,11 +74,18 @@ describe('OVM_SequencerEntrypoint', () => { let OVM_SequencerEntrypointFactory: ContractFactory before(async () => { - OVM_SequencerEntrypointFactory = await ethers.getContractFactory( - 'OVM_SequencerEntrypoint' + OVM_SequencerEntrypointFactory = getContractFactory( + 'OVM_SequencerEntrypoint', + wallet, + true ) }) + const iOVM_ECDSAContractAccount = getContractInterface( + 'OVM_ECDSAContractAccount', + true + ) + let OVM_SequencerEntrypoint: Contract beforeEach(async () => { OVM_SequencerEntrypoint = await OVM_SequencerEntrypointFactory.deploy() @@ -69,15 +108,16 @@ describe('OVM_SequencerEntrypoint', () => { const encodedTx = serializeNativeTransaction(DEFAULT_EIP155_TX) const sig = await signNativeTransaction(wallet, DEFAULT_EIP155_TX) - const expectedEOACalldata = getContractInterface( - 'OVM_ECDSAContractAccount' - ).encodeFunctionData('execute', [ - encodedTx, - 0, //isEthSignedMessage - `0x${sig.v}`, //v - `0x${sig.r}`, //r - `0x${sig.s}`, //s - ]) + const expectedEOACalldata = iOVM_ECDSAContractAccount.encodeFunctionData( + 'execute', + [ + encodedTx, + 0, //isEthSignedMessage + `0x${sig.v}`, //v + `0x${sig.r}`, //r + `0x${sig.s}`, //s + ] + ) const ovmCALL: any = Mock__OVM_ExecutionManager.smocked.ovmCALL.calls[0] expect(ovmCALL._address).to.equal(await wallet.getAddress()) expect(ovmCALL._calldata).to.equal(expectedEOACalldata) @@ -94,15 +134,16 @@ describe('OVM_SequencerEntrypoint', () => { const encodedTx = serializeNativeTransaction(createTx) const sig = await signNativeTransaction(wallet, createTx) - const expectedEOACalldata = getContractInterface( - 'OVM_ECDSAContractAccount' - ).encodeFunctionData('execute', [ - encodedTx, - 0, //isEthSignedMessage - `0x${sig.v}`, //v - `0x${sig.r}`, //r - `0x${sig.s}`, //s - ]) + const expectedEOACalldata = iOVM_ECDSAContractAccount.encodeFunctionData( + 'execute', + [ + encodedTx, + 0, //isEthSignedMessage + `0x${sig.v}`, //v + `0x${sig.r}`, //r + `0x${sig.s}`, //s + ] + ) const ovmCALL: any = Mock__OVM_ExecutionManager.smocked.ovmCALL.calls[0] expect(ovmCALL._address).to.equal(await wallet.getAddress()) expect(ovmCALL._calldata).to.equal(expectedEOACalldata) @@ -110,7 +151,17 @@ describe('OVM_SequencerEntrypoint', () => { for (let i = 0; i < 3; i += 2) { it(`should call ovmCreateEOA when tx type is ${i} and ovmEXTCODESIZE returns 0`, async () => { - Mock__OVM_ExecutionManager.smocked.ovmEXTCODESIZE.will.return.with(0) + let firstCheck = true + Mock__OVM_ExecutionManager.smocked.ovmEXTCODESIZE.will.return.with( + () => { + if (firstCheck) { + firstCheck = false + return 0 + } else { + return 1 + } + } + ) const calldata = await encodeSequencerCalldata( wallet, DEFAULT_EIP155_TX, @@ -145,15 +196,16 @@ describe('OVM_SequencerEntrypoint', () => { const encodedTx = serializeEthSignTransaction(DEFAULT_EIP155_TX) const sig = await signEthSignMessage(wallet, DEFAULT_EIP155_TX) - const expectedEOACalldata = getContractInterface( - 'OVM_ECDSAContractAccount' - ).encodeFunctionData('execute', [ - encodedTx, - 1, //isEthSignedMessage - `0x${sig.v}`, //v - `0x${sig.r}`, //r - `0x${sig.s}`, //s - ]) + const expectedEOACalldata = iOVM_ECDSAContractAccount.encodeFunctionData( + 'execute', + [ + encodedTx, + 1, //isEthSignedMessage + `0x${sig.v}`, //v + `0x${sig.r}`, //r + `0x${sig.s}`, //s + ] + ) const ovmCALL: any = Mock__OVM_ExecutionManager.smocked.ovmCALL.calls[0] expect(ovmCALL._address).to.equal(await wallet.getAddress()) expect(ovmCALL._calldata).to.equal(expectedEOACalldata) diff --git a/packages/contracts/test/data/json/libraries/codec/Lib_OVMCodec.test.json b/packages/contracts/test/data/json/libraries/codec/Lib_OVMCodec.test.json index b442f4e07c8cf..6808dcef02d4d 100644 --- a/packages/contracts/test/data/json/libraries/codec/Lib_OVMCodec.test.json +++ b/packages/contracts/test/data/json/libraries/codec/Lib_OVMCodec.test.json @@ -3,7 +3,8 @@ "decompressEIP155Transaction": { "decompression": { "in": [ - "0x0001f4000064000064121212121212121212121212121212121212121299999999999999999999" + "0x0001f4000064000064121212121212121212121212121212121212121299999999999999999999", + 420 ], "out": [ [ diff --git a/packages/hardhat-ovm/src/index.ts b/packages/hardhat-ovm/src/index.ts index 40d7212839056..727fd40014dee 100644 --- a/packages/hardhat-ovm/src/index.ts +++ b/packages/hardhat-ovm/src/index.ts @@ -13,6 +13,9 @@ import { /* Imports: Internal */ import './type-extensions' +const OPTIMISM_SOLC_VERSION_URL = + 'https://api.github.com/repos/ethereum-optimism/solc-bin/git/refs/heads/gh-pages' + const OPTIMISM_SOLC_BIN_URL = 'https://raw.githubusercontent.com/ethereum-optimism/solc-bin/gh-pages/bin' @@ -41,10 +44,32 @@ const getOvmSolcPath = async (version: string): Promise => { if (!fs.existsSync(ovmCompilersCache)) [fs.mkdirSync(ovmCompilersCache, { recursive: true })] + // Pull information about the latest commit in the solc-bin repo. We'll use this to invalidate + // our compiler cache if necessary. + const remoteCompilerVersion = await ( + await fetch(OPTIMISM_SOLC_VERSION_URL) + ).text() + + // Pull the locally stored info about the latest commit. If this differs from the remote info + // then we know to invalidate our cache. + let cachedCompilerVersion = '' + const cachedCompilerVersionPath = path.join( + ovmCompilersCache, + `version-info-${version}.json` + ) + if (fs.existsSync(cachedCompilerVersionPath)) { + cachedCompilerVersion = fs + .readFileSync(cachedCompilerVersionPath) + .toString() + } + // Check to see if we already have this compiler version downloaded. We store the cached files at // `X.Y.Z.js`. If it already exists, just return that instead of downloading a new one. const cachedCompilerPath = path.join(ovmCompilersCache, `${version}.js`) - if (fs.existsSync(cachedCompilerPath)) { + if ( + remoteCompilerVersion === cachedCompilerVersion && + fs.existsSync(cachedCompilerPath) + ) { return cachedCompilerPath } @@ -68,6 +93,7 @@ const getOvmSolcPath = async (version: string): Promise => { // figure out how to properly extend and/or hack Hardat's CompilerDownloader class. const compilerContent = await compilerContentResponse.text() fs.writeFileSync(cachedCompilerPath, compilerContent) + fs.writeFileSync(cachedCompilerVersionPath, remoteCompilerVersion) return cachedCompilerPath }