计算合约地址

本章讲解在 Solidity 中,新部署的智能合约的地址如何计算。

推特@Hita_DAO    DiscordHitaDAO

在以太坊中,合约地址的生成规则是由合约的创建方式、创建者地址,以及随机数等多个因素共同确定的。

不同的合约创建方式,生成合约地址的规则也不相同。

1. 通过外部账户创建合约

使用外部账户EOA 在以太坊上,创建一个智能合约,那么合约地址的生成规则为:

将创建者地址与 nonce 值一起进行 rlp 编码,再进行 keccak256 哈希计算,最后取其后 20 个字节。

这是在以太坊客户端中,使用了 go 语言生成合约地址的代码:

func CreateAddress(b common.Address, nonce uint64) common.Address {
  data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})
  return common.BytesToAddress(Keccak256(data)[12:])
}

我们也可以使用 js 语言,提前计算将要创建的智能合约的地址:

const { ethers } = require('ethers')

const ownerAddress = '0x7Cc9108d881a0141e5eF3326Cc5B5f15621b6998';
const ownerNonce = 464;
const address = ethers.utils.getContractAddress({
  from: ownerAddress,
  nonce: ownerNonce,
});
console.log(address);

其中,creatorAddress 是创建合约的外部账户的地址。

creatorNonce 是创建者的 nonce 值,它表示了创建者发出的交易数量,通常是递增的。

2. 合约内创建 CREATE 方式

在智能合约内,可以使用 CREATE 方式来创建一个合约。它与使用外部账户EOA 创建智能合约的地址生成规则一样。

在 Solidity 中使用 new 创建合约,或者通过内联汇编的 create 指令创建合约。合约地址由EVM 生成并返回。

具体规则与外部账户创建相同,由创建者的地址和 nonce 计算而成。

func CreateAddress(b common.Address, nonce uint64) common.Address {
  data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})
  return common.BytesToAddress(Keccak256(data)[12:])
}

3. 合约内创建 CREATE2 方式

CREATE2CREATE 相比,在创建合约时,为部署者提供了更多的控制权来决定合约的地址,常用于合约工厂、可升级合约等场景中。

使用 CREATE2  创建合约时,计算公式为:

具体规则为:

address = keccak256(0xFF, creatorAddress, salt, initCodeHash)。

生成的地址由4个部分决定:

    • 0xFF
      一个固定常数,避免和 create 创建的合约地址冲突。
    • creatorAddress
      合约创建者地址。
    • salt
      合约创建者给定的数值,在哈希函数充当盐值。
    • initCodeHash
      待部署合约的字节码的哈希值。

下面合约中的 predictCreate2Address 函数用于计算新创建合约的地址:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 被创建合约
contract Callee {
  string value1;
  string value2;

  // 构造函数有两个参数
  constructor(string memory _value1,string memory _value2) {
    value1 = _value1;
    value2 = _value2;
  }
}

// 合约创建者
contract ContractCreator {

  // 计算合约地址
  function predictCreate2Address(string memory value1,string memory value2) external view returns (address) {
    // 生成盐值
    bytes32 salt = keccak256(abi.encodePacked(value1,value2));
    // 将合约字节码和参数 value1, value2 打包编码
    bytes memory bytecode = abi.encodePacked(
        type(Callee).creationCode,
        abi.encode(value1, value2)
    );

    // 计算新合约所有参数的 hash 值
    bytes32 hash = keccak256(abi.encodePacked(
        bytes1(0xff),
        address(this),
        salt,
        keccak256(bytecode)
    ));

    // 截取 hash 值右边的160位,作为新合约地址
    address predictedAddress = address(uint160(uint(hash)));
    return predictedAddress;
  }
}