之前的文章 Solidity 智能合約 - 工廠模式 (Factory Pattern) 說明了生成智能合約的方法,這篇文章進一步說明智能合約地址生成的規則,包含了 createcreate2 的差異。

目前產生合約主要有兩種規則:

  1. create
  2. create2

create

我們可以在智能合約中使用這個指令來建立合約,而上一篇文章提到的 new 寫法其實也是使用 create 指令。另外,EOA 錢包部署合約的時候也適用這個規則。這個規則中,影響地址主要是下面兩個參數:

  1. 發送者 (sender)
  2. nonce

生成的公式如下:

1
keccak256(rlp([sender, nonce]))[12:]

後面的 [12:] 表示生成的 hash 取最後 20 bytes。如果是 EOA 錢包部署,sender 為錢包地址,nonce 為錢包發送交易時的 nonce。如果是智能合約中建立其他合約,sender 則為執行建立合約行為的主合約地址,nonce 則為此合約的第幾次執行生成合約,從 1 開始。

例如我在 hardhat 進行測試的時候,預設錢包是:
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

每次跑測試,部署使用前一篇文章中的 Factory 合約的時候,固定都是產生這個合約地址:
0x5FbDB2315678afecb367f032d93F642f64180aa3

我們用 JavaScript 程式驗算一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const rlp = require('rlp');
const { keccak256 } = require('js-sha3');

const sender = 'f39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
const nonce = 0;
const bytes = rlp.encode([Buffer.from(sender, 'hex'), nonce]);

// d694f39fd6e51aad88f6f4ce6ab8827279cfffb9226680
console.log(Buffer.from(bytes).toString('hex'))

const hash = keccak256(bytes);

// 14a8157e61feefbde33dcc305fbdb2315678afecb367f032d93f642f64180aa3
console.log(hash);

// 5fbdb2315678afecb367f032d93f642f64180aa3
console.log(hash.slice(-40));

可以看到結果符合規則。同樣的,使用 Factory 生成合約的時候,固定都會生成這個合約地址:
0xa16E02E87b7454126E5E10d957A927A7F5B5d2be

我們驗算一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const rlp = require('rlp');
const { keccak256 } = require('js-sha3');

const sender = '5FbDB2315678afecb367f032d93F642f64180aa3';
const nonce = 1;
const bytes = rlp.encode([Buffer.from(sender, 'hex'), nonce]);

// d6945fbdb2315678afecb367f032d93f642f64180aa301
console.log(Buffer.from(bytes).toString('hex'))

const hash = keccak256(bytes);

// e467a5b633a39bbdb7a33f03a16e02e87b7454126e5e10d957a927a7f5b5d2be
console.log(hash);

// a16e02e87b7454126e5e10d957a927a7f5b5d2be
console.log(hash.slice(-40));

結果正確。

create2

這個指令在 EIP-1014 後加入,new 建構子加上 salt 的時候使用的是 create2,參考前一篇文章。影響地址主要是下面三個參數:

  1. 發送者 (sender)
  2. salt
  3. init_code

生成的公式如下:

1
keccak256(0xff + sender + salt + keccak256(init_code))[12:]

移除了 nonce 的影響,改為自訂的 salt 亂數,以及加上了 init_code。init_code 是部署合約時的 bytecode。這次改成使用 Clones.cloneDeterministic 的方法,同樣第一個部署的 Factory 合約地址為:
0x5FbDB2315678afecb367f032d93F642f64180aa3

隨意使用一個 salt:
0xe467a5b633a39bbdb7a33f03a16e02e87b7454126e5e10d957a927a7f5b5d2be

implementation 的地址:
0xa16E02E87b7454126E5E10d957A927A7F5B5d2be

從之前的文章可以組合出 bytecode 如下:
0x3d602d80600a3d3981f3363d3d373d3d3d363d73a16e02e87b7454126e5e10d957a927a7f5b5d2be5af43d82803e903d91602b57fd5bf3

這次使用 Factory 生成合約的時候,生成出這個合約地址:
0xE0C69D3b6CB5Ac8770b65DfAc6174a2ACBD46D0C

同樣來驗算看看

1
2
3
4
5
6
7
8
9
10
11
12
const { keccak256 } = require('js-sha3');

const sender = '5FbDB2315678afecb367f032d93F642f64180aa3';
const salt = 'e467a5b633a39bbdb7a33f03a16e02e87b7454126e5e10d957a927a7f5b5d2be';
const initCode = '3d602d80600a3d3981f3363d3d373d3d3d363d73a16e02e87b7454126e5e10d957a927a7f5b5d2be5af43d82803e903d91602b57fd5bf3';

const initCodeHash = keccak256(Buffer.from(initCode, 'hex'));
const bytes = Buffer.from('ff' + sender + salt + initCodeHash, 'hex');
const hash = keccak256(bytes);

// e0c69d3b6cb5ac8770b65dfac6174a2acbd46d0c
console.log(hash.slice(-40));

結果正確。

結論

create 使用 nonce 來做為參數,但透過 Facotry 的方式在鏈上生成合約的時候,因為交易順序影響生成結果,在有其他人同時發送交易的情況下,不能預測和確保自己生成的合約地址。create2 在同樣情況下的 sender 和 init_code 是固定的,所以能夠使用 salt 來預測和產生固定的合約地址。

延伸閱讀

Solidity 智能合約 - 工廠模式 (Factory Pattern)
Solidity 智能合約 - 工廠模式的重組問題 (Reorg)
Solidity 智能合約 - 工廠模式的搶先交易問題 (Front Running)