Solidity: CREATE vs CREATE2

Solidity: CREATE vs CREATE2

The deployed contract address can be calculated beforehand via:

  • keccak256(deployerAddress, nonce) 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

If you're deploying a contract using new YourContract() without salt, then you're using CREATE. Here is an example written in TypeScript and ethers.js:

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}`)

You can specify the deployed address by increasing nonce:

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 following Solidity code demonstrates how to deploy a contract using CREATE2 which is introduced in EIP-1014 to provide more flexible address generation:

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

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

address computedAddress = address(
    uint256(
        keccak256(
            abi.encodePacked(
                bytes1(0xff),
                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);

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

Solidity: calldata, memory, and storage

Solidity: calldata, memory, and storage

Variables

There are three types of variables in Solidity:

  • Global variables
    • Provide information about the blockchain
    • For example, block.number, block.timestamp, or msg.sender
  • State variables
    • Declared outside a function
    • Stored on the blockchain
    • Also called "storage"
  • Local variables
    • Declared inside a function
    • Not stored on the blockchain, stored in memory instead
    • Erased between function calls

References:
https://docs.soliditylang.org/en/v0.7.6/units-and-global-variables.html
https://solidity-by-example.org/variables

Data Types

There are two types of data:

  • Value types: YourContract, address, bool, uint256, int256, enum, and bytes32 (fixed-size byte arrays)
  • Reference types: array, mapping, and struct

It's worth noting that bytes and string are dynamically-sized byte arrays and are considered reference types. However, byte (bytes1), bytes2, ..., bytes32 are value types since they're fixed-size byte arrays.

References:
https://docs.soliditylang.org/en/v0.7.6/types.html#value-types
https://docs.soliditylang.org/en/v0.7.6/types.html#reference-types

Data Location

When using a reference type, you must explicitly provide the data location where the type is stored. There are three data locations:

  • storage is where the state variables are stored, and its lifetime is the lifetime of the contract
  • memory means variables are temporary and erased between external function calls
  • calldata behaves mostly like memory, but is immutable

For reference types in function arguments, you must declare them as memory or calldata.

If possible, use calldata as the data location because it avoids copying, reduces gas usage, and ensures that the data cannot be modified. Arrays and structs with calldata data location can also be returned from functions, but it is not possible to allocate such types.

References:
https://docs.soliditylang.org/en/v0.7.6/types.html#data-location
https://medium.com/coinmonks/solidity-storage-vs-memory-vs-calldata-8c7e8c38bce
https://gist.github.com/hrkrshnn/ee8fabd532058307229d65dcd5836ddc

It is also a best practice to use external if you expect that the function will only ever be called externally, and use public if you need to call the function internally. The difference between both is that public function arguments are copied to memory, while in external functions, arguments are read directly from calldata, which is cheaper than memory allocation. external functions are sometimes more efficient when they receive large arrays.

References:
https://medium.com/newcryptoblock/best-practices-in-solidity-b324b65d33b1

Assignment Behaviour

  • Assignments between storage and memory (or from calldata) always create an independent copy.
  • Assignments from memory to memory only create references.
  • Assignments from storage to a local storage variable also only assign a reference.
  • All other assignments to storage always copy.

References:
https://docs.soliditylang.org/en/v0.7.6/types.html#data-location-and-assignment-behaviour

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