Solidity: CREATE vs CREATE2

Solidity: CREATE vs CREATE2

In Solidity, there are two opcodes for creating contracts: CREATE and CREATE2. Also, the deployed contract address can be precomputed via:

  • keccak256(deployerAddress, deployerNonce) if you're using CREATE opcode
  • keccak256(0xFF, deployerAddress, salt, bytecode) if you're using CREATE2 opcode

ref:
https://ethereum.stackexchange.com/questions/101336/what-is-the-benefit-of-using-create2-to-create-a-smart-contract

CREATE

Default opcode used when deploying smart contracts. If you're deploying a contract using new YourContract() without salt, then you're using CREATE.

The following code written in TypeScript and ethers.js, shows how to deploy a contract using CREATE under the hood:

import { ethers } from 'ethers';

const deployer = await ethers.getNamedSigner("deployer")
const nonce = await deployer.getTransactionCount()
const computedAddress = ethers.utils.getContractAddress({
    from: deployer.address,
    nonce: nonce,
})
console.log(`computed address: ${computedAddress}`)

const ktbArbitrageurFactory = await ethers.getContractFactory("KtbArbitrageur", deployer)
const ktbArbitrageur = await ktbArbitrageurFactory.deploy(oinchAggregationRouterV5)
console.log(`deployed address: ${ktbArbitrageur.address}`)

Though it's pretty inefficient, but you can specify the deployed address (to some extend) by keeping increasing nonce until it meets some conditions you set:

async increaseNonceToDeployUpgradeable(condition: string, targetAddr: string) {
    const { ethers } = this._hre
    const deployer = await ethers.getNamedSigner("deployer")

    // We use deployer's address and nonce to compute a contract's address which deployed with that nonce,
    // to find the nonce that matches the condition
    let nonce = await deployer.getTransactionCount()
    console.log(`Next nonce: ${nonce}`)

    let computedAddress = "0x0"
    let count = 0
    while (
        count < 2 ||
        (condition == "GREATER_THAN"
            ? computedAddress.toLowerCase() <= targetAddr.toLowerCase()
            : computedAddress.toLowerCase() >= targetAddr.toLowerCase())
    ) {
        // Increase the nonce until we find a contract address that matches the condition
        computedAddress = ethers.utils.getContractAddress({
            from: deployer.address,
            nonce: nonce,
        })
        console.log(`Computed address: ${nonce}, ${computedAddress}`)
        nonce += 1
        count += 1
    }

    // When deploying a upgradable contract,
    // it will deploy the implementation contract first, then deploy the proxy
    // so we need to increase the nonce to "the expected nonce - 1"
    let nextNonce = await deployer.getTransactionCount()
    for (let i = 0; i < count - 1 - 1; i++) {
        nextNonce += 1
        console.log(`Increasing nonce to ${nextNonce}`)
        const tx = await deployer.sendTransaction({
            to: deployer.address,
            value: ethers.utils.parseEther("0"),
        })
        await tx.wait()
    }

    console.log(`Finalized nonce`)
}

ref:
https://docs.ethers.org/v5/api/utils/address/#utils-getContractAddress

CREATE2

The Solidity code below demonstrates how to deploy a contract using CREATE2 which is introduced in EIP-1014 to provide more flexible and predictable address generation:

bytes32 salt = bytes32("perp");
address oinchAggregationRouterV5 = 0x1111111254EEB25477B68fb85Ed929f73A960582;

address computedAddress = address(
    uint256(
        keccak256(
            abi.encodePacked(
                bytes1(0xff), // avoid conflict with CREATE
                address(this), // deployer
                salt,
                keccak256(
                    abi.encodePacked(
                        type(KtbArbitrageur).creationCode, // bytecode
                        abi.encode(oinchAggregationRouterV5) // constructor parameter
                    )
                )
            )
        )
    )
);

KtbArbitrageur ktbArbitrageur = new KtbArbitrageur{ salt: salt }(oinchAggregationRouterV5);
console.logAddress(address(ktbArbitrageur));
console.logAddress(computedAddress);

You can change salt to an arbitrary value to produce different contract addresses.

ref:
https://docs.soliditylang.org/en/v0.7.6/control-structures.html#salted-contract-creations-create2

hardhat-deploy: Upgradeable Contracts with Linked Libraries

hardhat-deploy: Upgradeable Contracts with Linked Libraries

Library

Assume ContractA imports LibraryA, when deploying ContractA, LibraryA is embedded into ContractA if LibraryA contains only internal functions.

If LibraryA contains at least one external function, LibraryA must be deployed first, and then linked when deploying ContractA.

ref:
https://solidity-by-example.org/library/
https://docs.soliditylang.org/en/v0.7.6/using-the-compiler.html

Foundry

In Foundry tests, Foundry will automatically deploy libraries if they have external functions, so you don't need to explicitly link them.

hardhat-deploy

Whenever the library is changed, hardhat-deploy will deploy a new implementation and upgrade the proxy:

import { DeployFunction } from "hardhat-deploy/dist/types"
import { QuoteVault } from "../typechain-types"

