# Porting Solidity Contracts to Optimism: A Guide Using Uniswap V2 This guide will walk through the process of porting an existing Solidity project to support execution on the [Optimism](https://optimism.io/) Layer 2 network. For our example codebase, we'll use the [Uniswap V2](https://uniswap.org/blog/uniswap-v2/) decentralized exchange. We'll go through the process of modifying the Uniswap repository so it can support deployment on the Optimistic Virtual Machine (OVM) and the Ethereum Virtual Machine (EVM). At the end of this guide, you'll have a test suite that runs on both the EVM and the OVM! You'll also have a good idea of what it takes to get *your* Solidity project up and running on the OVM. - [Porting Solidity Contracts to Optimism: A Guide Using Uniswap V2](#porting-solidity-contracts-to-optimism-a-guide-using-uniswap-v2) - [Background](#background) - [Prerequisites](#prerequisites) - [Optimistic Rollups](#optimistic-rollups) - [The Porting Process](#the-porting-process) - [Getting Started](#getting-started) - [Package Upgrades](#package-upgrades) - [EVM Test Suite Updates](#evm-test-suite-updates) - [Optimism Setup](#optimism-setup) - [Dependencies](#dependencies) - [Compiling for the OVM](#compiling-for-the-ovm) - [OVM vs. EVM: Constructor Arguments](#ovm-vs-evm-constructor-arguments) - [OVM vs. EVM: Block Timestamps](#ovm-vs-evm-block-timestamps) - [Trade Deadlines, `permit` signatures, and auctions](#trade-deadlines-permit-signatures-and-auctions) - [Price Oracle](#price-oracle) - [Testing on the OVM](#testing-on-the-ovm) - [Provider and Chain ID Fixes](#provider-and-chain-id-fixes) - [Gas and Compiler Fixes](#gas-and-compiler-fixes) - [OVM vs. EVM: Gas](#ovm-vs-evm-gas) - [OVM vs. EVM: Compiling](#ovm-vs-evm-compiling) - [Final Gas Tweaks](#final-gas-tweaks) - [Going Forward](#going-forward) - [OVM vs. EVM: Contract Wallets](#ovm-vs-evm-contract-wallets) - [OVM vs. EVM: Gas, Part II](#ovm-vs-evm-gas-part-ii) - [Wrapping Up](#wrapping-up) ## Background ### Prerequisites This guide is targeted at existing smart contract developers. It assumes a basic knowledge of Solidity development and the tools commonly used along with it. This includes writing our tests in JavaScript and using `yarn` for package management. If you're brand new to smart contract development, we recommend checking out some of the great resources for getting started, including the [Solidity docs](https://docs.soliditylang.org/en/latest/) themselves. The good news is, almost all of your new knowledge will eventually translate directly to building contracts for the OVM. ### Optimistic Rollups At a very high level, the OVM is an optimistic implementation of the EVM. Transactions are executed on the OVM, which enables full EVM support, and the resulting state transitions are optimistically assumed to be valid. If someone does not believe a state transition is valid, they can submit a fraud proof to be verified by the EVM. As a result, the EVM only needs to execute computations when there is a dispute about a transaction's legitimacy. If you are unfamiliar with Optimistic Rollups and the OVM, these resources can help you learn more: - [Optimistic Virtual Machine Alpha](https://medium.com/ethereum-optimism/optimistic-virtual-machine-alpha-cdf51f5d49e): A high level introduction and overview of the OVM - [Optimism: Keeping Ethereum Half-Full](https://www.youtube.com/watch?v=eYeOW4ePgZE): Video introduction with some more depth - [OVM Deep Dive](https://medium.com/ethereum-optimism/ovm-deep-dive-a300d1085f52): An in-depth article covering the challenges faced and solutions used by the OVM ### The Porting Process Most existing Solidity projects will have three categories of changes required to get things running on the OVM. 1. **Tooling updates** - The OVM currently works with the [Waffle v3](https://getwaffle.io/) testing framework. If you're using an older version of Waffle, you'll need to upgrade. If you're using a different test framework, you may need to migrate, though future versions of the OVM may support other frameworks in addition to Waffle. In this guide, we'll upgrade Uniswap from Waffle v2 to Waffle v3. 2. **Test suite updates** - In addition to updating tooling, some tests themselves will need to be modified to account for minor differences in the EVM and the OVM. These include considerations such as gas differences and chain identifiers, which we'll touch on in this guide. Our test harness will also have to support running on a local OVM node. 3. **Contract and compiler modifications** - Some differences between the EVM and the OVM precipitate changes to the Solidity contracts themselves or the compiler settings. In general, though, *most* Solidity written for the EVM should "just work" for the OVM. While we'll touch on some of the cases that might require contract changes, we'll find that the Uniswap V2 contracts will compile and run unmodified. ## Getting Started Start by cloning the [Uniswap V2 Core](https://github.com/Uniswap/uniswap-v2-core) repo. Install dependencies with `yarn` and run tests with `yarn test` to make sure all tests pass. **NOTE**: This repo currently has a dependency resolution problem when installing with a lockfile. If a `yarn.lock` file exists, it is recommended to delete it before running `yarn`. ### Package Upgrades Because we want to support running tests on the EVM and OVM, let's setup the tasks for these two test suites with the following changes to `package.json`: ```diff - "test": "mocha", - "prepublishOnly": "yarn test" + "prepublishOnly": "yarn test", + "test:evm": "yarn compile && mocha", + "test:ovm": "yarn compile && echo OVM TESTS NOT IMPLEMENTED" ``` We'll also need to update a few dependencies. Uniswap uses [Waffle](https://getwaffle.io/) v2 for their test suite, but the Optimism tooling requires Waffle v3. So let's make one more change to `package.json`: ```diff - "ethereum-waffle": "^2.4.1", + "ethereum-waffle": "^3.2.1", ``` Run `yarn` to update our dependencies, and run the new `yarn test:evm` command to run the standard EVM test suite. **It will now fail with many errors**, due to breaking changes introduced with the Waffle upgrade. In the next section, we'll resolve the errors caused by these dependency upgrades, including our jump from v4 to v5 of the [ethers](https://docs.ethers.io/v5/) JavaScript library. ### EVM Test Suite Updates To resolve the breaking changes introduced by upgrading our test suite, make the changes detailed below. Note that this section is unrelated to the core changes required to deploy and test Uniswap on the OVM, but is still required. *For more information on the breaking changes between Waffle v2 and v3, and between ethers v4 and v5, see the [Waffle](https://ethereum-waffle.readthedocs.io/en/latest/migration-guides.html#migration-from-waffle-2-5-to-waffle-3-0-0) and [ethers](https://docs.ethers.io/v5/migration/ethers-v4/) migration guides.* In `test/UniswapV2ERC20.spec.ts`: ```diff - import { MaxUint256 } from 'ethers/constants' + import { MaxUint256 } from '@ethersproject/constants' - import { bigNumberify, hexlify, keccak256, defaultAbiCoder, toUtf8Bytes } from 'ethers/utils' + import { BigNumber } from '@ethersproject/bignumber' + import { defaultAbiCoder } from '@ethersproject/abi' + import { hexlify } from '@ethersproject/bytes' + import { keccak256 } from '@ethersproject/keccak256' + import { toUtf8Bytes } from '@ethersproject/strings' const provider = new MockProvider({ + ganacheOptions: { hardfork: 'istanbul', mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', gasLimit: 9999999 + } }) - expect(await token.nonces(wallet.address)).to.eq(bigNumberify(1)) + expect(await token.nonces(wallet.address)).to.eq(BigNumber.from(1)) ``` In `test/shared/utilities.ts`: ```diff - import { Web3Provider } from 'ethers/providers' + import { Web3Provider } from '@ethersproject/providers' - import { BigNumber, bigNumberify, getAddress, keccak256, defaultAbiCoder, toUtf8Bytes, solidityPack } from 'ethers/utils' + import { BigNumber } from '@ethersproject/bignumber' + import { defaultAbiCoder } from '@ethersproject/abi' + import { getAddress } from '@ethersproject/address'; + import { keccak256 } from '@ethersproject/keccak256' + import { pack as solidityPack } from '@ethersproject/solidity' + import { toUtf8Bytes } from '@ethersproject/strings' export function expandTo18Decimals(n: number): BigNumber { - return bigNumberify(n).mul(bigNumberify(10).pow(18)) + return BigNumber.from(n).mul(BigNumber.from(10).pow(18)) } - ;(provider._web3Provider.sendAsync as any)( + ;(provider.provider.sendAsync as any)( export function encodePrice(reserve0: BigNumber, reserve1: BigNumber) { - return [reserve1.mul(bigNumberify(2).pow(112)).div(reserve0), reserve0.mul(bigNumberify(2).pow(112)).div(reserve1)] + return [reserve1.mul(BigNumber.from(2).pow(112)).div(reserve0), reserve0.mul(BigNumber.from(2).pow(112)).div(reserve1)] } ``` In `test/UniswapV2Factory.spec.ts`: ```diff - import { AddressZero } from 'ethers/constants' + import { AddressZero } from '@ethersproject/constants' - import { bigNumberify } from 'ethers/utils' + import { BigNumber } from '@ethersproject/bignumber'; const provider = new MockProvider({ + ganacheOptions: { hardfork: 'istanbul', mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', gasLimit: 9999999 + } }) - const loadFixture = createFixtureLoader(provider, [wallet, other]) + const loadFixture = createFixtureLoader([wallet, other], provider) await expect(factory.createPair(...tokens)) .to.emit(factory, 'PairCreated') - .withArgs(TEST_ADDRESSES[0], TEST_ADDRESSES[1], create2Address, bigNumberify(1)) + .withArgs(TEST_ADDRESSES[0], TEST_ADDRESSES[1], create2Address, BigNumber.from(1)) ``` In `test/shared/fixtures.ts`: ```diff - import { Web3Provider } from 'ethers/providers' + import { Web3Provider } from '@ethersproject/providers' - export async function factoryFixture(_: Web3Provider, [wallet]: Wallet[]): Promise<FactoryFixture> { + export async function factoryFixture([wallet]: Wallet[], _: Web3Provider): Promise<FactoryFixture> { const factory = await deployContract(wallet, UniswapV2Factory, [wallet.address], overrides) return { factory } } - export async function pairFixture(provider: Web3Provider, [wallet]: Wallet[]): Promise<PairFixture> { + export async function pairFixture([wallet]: Wallet[], provider: Web3Provider): Promise<PairFixture> { - const { factory } = await factoryFixture(provider, [wallet]) + const { factory } = await factoryFixture([wallet], provider) ... return { factory, token0, token1, pair } } ``` In `test/UniswapV2Pair.spec.ts`: ```diff - import { BigNumber, bigNumberify } from 'ethers/utils' + import { BigNumber } from '@ethersproject/bignumber' - import { AddressZero } from 'ethers/constants' + import { AddressZero } from '@ethersproject/constants' const provider = new MockProvider({ + ganacheOptions: { hardfork: 'istanbul', mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', gasLimit: 9999999 + } }) - const loadFixture = createFixtureLoader(provider, [wallet, other]) + const loadFixture = createFixtureLoader([wallet, other], provider) // Make the below change everywhere you see `bigNumberify` - bigNumberify + BigNumber.from ``` You can now run `yarn test:evm` and all tests should pass! ## Optimism Setup ### Dependencies Now that we've migrated to Waffle v3, let's install the tools we'll need to work with Optimism. These tools require Node v10. We recommend using a Node version manager, such as [Volta](https://docs.volta.sh/guide/getting-started). If you are using Volta, run `volta pin node@10 && volta pin yarn`. This will automatically update `package.json` to specify the required node version. (Note that if we only pin node, but not yarn, volta will complain). There's two Optimism packages we'll need: - Typically you compile contracts to be deployed on the EVM, but here we'll use [`@eth-optimism/solc`](https://www.npmjs.com/package/@eth-optimism/solc) to compile the contracts for the OVM. The Uniswap contracts use Solidity 0.5.16, so when installing this package we must specify that to ensure we get the right version. From the npm page, we see the most recent version of 0.5.16 is called `0.5.16-alpha.7` - [`@eth-optimism/ovm-toolchain`](https://www.npmjs.com/package/@eth-optimism/ovm-toolchain) provides wrappers and plugins for common tools. For example, it provides custom implementations of Ganache, Waffle V3, and Buidler which are compatible with the OVM. *(Note: even though Buidler was [replaced by Hardhat](https://medium.com/nomic-labs-blog/buidler-has-evolved-introducing-hardhat-4bccd13bc931), this package is not yet compatible with Hardhat).* Let's install both of these with: ```sh $ yarn add --dev @eth-optimism/solc@0.5.16-alpha.7 @eth-optimism/ovm-toolchain ``` ### Compiling for the OVM We're just about ready to compile our contracts for the OVM! Let's prepare a few final things first. When compiling, we need to make sure to use the OVM compiler we just installed. The existing compile script reads the EVM compiler settings from `.waffle.json`. We'll create another file to manage OVM compiler settings, called `.waffle-ovm.json`, and point it to the OVM compiler as follows: ```json { "compilerVersion": "./node_modules/@eth-optimism/solc" } ``` All compiler settings can be specified in this file, but for now this will be the only setting. Finally, let's setup our OVM `package.json` scripts. To be explicit, we'll append `:evm` to the existing scripts in `package.json`, and add a few new `:ovm` ones. The result should look like this: ```json "scripts": { "lint": "yarn prettier ./test/*.ts --check", "lint:fix": "yarn prettier ./test/*.ts --write", "clean": "rimraf ./build/", "precompile:evm": "yarn clean", "precompile:ovm": "yarn clean", "compile:evm": "waffle .waffle.json", "compile:ovm": "waffle .waffle-ovm.json", "pretest:evm": "yarn compile:evm", "pretest:ovm": "yarn compile:ovm", "test:evm": "mocha", "test:ovm": "echo OVM TESTS NOT IMPLEMENTED", "prepublishOnly": "yarn test:evm && yarn test:ovm" }, ``` Let's make sure things are working. Check that all tests still pass when running `yarn test:evm`. Now run `yarn test:ovm`. This won't run any tests yet, but it will compile the contracts for the OVM. If you did everything correctly, you should see `OVM TESTS NOT IMPLEMENTED` in the terminal, along with two compiler warnings: ``` contracts/UniswapV2Factory.sol:15:46: Warning: OVM: Taking arguments in constructor may result in unsafe code. constructor(address _feeToSetter) public { ^ contracts/test/ERC20.sol:6:43: Warning: OVM: Taking arguments in constructor may result in unsafe code. constructor(uint _totalSupply) public { ``` These are new compiler warnings specific to the OVM, so let's understand what's going on here. ### OVM vs. EVM: Constructor Arguments In this section we'll look at the warnings we saw above when compiling for the OVM. While, this guide focuses on some of the OVM vs. EVM differences that specifically impact Uniswap V2, you can read about other OVM vs. EVM differences [here](https://hackmd.io/elr0znYORiOMSTtfPJVAaA?view) and [here](https://hackmd.io/Inuu-T_UTsSXnzGtrLR8gA). `UniswapV2Factory.sol` is responsible for deploying new pairs, and its constructor takes one argument which the compiler warns may result in unsafe code. (Learn more about this argument in the Uniswap [docs](https://uniswap.org/docs/v2/smart-contracts/factory/#feetosetter)). This warning is emitted because not all EVM opcodes are supported in the OVM! As a result, when your encoded constructor arguments are added to the contract bytecode, there is a chance this encoding contains unsupported opcodes. If it does, contract deployment will fail. As explained [here](https://hackmd.io/Inuu-T_UTsSXnzGtrLR8gA#Constructor-Parameters-may-be-Unsafe): > Only a few opcodes are banned, so this is a relatively unlikely event. However, if you have a strong requirement that your contract can be successfully deployed multiple times, with absolutely any parameters, it is problematic. In that case, you will need to remove constructor arguments and replace them with an `initialize(...)` method which can only be called once on the deployed code. Because the `UniswapV2Factory` constructor simply saves the constructor argument to storage, we can safely ignore the warning here. Similarly, the test `ERC20` contract only mints `_totalSupply` tokens at construction, so this operation should also be safe. ### OVM vs. EVM: Block Timestamps The difference between block timestamps on the OVM and the EVM don't naturally surface during the migration process, but Uniswap does rely on `block.timestamp` in a few places so it's important to mention. If your contracts rely on `block.timestamp`, you'll want to understand these differences and consider their impact carefully. The OVM does not have blocks; it just has an ordered list of transactions. As such,`block.timestamp` returns the "Latest L1 Timestamp", which corresponds to the timestamp of the last L1 block in which a rollup batch was posted. Be aware that this will lag behind the EVM's `block.timestamp` by about 1–10 minutes. Uniswap uses `block.timestamp` for a few different purposes, so let's see how the behavior of each changes as a result of the differing functionality. #### Trade Deadlines, `permit` signatures, and auctions When executing a trade you can set a deadline, and if your transaction is mined after this deadline the transaction will be reverted. (This actually occurs in the [router contract](https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/UniswapV2Router02.sol), outside of the repository we cloned, but is worth mentioning anyway). The OVM timestamp lags behind the EVM, so it's possible that your OVM Uniswap trades execute up 10 minutes after your specified deadline. Similarly, you can use the `permit` method to give approval to an address to spend your LP tokens with just a signature. These signatures contain a deadline, and the approval must be sent before that deadline. Just like with trade deadlines, this means it's possible that this approval is executed after your desired deadline. One other major area timestamps come into play is with bid and auction durations. [MakerDAO](https://makerdao.com/en/) used have to bid durations of 10 minutes, and it's important to know that **due to the timestamp lag, these 10 minute durations would be unsafe on the OVM!** Note that these durations have since [increased](https://blog.makerdao.com/recent-market-activity-and-next-steps/) since it turns out they weren't ideal on L1 either. #### Price Oracle Uniswap uses a [cumulative-price variable](https://uniswap.org/docs/v2/core-concepts/oracles/) to enable developers to build safe Oracles on top of Uniswap. This variable tracks the "sum of the Uniswap price for every second in the entire history of the contract", meaning it must know the current time whenever it's updated. Because timestamps lag, the resulting price from an OVM Uniswap oracle will likely vary a bit from an EVM Uniswap oracle that had identical historical states. ## Testing on the OVM Our final step is getting our tests working for the OVM. If we make the following change to the `test:ovm` script, so it runs mocha, **there will be plenty of test failures**. Try it! ```diff "test:evm": "mocha", - "test:ovm": "echo OVM TESTS NOT IMPLEMENTED", + "test:ovm": "mocha", ``` Let's fix those failures using the `@eth-optimism/ovm-toolchain` package we installed earlier. Remember, this package provides Optimism-specific implementations of Ganache, Waffle, and Buidler. We'll need to setup the tests differently depending on whether we are running them against the EVM or OVM, so we'll use an environment variable called `MODE` to do this. If `MODE=OVM`, we'll use our new OVM settings, otherwise we'll fallback to the standard EVM configuration. We can do this easily with the following change in `package.json`: ```diff - "test:ovm": "mocha", + "test:ovm": "export MODE=OVM && mocha", ``` Next we'll create a file in `test/shared` called `config.ts`, where we'll handle all chain-specific logic. Leave this blank for now, we'll adjust it in the next section. ### Provider and Chain ID Fixes Waffle starts a local node when a new provider instance is created with `new MockProvider()`. Typically this is a normal EVM instance, but for the OVM tests we need an OVM instance. As a result we'll use the `MockProvider` from `@eth-optimism/ovm-toolchain` for the OVM tests, instead of Waffles's standard `MockProvider`. You may recall seeing `const provider = new MockProvider({...})` in the Uniswap tests, and this is exactly what we'll update. Uniswap tokens support the signature-based `permit` method to enable approvals via meta-transactions, and to avoid replay attacks the signature is based on the chain ID. While Ethereum mainnet uses a chain ID of 1, the OVM has a chain ID of 420. That means tests reliant on the chain ID will fail unless we update them. We can handle both of these changes in our `config.ts` as shown below: ```typescript= /** * @dev Handles chain-specific configuration based on whether we are running * EVM or OVM tests */ import { MockProvider } from 'ethereum-waffle' // standard EVM MockProvider from Waffle import { waffleV3 } from '@eth-optimism/ovm-toolchain' // custom OVM version of Waffle V3 // Determine which network we are on const isOVM = process.env.MODE === 'OVM' // Get provider: We keep the same provider config that Uniswap tests were // already using, but generate the provider instance based on the test mode const options: any = { ganacheOptions: { hardfork: 'istanbul', mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', gasLimit: 9999999 } } const provider = isOVM ? new waffleV3.MockProvider(options) : new MockProvider(options) // Get Chain ID const chainId = isOVM ? 420 : 1 export { provider, chainId } ``` Since `@eth-optimism/ovm-toolchain` does not have type definitions, the TypeScript compiler will complain since the new `MockProvider` implicitly has an `any` type. To avoid having to declare these types ourselves let's just edit the `tsconfig.json` to turn off this rule: ```diff= { "compilerOptions": { "target": "es5", "module": "commonjs", "strict": true, "esModuleInterop": true, "resolveJsonModule": true, + "noImplicitAny": false } } ``` We can test out our new provider and chain ID by updating all the instances in the tests, then making sure the EVM tests still run. Make the following changes in `test/UniswapV2ERC20.spec.ts` ```diff - import { solidity, MockProvider, deployContract } from 'ethereum-waffle' + import { solidity, deployContract } from 'ethereum-waffle' + import { provider } from './shared/config' - const provider = new MockProvider({ - ganacheOptions: { - hardfork: 'istanbul', - mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', - gasLimit: 9999999 - } - }) ``` Make similar changes in `test/UniswapV2Factory.spec.ts` **and** `test/UniswapV2Pair.spec.ts`: ```diff - import { solidity, MockProvider, createFixtureLoader } from 'ethereum-waffle' + import { solidity, createFixtureLoader } from 'ethereum-waffle' + import { provider } from './shared/config' - const provider = new MockProvider({ - ganacheOptions: { - hardfork: 'istanbul', - mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', - gasLimit: 9999999 - } - }) ``` We update the chain ID in `test/shared/utilities.ts`: ```diff + import { chainId } from './config' function getDomainSeparator(name: string, tokenAddress: string) { return keccak256( defaultAbiCoder.encode( ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], [ keccak256(toUtf8Bytes('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')), keccak256(toUtf8Bytes(name)), keccak256(toUtf8Bytes('1')), - 1 + chainId, tokenAddress ] ) ) } ``` and again in `test/UniswapV2ERC20.spec.ts`: ```diff - import { provider } from './shared/config' + import { provider, chainId } from './shared/config' expect(await token.DOMAIN_SEPARATOR()).to.eq( keccak256( defaultAbiCoder.encode( ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], [ keccak256( toUtf8Bytes('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') ), keccak256(toUtf8Bytes(name)), keccak256(toUtf8Bytes('1')), - 1 + chainId, token.address ] ) ) ) ``` Ok, done! Run `yarn test:evm` to make sure all tests still pass. Now run `yarn test:ovm` and some tests pass! But a lot still fail. You should see something like this: ``` UniswapV2ERC20 ✓ name, symbol, decimals, totalSupply, balanceOf, DOMAIN_SEPARATOR, PERMIT_TYPEHASH (1271ms) ✓ approve (335ms) ✓ transfer (524ms) ✓ transfer:fail (381ms) ✓ transferFrom (868ms) ✓ transferFrom:max (855ms) ✓ permit (820ms) UniswapV2Factory 1) "before each" hook for "feeTo, feeToSetter, allPairsLength" UniswapV2Pair 2) "before each" hook for "mint" 7 passing (19s) 2 failing 1) UniswapV2Factory "before each" hook for "feeTo, feeToSetter, allPairsLength": RuntimeError: VM Exception while processing transaction: out of gas at Function.RuntimeError.fromResults (node_modules/ganache-core/lib/utils/runtimeerror.js:94:13) at BlockchainDouble.processBlock (node_modules/ganache-core/lib/blockchain_double.js:627:24) at process._tickCallback (internal/process/next_tick.js:68:7) 2) UniswapV2Pair "before each" hook for "mint": RuntimeError: VM Exception while processing transaction: out of gas at Function.RuntimeError.fromResults (node_modules/ganache-core/lib/utils/runtimeerror.js:94:13) at BlockchainDouble.processBlock (node_modules/ganache-core/lib/blockchain_double.js:627:24) at process._tickCallback (internal/process/next_tick.js:68:7) ``` ### Gas and Compiler Fixes Time to fix the above test failures. The error message is `out of gas`, so maybe our gas limits aren't high enough. In `config.ts` we specify a very high gas limit of 9,999,999. What's going on? Notice the failures are in the `beforeEach` hooks. The only thing we do in those hooks is deploy a contract. It seems our contract deployment is failing with a misleading error message. Let's dig into why. #### OVM vs. EVM: Gas One important difference between the EVM and the OVM is the concept of "blocks." While EVM networks consist of an ever-growing chain of blocks, blocks don't exist on the OVM. As such, there's no concept of a *block* gas limit. Instead there is a *transaction* gas limit, which is currently set to 9,000,000. You can read about this, and other gas related differences, [here](https://hackmd.io/Inuu-T_UTsSXnzGtrLR8gA#Gas-in-the-OVM). #### OVM vs. EVM: Compiling Spoiler: Our contract deployment is failing because when we compiled it for the OVM, it became too big! Both the Ethereum mainnet and the OVM have a 24 kB limit on deployed contract size—you cannot deploy contracts bigger than this. Compiling for the OVM results in an increase in contract size compared to compiling for the EVM, and the default compiler settings are especially bloat-inducing. Fortunately, we can shrink it with the optimizer. Let's edit our `.waffle-ovm.json` file to match the compiler settings used by Uniswap, which gives us this: ```json= { "compilerVersion": "./node_modules/@eth-optimism/solc", "outputType": "all", "compilerOptions": { "outputSelection": { "*": { "*": [ "evm.bytecode.object", "evm.deployedBytecode.object", "abi", "evm.bytecode.sourceMap", "evm.deployedBytecode.sourceMap", "metadata" ], "": ["ast"] } }, "evmVersion": "istanbul", "optimizer": { "enabled": true, "runs": 999999 } } } ``` Run `yarn test:ovm`, and almost all tests pass! (There's a few small gas-related failures we'll adjust later after we review what just happened). We turned on the optimizer with a setting of 999,999 runs. This shrank our contract size significantly, bringing us back under the 24 kB size limit! It’s important to understand how the Solidity optimizer works, because even though 999,999 runs works here, that may not always be the case. The `runs` parameter [specifies](https://www.reddit.com/r/ethdev/comments/jigz5o/ask_the_solidity_team_anything_1/gahssci?utm_source=share&utm_medium=web2x&context=3) "roughly how often each opcode of the deployed code will be executed across the lifetime of the contract". A value of 1 produces shorter code that is more expensive to execute, while large values produce longer code that is cheaper. So you can see how we got a bit lucky in this case—because a large number of runs produces code that's bigger in size, it's possible with some contracts that using a large number of runs would result in a contract that is still to big deploy. When compiling with the OVM, at a minimum be sure to turn on the optimizer with `runs` set to 1. Even a small value will result in a significant reduction in contract size! *One other note on OVM compilation: The `deployedBytecode` output of solc is currently broken. This will most likely not affect you, as contract deployments deploy the `bytecode`, but it's worth being aware of*. #### Final Gas Tweaks There were two tests that failed because they look for an exact gas usage. Because the OVM is not identical to the EVM, gas usage differs a bit. From the failure messages we can see what the expected gas usage should be for the OVM, so let's fix this quickly. First we need to be able to identify which mode we're using in the tests, so let's export that in `config.ts`: ```diff - export { provider, chainId } + export { provider, chainId, isOVM } ``` In `UniswapV2Factory.spec.ts`, let's update the failing test: ```diff - import { provider } from './shared/config' + import { provider, isOVM } from './shared/config' - expect(receipt.gasUsed).to.eq(2512920) + expect(receipt.gasUsed).to.eq(isOVM ? 4121083 : 2512920) ``` And similarly in `UniswapV2Pair.spec.ts`: ```diff - import { provider } from './shared/config' + import { provider, isOVM } from './shared/config' - expect(receipt.gasUsed).to.eq(73462) + expect(receipt.gasUsed).to.eq(isOVM ? 657108 : 73462) ``` Finally, run `yarn test:evm` and `yarn test:ovm`, and everything should pass! ## Going Forward If you made it this far, congrats— you're done! You've successfully converted the core Uniswap V2 repository to be compatible with the OVM! 🎉 A diff of the full set of required changes made be found [here](https://github.com/ScopeLift/uniswap-v2-core/compare/master...ScopeLift:optimism-integration). Hopefully, you've gotten a pretty good sense of what it takes to upgrade your own projects to run on the OVM as well. To review, most projects will require three kinds of changes, which we walked through above: 1. Tooling updates 2. Test suite updates 3. Contract and compiler modifications Before you start converting other contracts for the OVM, there are a few other important distinctions worth mentioning. Let's wrap up by reviewing these. ### OVM vs. EVM: Contract Wallets Right now, when you run `yarn test:ovm`, all your tests will pas as expected. But let's say we wanted to do some debugging, and we only want to run the `createPair:gas` test of `UniswapV2Factory.spec.ts`. No problem, let's just throw a `.only` modifier on that test and re-run our test command: ```diff - it('createPair:gas', async () => { + it.only('createPair:gas', async () => { const tx = await factory.createPair(...TEST_ADDRESSES) const receipt = await tx.wait() expect(receipt.gasUsed).to.eq(isOVM ? 4121083 : 2512920) }) ``` All of a sudden, this test now fails! Like the EVM, gas usage on the OVM is deterministic, so what's going on here? The answer is simple: for any given account, their first ever transaction is a bit more expensive as a [contract wallet](https://github.com/ethereum-optimism/contracts-v2/tree/master/contracts/optimistic-ethereum/OVM/accounts) is deployed for that user. ### OVM vs. EVM: Gas, Part II The gas situation in OVM is actually a bit more complex than described earlier. There are actually three types of gas! However, we can simplify things quite a bit for now so you only need to worry about one type of gas. As mentioned above the transaction gas limit is 9M gas. This was chosen because it's high enough to allow most transaction types, but low enough that it's unlikely the L1 gas limit drops to this value. As a user and a developer, all you currently need to worry about is ensuring that transactions don't use above 9M gas, otherwise they will revert! ### Wrapping Up You're now armed with all the knowledge you need to begin converting your Solidity projects to run on Optimism. As you've seen, the process is mostly about tooling and testing, with only minor modifications required to your smart contract code. We hope you're as excited as we are to start writing secure, scaleable contracts on the OVM. # Archive *This used to be the content of "OVM vs. EVM: Gas, Part II". It's no longer part of the main guide since the OVM gas situation can be complex, and is partially still under development, but I'm leaving it here for reference.* The gas situation in OVM is a bit more complex than described earlier. There are actually 3 types of gas! The first type is the gas used in an L1 fraud proof to ensure the transaction is actually executable on L1. If you noticed that the cost of a swap increased significantly from 73k to 657k, this is the reason. The swap itself isn't ~10x more expensive. That 657k gas cost is the cost of a fraud proof for that transaction. You can think of this as the maximum possible gas cost of this transaction, but it's not the the amount you'd actually be charged on the OVM. This first type of gas does not reflect the cost of the witness data required in the setup phase of a fraud proof. As a result there is a a second type, called "nuisance gas," which provides a maximum gas cost on this. Storage operations and contract creation both have a separate nuisance gas cost, and if too much is spent, a call fails. As a user, you don't need to worry about these first two types of gas. As a developer, you can think of these as analogous to the contract size limit— you must ensure the value here doesn't exceed the L1 block gas limit so that the transaction can actually be executed. The last type of gas is what we discussed [earlier](#OVM-vs.-EVM:-Gas). This is what you'd think of as the "gas cost" on Ethereum, and this is what users will be concerned with. Because there are no blocks, we have no gas limit, but there is a gas rate limit which the OVM tracks in a special storage slot. If gas usage grows too fast during a given epoch, transactions will revert until the end of that epoch.