const func: DeployFunction = async function (hre) {
    const { deployments, ethers } = hre

    // deploy library
    await deployments.deploy("PerpFacade", {
        from: deployerAddress,
        contract: "contracts/lib/PerpFacade.sol:PerpFacade",
    })
    const perpFacadeDeployment = await deployments.get("PerpFacade")

    // deploy upgradeable contract
    await deployments.deploy("QuoteVault", {
        from: deployerAddress,
        contract: "contracts/QuoteVault.sol:QuoteVault",
        proxy: {
            owner: multisigOwnerAddress, // ProxyAdmin.owner
            proxyContract: "OpenZeppelinTransparentProxy",
            viaAdminContract: "DefaultProxyAdmin",
            execute: {
                init: {
                    methodName: "initialize",
                    args: [
                        "Kantaban USDC-ETH QuoteVault",
                        "kUSDC-ETH",
                        usdcDecimals,
                        USDC,
                        WETH,
                    ],
                },
            },
        },
        libraries: {
            PerpFacade: perpFacadeDeployment.address,
        },
    })
    const quoteVaultDeployment = await deployments.get("QuoteVault")

    // must specify library address when instantiating the contract:
    const quoteVaultFactory = await ethers.getContractFactory("contracts/QuoteVault.sol:QuoteVault", {
        libraries: {
            PerpFacade: perpFacadeDeployment.address,
        },
    })
    const quoteVault = quoteVaultFactory.attach(quoteVaultDeployment.address) as unknown as QuoteVault
    console.log(await quoteVault.decimals())
}

export default func

ref:
https://github.com/wighawag/hardhat-deploy#handling-contract-using-libraries

Write next generation JavaScript with Babel 7

Write next generation JavaScript with Babel 7

You write the next generation JavaScript code (ES6 or ES2018!) and using Babel to convert them to ES5. Even more, with the new @babel/preset-env module, it is able to intellectually convert your next generation ECMAScript code to compatible syntax based on browser compatibility statistics. So you don't have to target specific browser versions anymore!

ref:
https://babeljs.io/
https://babeljs.io/docs/en/babel-preset-env

There is a real-world project with proper configurations. The following article is based on this project.
https://github.com/vinta/pangu.js

Babel

$ npm install \
@babel/core \
@babel/cli \
@babel/preset-env \
--save-dev

ref:
https://babeljs.io/setup

// babel.config.js
module.exports = function(api) {
    api.cache(false);
    return {
        presets: [
            "@babel/preset-env"
        ],
        comments: false
    };
};

ref:
https://babeljs.io/docs/en/configuration

It is also recommended to put common commands in the scripts section of the package.json file.

// package.json
{
    ...
    "scripts": {
        "clear:shared": "rm -rf ./dist/shared/",
        "clear:browser": "rm -rf ./dist/browser/",
        "clear:node": "rm -rf ./dist/node/",
        "clear": "npm run clear:shared && npm run clear:browser && npm run clear:node",
        "build:shared": "npm run clear:shared && babel src/shared/ -d dist/shared/",
        "build:browser": "npm run clear:browser && webpack",
        "build:node": "npm run clear:node && babel src/node/ -d dist/node/",
        "build": "npm run build:shared && npm run build:browser && npm run build:node",
    },
    ...
}
$ npm run build:node

ref:
https://babeljs.io/docs/en/babel-cli/

Webpack

$ npm install \
webpack \
webpack-cli \
babel-loader \
terser-webpack-plugin \
--save-dev
// webpack.config.js
var _ = require('underscore');
var fs = require('fs');
var path = require('path');
var TerserPlugin = require('terser-webpack-plugin');
var webpack = require('webpack');

var packageInfo = require('./package.json');

var entryPath = './src/browser/pangu.js';

module.exports = {
  target: 'web',
  // mode: 'development',
  mode: 'production',
  entry: {
    'pangu': entryPath,
    'pangu.min': entryPath
  },
  output: {
    path: path.resolve(__dirname, 'dist/browser/'),
    filename: '[name].js',
    library: 'pangu',
    libraryTarget: 'umd',
    umdNamedDefine: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules|node/,
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            presets: [
              [
                "@babel/preset-env",
                {
                  "modules": "umd"
                }
              ]
            ]
          }
        }
      }
    ]
  },
  devtool: false,
  optimization: {
    minimizer: [
      new TerserPlugin({
        include: /\.min\.js$/
      })
    ],
  },
}

ref:
https://webpack.js.org/configuration/

@babel/preset-env transpiles your files to commonjs by default, which requires the transpiled files to be included by require or import. To make this compatible with your Chrome extension, you need to transpile the files as umd module.

ref:
https://stackoverflow.com/questions/52929562/babel-7-uncaught-referenceerror-after-transpiling-a-module

$ nom run build:browser

Karma

$ npm install \
@babel/register \
karma-babel-preprocessor \
karma-chrome-launcher \
karma-coverage \
karma-mocha \
karma-mocha-reporter \
puppeteer \
chai \
--save-dev
// karma.conf.js
module.exports = function(config) {
  config.set({
    frameworks: [
      'mocha'
    ],
    browsers: [
      'ChromeHeadless'
    ],
    files: [
      'node_modules/chai/chai.js',
      'dist/browser/pangu.js',
      'test/browser/*.js',
    ],
    preprocessors: {
        'dist/browser/pangu.js': ['coverage'],
    },
    reporters: [
      'mocha',
      'coverage'
    ],
    singleRun: true,
    coverageReporter: {
      type: 'lcov',
      subdir: '.'
    },
  });
};

ref:
https://karma-runner.github.io/3.0/config/configuration-file.html

$ nom run test:browser