以太坊智能合約地址派生方式:EOA、CREATE 和 CREATE2

1. 引言

在以太坊上,智能合約可以通過以下三種方式之一進行部署:

  • 1)由外部賬戶(Externally Owned Account, EOA)發起交易,其中 to 字段設為 null,而 data 字段包含合約的初始化代碼。
  • 2)智能合約調用 CREATE 操作碼。
  • 3)智能合約調用 CREATE2 操作碼。

本文將探討如何預測在這三種情況下即將創建的合約地址。

2. 預測由 EOA 或 CREATE 部署的智能合約地址

對于由 EOA 或 CREATE 操作碼(非 CREATE2)部署的合約,其地址是通過對 RLP 編碼 的 發送者地址和 nonce 進行 Keccak-256 哈希計算得到的。合約地址取該哈希的最后 20 字節(160 位)。

address = keccak256(RLP([deployer, nonce]))[:20]

如上式所示,這種地址計算方式只依賴于 部署者的地址其 nonce。它不會考慮合約的字節碼、構造函數參數或任何其他因素。

2.1 遞歸長度前綴編碼(Recursive Length Prefix, RLP)

在高層次上,RLP 會將要發送的數據項拼接起來。
除去范圍 [0x00, 0x7f] 內的單字節,每個數據項都會有一個或多個前綴字節,指示該項是字符串還是列表,以及其負載的長度。詳情可參看RLP文檔 。

為了理解 RLP 編碼在合約地址預測中的應用,來看一個實際示例。

2.2 RLPDemo 示例

在下面的 RLPDemo 合約中,predictContractAddress 函數實現了與 CREATE 操作碼相同的地址派生邏輯:

  • 通過對 發送者地址和 nonce 進行 RLP 編碼,計算出預期的部署地址。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;contract RLPDemo {  // 預測給定地址會部署的合約地址function predictContractAddress(  address deployer,  uint nonce  ) public pure returns (address) {  // 實現與 CREATE 操作碼相同的地址推導邏輯// CREATE 地址推導規則:// keccak256(rlp([sender_address, sender_nonce]))bytes memory rlpEncoded;// RLP 編碼規則:// - nonce = 0 時,RLP 編碼為 [0x80](空字節串)// - nonce = 1 到 127 時,RLP 編碼就是該字節本身 (0x01 到 0x7f)// - nonce = 128 到 255 時,RLP 編碼為 [0x81, nonce]//   其中 0x81 表示后續字節是單字節長度前綴// 注意:完整的 RLP 規范支持任意長度整數的編碼,// 但此函數只支持 nonce ≤ 255。if (nonce == 0) {  // nonce = 0rlpEncoded = abi.encodePacked(  bytes1(0xd6), // 某list的RLP前綴bytes1(0x94), // 某20 字節地址的RLP前綴deployer,     // 部署者地址(20 字節)bytes1(0x80)  // nonce = 0 的 RLP 編碼——即為0x80);  } else if (nonce < 128) {  // nonce = 1–127rlpEncoded = abi.encodePacked(  bytes1(0xd6), // 某list的RLP前綴bytes1(0x94), // 某20 字節地址的RLP前綴deployer,     // 部署者地址(20 字節)uint8(nonce)  // nonce 單字節);  } else if (nonce < 256) {  // nonce = 128–255rlpEncoded = abi.encodePacked(  bytes1(0xd7), // 某list的RLP前綴(更長一字節)bytes1(0x94), // 某20 字節地址的RLP前綴deployer,     // 部署者地址(20 字節)bytes1(0x81), // 某單字節的RLP前綴uint8(nonce)  // nonce 單字節);  } else {  revert("Nonce too large for this demo");  }bytes32 hash = keccak256(rlpEncoded);  return address(uint160(uint256(hash)));  }  
}

為了驗證 predictContractAddress 是否能正確運行,在此使用了 EOA 地址 0x17F6AD8Ef982297579C203069C1DbfFE4348c372 部署了 RLPDemo(即上面的合約)。
結果得到的合約地址是:

0xE2DFC07f329041a05f5257f27CE01e4329FC64Ef

上面描述的部署結果展示在下圖右側:
在這里插入圖片描述

如圖左側所示,調用 predictContractAddress,輸入部署者地址 0x17F6AD8Ef982297579C203069C1DbfFE4348c372nonce = 0,成功預測了此前部署的合約地址:

0xE2DFC07f329041a05f5257f27CE01e4329FC64Ef

接下來,將探討 nonce 在外部賬戶(EOA)與合約賬戶 中的不同解釋方式。

2.3 賬戶部署過程中的 Nonce 序列

先來理解一下以太坊中 nonce 的定義。根據 以太坊黃皮書,賬戶的 nonce 定義為:

nonce: 一個標量值,等于從該地址發送的交易數量,或者在與代碼相關聯的賬戶情況下,該賬戶創建的合約數量。對于狀態中的地址 aaa 的賬戶,可以形式化表示為 σ[a]n\sigma[a]_nσ[a]n?

從這個定義可以看出,nonce 是賦予發起交易或部署合約的地址的一個計數值。由于 EOA(Externally Owned Account,外部賬戶)可以直接發起和簽名交易,nonce 計數可以反映 ETH/代幣轉賬、合約調用以及合約部署等操作。需要注意的是,即使交易回滾(revert),nonce 仍然會遞增。因為被回滾的交易仍然被打包進區塊中,這依舊計入 nonce 的增加。

相比之下,智能合約不能自主發起交易;它們只能在被 EOA 或其他合約調用時執行。因此,合約賬戶的 nonce 僅反映該合約發起的合約創建操作

注意:內部調用(internal call)、消息調用(message call)、事件以及交易內部發生的其他操作都不會增加賬戶的 nonce。

接下來看看 nonce 如何初始化,并如何用于預測 EOA 和合約賬戶的地址

  • 新建的 EOA 的 nonce 初始值為 0,每次交易遞增 1。
    如果新建的 EOA 用來部署合約,0 將作為 nonce 來預測該合約的地址。
    但如果該賬戶已經進行過轉賬或部署操作,那么其 nonce 將大于 0。
  • 合約賬戶 的 nonce 在創建時初始化為 1,這是 EIP-161 的規定。
    當合約通過 CREATECREATE2 創建其他合約時,其 nonce 會遞增 1。

如,假設剛剛部署了合約 A。

  • 部署完成后,合約 A 的 nonce 被設置為 1。如果合約 A 隨后創建另一個合約(如合約 B),那么該創建操作將使用 nonce = 1 來計算合約 B 的地址。
    • 一旦合約 B 創建完成,合約 A 的 nonce 遞增為 2
    • 如果合約 A 想再創建另一個合約(如合約 C),它將使用 nonce = 2。合約 C 創建完成后,合約 A 的 nonce 變為 3,以此類推。
    • 合約 B 和合約 C 作為新合約,它們的初始 nonce 都是 1

2.4 如何獲取賬戶的 Nonce

EVM 沒有直接獲取賬戶 nonce 的操作碼。但可以使用 eth_getTransactionCount RPC 方法來獲取指定賬戶的 nonce。

這個方法返回指定地址的 交易數量,對應賬戶的 nonce。

  • 對于 EOA,這包括 ETH/代幣轉賬、合約調用和合約部署。
  • 對于 智能合約eth_getTransactionCount 只反映該合約地址創建的合約數量。

下面的圖片展示了:只有合約部署才會增加合約地址的 eth_getTransactionCount nonce
在這里插入圖片描述

以下是一個使用 eth_getTransactionCount 方法獲取 nonce 的 JavaScript 示例:

// NECESSARY IMPORTS
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';// CREATE A PUBLIC CLIENT
const publicClient = createPublicClient({chain: mainnet,transport: http()
});// GET TRANSACTION COUNT (NONCE)
const transactionCount = await publicClient.getTransactionCount({address: '0xYourContractAddress'
});
console.log(transactionCount);

在測試中,可以使用 Foundry 的 vm.getNonce cheatcode。

2.4.1 Foundry 的 getNonce 方法

在 Foundry 中,vm.getNonce cheatcode 允許獲取給定賬戶或錢包在 EVM 上的當前 nonce。

在 Foundry 環境中可用的 getNonce 方法如下:

// 返回指定賬戶的 nonce
function getNonce(address account) external returns (uint64);

在下面的 test_eoaAndContractNonces() 示例中,驗證了:

  • EOA(userEOA)的 nonce 從 0 開始;
  • 新部署的合約(SomeContract)的 nonce 從 1 開始,符合預期。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;import "forge-std/Test.sol";contract SomeContract {// 如果需要,可以在這里編寫邏輯
}contract CreateAddrTest is Test {address userEOA = address(0xA11CEB0B);SomeContract public newContract;function setUp() public {// 給 EOA 賬戶分配 10 ethervm.deal(userEOA, 10 ether);// 部署 SomeContract(它會在構造函數中部署 Dummy 合約)newContract = new SomeContract();}function test_eoaAndContractNonces() public view {// 1. EOA 的 nonce 初始應為 0uint256 eoaNonce = vm.getNonce(userEOA);console.log("EOA nonce:", eoaNonce);assertEq(eoaNonce, 0);// 2. 新部署合約的 nonce 初始應為 1uint256 contractNonce = vm.getNonce(address(newContract));console.log("SomeContract contract nonce:", contractNonce);assertEq(contractNonce, 1);}
}

終端輸出結果:
在這里插入圖片描述

2.5 使用 EOA 部署合約時預測合約地址(基于 LibRLP)

https://github.com/Vectorized/solady 提供了一個實用工具 LibRLP,其中包含一個 computeAddress 函數,該函數使用其內部的 RLP 編碼實現來計算地址。這個輔助函數抽象掉了編碼的細節,直接返回由 EOA 或 CREATE 部署生成的合約地址。

function computeAddress(address deployer, uint256 nonce)  internal  pure  returns (address deployed)  {  /// @solidity memory-safe-assembly  assembly {  for {} 1 {} {  // 整數 0 會被視為一個空的字節字符串,// 因此它只有一個長度前綴 0x80,// 計算方式為 `0x80 + 0`。// 在 [0x00, 0x7f] 范圍內的一字節整數// 其自身值即作為長度前綴,// 并不會再有額外的 `0x80 + length` 前綴。if iszero(gt(nonce, 0x7f)) {  mstore(0x00, deployer)  // 使用 `mstore8` 而不是 `or`,可以自然清除// `deployer` 的高位臟比特。  mstore8(0x0b, 0x94)  mstore8(0x0a, 0xd6)  // `shl 7` 等價于乘以 0x80。  mstore8(0x20, or(shl(7, iszero(nonce)), nonce))  deployed := keccak256(0x0a, 0x17)  break  }  let i := 8  // 使用循環泛化所有情況,保持字節碼盡量小。  for {} shr(i, nonce) { i := add(i, 8) } {}  // `shr 3` 等價于除以 8。  i := shr(3, i)  // 逆序存儲到 slot,以正確覆蓋值。  mstore(i, nonce)  mstore(0x00, shl(8, deployer))  mstore8(0x1f, add(0x80, i))  mstore8(0x0a, 0x94)  mstore8(0x09, add(0xd6, i))  deployed := keccak256(0x09, add(0x17, i))  break  }  }  }

為了理解其實際效果,將部署下面的 CreateAddressPredictor 合約。然后調用 addrWithLibRLP 來測試計算結果是否與實際部署的 CreateAddressPredictor 地址一致。

// SPDX-License-Identifier: MIT  
pragma solidity ^0.8.4;// 導入 LibRLP,其中包含上面展示的 computeAddress 函數。
import {LibRLP} from "contracts/LibRLP.sol";contract CreateAddressPredictor {  // 合約內部調用 Solady 的地址計算邏輯,// 并通過 addrWithLibRLP 暴露出來。  function addrWithLibRLP(  address _deployer,  uint256 _nonce  ) public pure returns (address deployed) {  return LibRLP.computeAddress(_deployer, _nonce);  }  
}

使用 EOA 地址 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,在 Remix 上部署了 CreateAddressPredictor,部署地址為:

0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95

這是終端的結果:
在這里插入圖片描述

當調用 addrWithLibRLP,傳入與部署 CreateAddressPredictor 相同的 EOA 和 nonce = 0 時,返回的地址與實際部署地址一致,符合預期。

如下圖所示,實際部署的合約地址與預測出的地址完全一致:
在這里插入圖片描述

注意:

  • 在這個例子中,如果 nonce 設置為非零值,解碼輸出將返回錯誤的地址,因為以上是用一個新建的 EOA 賬戶做的測試。

2.6 預測由合約部署的合約地址

如前所述,無論部署者是 EOA 還是合約,合約地址的推導方式都是相同的。只需要正確設置部署者地址和 nonce。

在下面的測試中,Deployer 合約演示了如何通過 computeAddress 方法計算出的地址與合約實際部署的地址相對應。

// SPDX-License-Identifier: MIT  
pragma solidity ^0.8.0;  
import "contracts/LibRLP.sol";contract C {}contract Deployer {  // 注意:nonce 并不會存儲在鏈上 —— 這里只是用來做記錄  uint256 public contractNonce = 1;function deploy() public returns (address c) {  address predicted = predictAddress(address(this), contractNonce);  c = address(new C());  require(c == predicted, "Address mismatch");  contractNonce += 1;return c;  }function predictAddress(  address _deployer,  uint256 _nonce  ) public pure returns (address deployed) {  return LibRLP.computeAddress(_deployer, _nonce);  }  
}

C 合約由 Deployer 合約使用 new 關鍵字部署(底層調用的是 CREATE opcode)。

在上述例子中,使用 contractNonce 來存儲部署次數,方便追蹤。因為如果要從智能合約中直接發起 RPC 調用則需要預言機支持。
由于 contractNonce 初始化為 1,并且在每次部署后遞增,所以預測的地址總是與實際部署地址一致。因此 deploy() 調用不會回滾。

換句話說,用 nonce 來追蹤部署次數,只是為了方便而已。

require(c == predicted, "Address mismatch");  
// 如果條件不滿足,deploy() 調用會回滾

假設在第一次部署成功后,再次部署第二個合約。在此之前,contractNonce 將被遞增為 2,然后才會發生第二次部署。

這是第二次部署時 deploy() 調用的結果:
在這里插入圖片描述

下面的圖片顯示,實際部署的合約地址與 predictAddress 調用返回的地址(內部調用了 LibRLP.computeAddress)完全一致。
在這里插入圖片描述

3. 如何使用 CREATE2 預測合約地址

Create2 在 EIP-1014 中被引入。

當使用 CREATE2 指令部署合約時,其地址取決于三個組成部分:

  • 1)部署合約的地址
  • 2)用戶提供的 salt
  • 3)以及 合約 創建(init)字節碼 的哈希值。

公式如下:

Create_contract_address = keccak256(0xff ++ deployer ++ salt ++ keccak256(init_code))

利用這個關系,可以在部署前預計算合約的地址,如下方 getAddress 函數所示:

function getAddress(  bytes memory createCode,  uint _salt  
) public view returns (address) {  bytes32 hash = keccak256(  abi.encodePacked(  bytes1(0xff),  address(this),  _salt,  keccak256(createCode)  )  );  return address(uint160(uint(hash)));  
}

其中:

  • 0xff 是一個常量,用來區分 CREATE2CREATE
  • salt 是用戶定義的值(32 字節),用來確保唯一性。
  • keccak256(createCode) 是合約初始化代碼的哈希值。

3.1 為什么 CREATE2 要在前面加上 1 個 0xff 字節

keccak256 輸入中,0xff 是區分字節,用于確保 CREATECREATE2 生成的地址不會發生沖突。

回憶一下,CREATE 使用 RLP 對兩個元素([deployer, nonce])進行編碼來計算地址。背景信息:

  • 部署者的地址始終是 20 字節;
  • nonce 的字節長度取決于其值(實際應用中范圍是 0–8 字節,理論上不受限)。

由于 RLP 列表前綴取決于 payload 的總長度,當 nonce 變大時,payload 的長度可能增加,從而改變前綴。

如,如果 payload 長度 ≤ 55 字節,前綴范圍是:

0xc0 + payload_length

如果 nonce 大到其 RLP 編碼超過 34 字節,那么整個 [deployer, nonce] payload 就會超過 55 字節閾值,此時列表前綴會落在范圍 [0xf8, 0xff]。不過這種情況在現實中不會發生,因為 34 字節的 nonce 意味著超過 170 億筆交易 —— 遠超出實際可能。

此外,EIP-2681 規定 nonce 的硬性上限為 8 字節(64 位),即任何 nonce ≥ 2^64-1 的交易無效。因此,rlp.encode([deployer, nonce]) 的前綴始終落在 [0xc0, 0xf7] 范圍內。

如:

// 在此,0xd6 表示一個長度為 22 字節的 RLP 列表
rlp.encode([deployer, nonce]) = 0xd6 94 <20-byte deployer> <nonce>

因此,如果 CREATE2 不在前面加上 0xff,而是簡單地將 deployer ++ salt ++ keccak256(init_code) 拼接后哈希,那么理論上(雖然幾乎不可能),可能會存在某些取值,使得結果字節串的前綴與某個 [deployer, nonce] 的 RLP 編碼一致。這樣的話,兩個域就無法做到嚴格區分。

通過在最前面加上一個 0xff 字節,CREATE2 可以確保哈希輸入始終以 0xff 開頭,而這個值在真實賬戶的 RLP 編碼中永遠不會出現。這樣就能在計算哈希前實現 完全的域隔離

3.2 CREATE2 預計算示例

接下來看一個例子:使用 getAddress 方法從合約 A 計算新地址。注意,這個合約沒有構造函數。

contract A {  address public owner;function getBalance() public view returns (uint256) {  return address(this).balance;  }  
}

在下面的 DeployNewAddr 合約中,getAddress 函數接收合約 A 的創建字節碼和一個 salt,并計算出 CREATE2 部署合約時的地址。在這里,計算過程中使用的是 DeployNewAddr 的地址(即 address(this))。因此,最終的合約地址取決于:

  • DeployNewAddr 的地址,
  • 提供的 salt
  • 以及創建(init)字節碼 的哈希。
contract DeployNewAddr {  function getAddress(  bytes memory createCode,  uint _salt  ) public view returns (address) {  bytes32 hash = keccak256(  abi.encodePacked(  bytes1(0xff),  address(this),  _salt,  keccak256(createCode)  )  );  return address(uint160(uint(hash)));  }function getContractABytecode() public pure returns (bytes memory) {  bytes memory bytecode = type(A).creationCode;return abi.encodePacked(bytecode);  }  
}

注意:

  • getAddress() 只有在真正執行部署的合約是 DeployNewAddr 本身時,才會返回正確的 CREATE2 地址。
  • 如果是其它合約用相同的字節碼和 salt 部署,那么結果地址會不同,因為計算中用到的 address(this)(部署者地址)不一致。

因此,當在真實部署環境之外使用 getAddress() 時,必須確保計算中使用的部署者地址與實際執行部署的合約一致。

接下來,考慮 合約 A 有構造函數并帶參數的情況

3.3 在 getContractABytecode 方法中處理帶構造函數參數的合約

當合約被部署時(無論是由 EOA 還是合約部署),EVM 會執行合約的 創建代碼
這部分創建代碼由 creationCode(已編譯的初始化字節碼)ABI 編碼后的構造函數參數 拼接而成。
因此,這種行為并不是 CREATE2 所特有的。

回顧上一節,選擇通過一個輔助函數 getContractABytecode 來獲取 getAddresscreateCode 參數。
因此,對于帶有構造函數參數的合約 A,該輔助函數需要在合約的 創建字節碼后拼接參數,并且要保證參數的編碼格式正確。

下面是一個修改后的合約 A,它有一個構造函數參數:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;contract A {address public owner;constructor(address _owner) payable {owner = _owner;}function getBalance() public view returns (uint256) {return address(this).balance;}
}

如果合約 A 包含如上所示的構造函數參數,那么 getContractABytecode 函數會將 _owner 參數的 ABI 編碼追加到字節碼中,即:

abi.encodePacked(bytecode, abi.encode(_owner))

如果合約 A 有多個構造函數參數,如下所示:

contract A {address public owner;address public artMaster;constructor(address _owner, address _artMaster) payable {owner = _owner;artMaster = _artMaster;}/***********other logic***********/
}

在這種情況下,部署字節碼必須包含所有參數,并且按正確順序編碼:

abi.encodePacked(bytecode, abi.encode(arg1, arg2, ...))

因此,對于有 _owner_artMaster 兩個構造函數參數的合約 A,getContractABytecode 函數如下所示:

function getContractAInitByteCode(address _owner,address _artMaster
) public pure returns (bytes memory) {bytes memory bytecode = type(A).creationCode;return abi.encodePacked(bytecode, abi.encode(_owner, _artMaster));
}

在測試上面 DeployNewAddr 合約中的 getAddress 方法之前,先來看另一種 CREATE2 的部署方式。

  • 這種方式 不需要手動傳遞創建字節碼,而是通過 Solidity 的 原生合約實例化語法 隱式生成。

3.4 不手動傳遞創建字節碼的 CREATE2 合約部署

這種 CREATE2 方法使用 Solidity 內置的 new 關鍵字,結合 salt 參數來部署合約并返回其地址。
在這種方式下,編譯器會自動處理創建字節碼的生成和構造函數參數的編碼,因此無需手動傳遞或拼接。

下面的 DeployNewAddr1 演示了這種方式:

contract DeployNewAddr1 {// 返回新部署合約的地址// DeployNewAddr1 展示了一個沒有構造函數參數的簡單部署 (A())。function deploy(uint _salt) external returns (address x) {A Create2NewAddr = new A{salt: bytes32(_salt)};return address(Create2NewAddr);}
}

上面代碼中的 deploy 函數演示了 當合約 A 沒有構造函數參數時的部署方法。

下面的 DeployNewAddr2DeployNewAddr3 展示了 當合約 A 有一個或兩個構造函數參數時的部署方式:

contract DeployNewAddr2 {// 包含一個構造函數參數 _owner// 該參數會傳入合約 A 的構造函數// Solidity 會自動編碼構造函數參數并將其拼接到創建字節碼function deploy(uint _salt, address _owner) external returns (address x) {A Create2NewAddr = new A{salt: bytes32(_salt)}(_owner);return address(Create2NewAddr);}
}
contract DeployNewAddr3 {// 包含兩個構造函數參數(msg.sender 和 _artMaster)// 與 DeployNewAddr2 一樣,這些參數會被編碼并包含在創建字節碼中function deploy(uint _salt,address _owner,address _artMaster) external returns (address x) {A Create2NewAddr = new A{salt: bytes32(_salt)}(_owner, _artMaster);return address(Create2NewAddr);}
}

接下來,運行帶有一個構造函數參數的代碼,看看 DeployNewAddr2 中的 deploy 是否會返回與 DeployNewAddr 合約中 getAddress 相同的預測地址。
這兩個方法都使用 salt = 29,其中 getAddress 方法的 bytecode 來自 getContractABytecode

調用兩個方法得到的結果如下所示:
在這里插入圖片描述
從上圖可以看到,deploy 函數返回的地址與 getAddress 函數返回的地址 完全一致。

4. 如何部署兩個相互引用地址(A 和 B)的智能合約,并且地址不可變

接下來將舉例展示地址預測如何減少合約部署的成本。

假設想要部署兩個智能合約(A 和 B),并且每個合約都需要引用對方的地址。此外,它們的地址必須永不改變(不可變)

這種設置引入了幾個需要解決的挑戰:

  • 先部署 A 時,A 無法引用尚未存在的 B
  • 先部署 B 時,會出現相反的問題 —— B 無法引用未部署的 A
  • 部署完成后,地址必須是不可變的;不能使用 setter 函數或外部更新

一種解決辦法是:

  • 通過工廠合約的地址預計算 A 和 B 的地址。
  • 然后用預計算得到的 B 的地址作為構造函數參數部署 A,
  • 同樣用 A 的預計算地址來部署 B。

盡管這種方法在技術上是正確的,但它存在一些權衡:

  • 工廠合約本身需要部署并存儲在鏈上,從而增加整體字節碼占用
  • 部署工廠合約以及調用其邏輯來部署目標合約,都會消耗額外的 gas

為了避免這些開銷,可以使用普通的合約部署方式,并利用本文討論的技術來預測地址

4.1 使用 Foundry 腳本(RLP 方法)預計算合約地址

以下步驟展示了如何使用 foundry 腳本 來完成:

  • 會打印工廠賬戶(一個 EOA)的地址,獲取它的當前 nonce,預計算合約地址(基于 EOA 的 nonce),并在部署時讓它們相互引用。

具體分為3步:

  • 1)步驟 1:編寫腳本打印工廠(部署者)地址
  • 2)步驟 2:計算合約 A 和 B 的地址
  • 3)步驟 3: 使用對應的構造函數參數(即預計算的地址)部署合約

4.1.1 步驟 1:編寫腳本打印工廠(部署者)地址

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;import {Script, console} from "forge-std/Script.sol";
import {A, B} from "../src/DeployAddr.sol";contract DeployAddrScript is Script {A public a;function run() public {uint256 pk = vm.envUint("PRIV_KEY");address dep = vm.addr(pk);// ?? WARNING: 使用 vm.envUint 時,私鑰會以明文加載到內存// ?? 切勿在生產環境或管理真實資金的私鑰上使用此模式// ?? 可以假設存放在 .env 文件中的任何密鑰最終都會泄露console.log("This is the deployer's address:", dep);vm.startBroadcast(pk);new A(address(0));vm.stopBroadcast();}
}

終端輸出結果:

$ forge script script/DeployAddr.s.sol --rpc-url http://localhost:8545
[?] Compiling...
No files changed, compilation skipped
Script ran successfully.== Logs ==This is the deployer's address: 0x8768C6FB71815b2e8Ab6dD31b67a926781aC8f1A

4.1.2 步驟 2:計算合約 A 和 B 的地址

現在已經拿到部署者(由私鑰推導)的地址,可以使用以下命令確定性地生成合約地址:

cast compute-address <address> --nonce <value>

如,對于部署者地址 0x8768C6FB71815b2e8Ab6dD31b67a926781aC8f1A

cast compute-address 0x8768C6FB71815b2e8Ab6dD31b67a926781aC8f1A --nonce 1
Computed Address: 0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D

?? 注意:如果使用了錯誤的 nonce,就會得到錯誤的地址。如,在上面腳本中,一旦 new A(address(0)) 部署完成(通過 EOA),部署者的 nonce 會從 0 遞增到 1。

如果在那之后還用 nonce=0 來計算地址,就會與實際部署的地址不符。

另一種方法是結合 vm.getNonce cheatcodecomputeAddress 來確定 A 和 B 的地址,如下所示:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;import {Script, console} from "forge-std/Script.sol";
import {A, B} from "../src/DeployAddr.sol";
import {LibRLP} from "lib/LibRLP.sol";contract DeployAddrScript is Script {A public a;//B public b;function run() public {uint256 pk = vm.envUint("PRIV_KEY");address dep = vm.addr(pk);console.log("This is the deployer's address:", dep);vm.startBroadcast(pk);// nonce = 0,new A(address(0));// 部署一個新的 A 實例,構造函數參數為 address(0)// 部署后,nonce = 1// 獲取當前 nonceuint256 currentNonce = vm.getNonce(dep);console.log("This is the current nonce: %s", currentNonce);address predicted_a = LibRLP.computeAddress(dep, currentNonce);address predicted_b = LibRLP.computeAddress(dep, currentNonce + 1);console.log("predicted_a: %s", predicted_a);console.log("predicted_b: %s", predicted_b);vm.stopBroadcast();}
}

這是運行腳本后的終端結果:

Script ran successfully.== Logs ==This is the deployer's address: 0x8768C6FB71815b2e8Ab6dD31b67a926781aC8f1A  This is the current nonce: 1predicted_a: 0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D  predicted_b: 0xca3fF2a864026daC337312142Aa71D57c7D8Dde3

4.1.3 步驟 3: 使用對應的構造函數參數(即預計算的地址)部署合約

現在,部署合約 A 和 B,并將結果與 predicted_a 和 predicted_b 進行比較。

// SPDX-License-Identifier: UNLICENSED  
pragma solidity ^0.8.13;import {Script, console} from "forge-std/Script.sol";  
import {A, B} from "../src/DeployAddr.sol";  
import {LibRLP} from "lib/LibRLP.sol";contract DeployAddrScript is Script {  A public a;  B public b;function run() public {  uint256 pk = vm.envUint("PRIV_KEY");  address dep = vm.addr(pk);console.log("This is the deployer's address:", dep);vm.startBroadcast(pk);// 計算該地址的當前 nonceuint256 currentNonce = vm.getNonce(dep);console.log("This is the current nonce: %s", currentNonce);address predicted_a = LibRLP.computeAddress(dep, currentNonce);  address predicted_b = LibRLP.computeAddress(dep, currentNonce + 1);A a = new A(predicted_b);  B b = new B(predicted_a);console.log("address(a): %s", address(a));  console.log("predicted_a: %s", predicted_a);  console.log("address(b): %s", address(b));  console.log("predicted_b: %s", predicted_b);vm.stopBroadcast();  }  
}

運行該腳本的終端結果如下:

Script ran successfully.== Logs ==This is the deployer's address: 0x8768C6FB71815b2e8Ab6dD31b67a926781aC8f1A  This is the current nonce: 1address(a): 0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D  predicted_a: 0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D  address(b): 0xca3fF2a864026daC337312142Aa71D57c7D8Dde3  predicted_b: 0xca3fF2a864026daC337312142Aa71D57c7D8Dde3

可以看到,在終端結果中,已部署的地址 ab 分別與預計算的地址 predicted_apredicted_b 完全一致。

5. 總結

本文探索了以太坊合約地址在不同部署方式下的預測方法。

  • 對于使用 CREATE 指令部署的合約,展示了結果地址只依賴于 部署者的地址和 nonce ——構造函數參數和字節碼本身并不起作用。
  • 對于 CREATE2,解釋了地址預測如何結合 salt完整創建字節碼(包含構造函數參數)的 keccak256 哈希值
  • 最后,描述了如何使用 Foundry 腳本和 computeAddress 在鏈下高效預計算并部署兩個相互依賴的合約。

本文提到的 EIP 有:

  • EIP-161: 定義了賬戶創建交易,引入了“空賬戶”的概念、nonce 處理規則及其清理方式。
  • EIP-1014: 引入了 CREATE2 指令。
  • EIP-2681: 定義了賬戶 nonce 的范圍為 0 到 2^64 - 1

參考資料

[1] RareSkills團隊2025年8月博客 How Ethereum address are derived (EOAs, CREATE, and CREATE2)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/94398.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/94398.shtml
英文地址,請注明出處:http://en.pswp.cn/web/94398.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基于RISC-V架構的國產MCU在eVTOL領域的應用研究與挑戰分析

摘要電動垂直起降飛行器&#xff08;eVTOL&#xff09;作為未來城市空中交通的重要組成部分&#xff0c;對嵌入式控制系統的性能、可靠性和安全性提出了極高的要求。RISC-V作為一種新興的開源指令集架構&#xff0c;為國產微控制器&#xff08;MCU&#xff09;的研發和應用帶來…

深度學習中的“集體智慧”:Dropout技術詳解——不僅是防止過擬合,更是模型集成的革命

引言&#xff1a;從“過擬合”的噩夢說起 在訓練深度學習模型時&#xff0c;我們最常遇到也最頭疼的問題就是過擬合&#xff08;Overfitting&#xff09;。 想象一下&#xff0c;你是一位正在備考的學生&#xff1a; 欠擬合&#xff1a;你根本沒學進去&#xff0c;所有題都做錯…

在JavaScript中,比較兩個數組是否有相同元素(交集)的常用方法

方法1&#xff1a;使用 some() includes()&#xff08;適合小數組&#xff09;function haveCommonElements(arr1, arr2) {return arr1.some(item > arr2.includes(item)); }// 使用示例 const arrA [1, 2, 3]; const arrB [3, 4, 5]; console.log(haveCommonElements(ar…

心路歷程-Linux的系統破解詳細解說

CentOS7系統密碼破解 密碼破解是分兩種情況的&#xff1b;一種是在系統的界面內&#xff0c;一種就是不在系統的頁面&#xff1b; 今天我們就來聊聊這個系統破解的話題&#xff1b; 1.為什么需要破解密碼&#xff1f;–>那當然是忘記了密碼&#xff1b;需從新設置密碼 2.但是…

IDE和AHCI硬盤模式有什么區別

IDE&#xff08;Integrated Drive Electronics&#xff09;和 AHCI&#xff08;Advanced Host Controller Interface&#xff09;是硬盤控制器的工作模式&#xff0c;主要區別在于性能、功能兼容性以及對現代存儲設備的支持程度。以下是詳細對比和分析&#xff1a;一、本質區別…

【密碼學實戰】密碼實現安全測試基礎篇 . KAT(已知答案測試)技術解析與實踐

KAT 測試技術解析 在密碼算法的安全性驗證體系中&#xff0c;Known Answer Test&#xff08;KAT&#xff0c;已知答案測試&#xff09;是一項基礎且關鍵的技術。它通過 “已知輸入 - 預期輸出” 的確定性驗證邏輯&#xff0c;為密碼算法實現的正確性、合規性提供核心保障&…

如何用Redis作為消息隊列

說明&#xff1a;以前背八股文&#xff0c;早就知道 Redis 可以作為消息隊列&#xff0c;本文介紹如何實現用 Redis 作為消息隊列。 介紹 這里直接介紹 yudao 框架中的實現。yudao 是一套現成的開源系統框架&#xff0c;里面集成了許多基礎功能&#xff0c;我們可以在這基礎上…

解決 uniapp 修改index.html文件不生效的問題

業務場景&#xff1a;需要在H5網站設置追蹤用戶行為&#xff08;即埋點&#xff09;的script代碼。 問題&#xff1a;無論如何修改根目錄下的index.html文件都不會生效 問題原因&#xff1a;在 manifest.json 文件中有個【web配置】—>【index.html模版路徑】&#xff0c;…

C語言第十一章內存在數據中的存儲

一.整數在內存中的存儲在計算機內存中&#xff0c;所有的數字都是以二進制來存儲的。整數也不例外&#xff0c;在計算機內存中&#xff0c;整數往往以補碼的形式來存儲數據。這是為什么呢&#xff1f;在早期計算機表示整數時&#xff0c;最高位為符號位。但是0卻有兩種表示形式…

K8s部署dashboard平臺和基本使用

Kubernetes 的默認 Dashboard 主要用于基本的資源查看與管理,如查看 Pod、Service 等資源的狀態,進行簡單的創建、刪除操作 。然而,在企業級復雜場景下,其功能顯得較為局限。 與之相比,開源的 Kubernetes Dashboard 增強版工具 ——Dashboard UI ,為用戶帶來了更強大的功…

JavaEE進階-文件操作與IO流核心指南

文章目錄JavaEE進階文件操作與IO流核心指南前言&#xff1a;為什么需要文件操作&#xff1f;一、java.io.File 類的基本用法1.1 文件路徑1.2 常用方法示例獲取文件信息創建和刪除文件目錄操作文件重命名和移動二、IO流的基本概念2.1 核心困境&#xff1a;字節流 vs. 字符流字節…

動手學深度學習03-線性神經網絡

動手學深度學習pytorch 參考地址&#xff1a;https://zh.d2l.ai/ 文章目錄動手學深度學習pytorch1-第03章-線性神經網絡1. 線性回歸1.1 什么是線性回歸&#xff1f;1.2 如何表示線性回歸的預測公式&#xff1f;2. 損失函數2.1 什么是損失函數&#xff1f;2.2 如何表示整個訓練集…

如何安全解密受限制的PDF文件

當你需要從PDF中復制一段文字用于報告或引用時&#xff0c;如果文件被禁止復制&#xff0c;解密后即可輕松提取內容&#xff0c;避免手動輸入的麻煩。它解壓后雙擊主程序即可運行&#xff0c;無需安裝&#xff0c;即開即用&#xff0c;十分便捷。建議先將界面語言切換為中文&am…

利用DeepSeek輔助編譯c#項目tinyxlsx生成xlsx文件

繼續在尋找比較快的xlsx寫入庫&#xff0c;從https://github.com/TinyXlsx/TinyXlsx/ 看到它的測試結果&#xff0c;比c的openXLSX快幾倍&#xff0c;就想試用一下&#xff0c;仔細一看&#xff0c;它是個c#項目&#xff0c;需要.NET 8.0。 于是上微軟網站下載了.NET 8.0 SDK&a…

構建現代高并發服務器:從內核機制到架構實踐

引言:高并發的挑戰與演進 在當今互聯網時代,高并發處理能力已成為服務器的核心競爭力。傳統的"一個連接一個線程"(Thread-per-Connection)模型由于資源消耗巨大、上下文切換成本高和可擴展性差,早已無法應對數萬甚至百萬級的并發連接需求。現代高并發服務器基于…

1SG10MHN3F74C2LG Intel Stratix 10 系列 FPGA

1SG10MHN3F74C2LG 是 Intel 推出的 Stratix 10 系列 FPGA 家族中的高端型號&#xff0c;它基于 Intel 與 TSMC 合作的 14 納米 FinFET 工藝制造&#xff0c;是面向超高性能計算、數據中心加速、5G 通信基礎設施、以及高端網絡設備的旗艦級可編程邏輯器件。這顆 FPGA 以極高的邏…

IIS訪問報錯:HTTP 錯誤 500.19 - Internal Server Error

無法訪問請求的頁面&#xff0c;因為該頁的相關配置數據無效。 由于權限不足而無法讀取配置文件解決辦法&#xff1a;文件夾添加用戶權限Everyone文件夾->鼠標右鍵->屬性->安全->組或用戶名->編輯->添加->錄入Everyone->檢查名稱->一路點確定

AI對口型唱演:科技賦能,開啟虛擬歌者新篇章

最近在短視頻平臺閑逛&#xff0c;發現不少朋友都在玩“AI對口型唱演”&#xff0c;這類視頻簡直成了新晉流量密碼。從熱門歌曲到經典臺詞&#xff0c;配上夸張的口型和表情&#xff0c;分分鐘就能沖上排行榜前排。不過問題也來了——市面上這么多專用軟件&#xff0c;到底哪家…

爬蟲逆向--Day16Day17--核心逆向案例3(攔截器關鍵字、路徑關鍵字、請求堆棧、連續請求)

一、入口定位入口定位-- 關鍵字搜索-- 方法關鍵字--最簡單&#xff0c;最高效的 排第一-- encrypt 加密-- decrypt 解密-- JSON.stringify 給一個JS對象做Json字符串處理的把一個對象轉換為Json字符串JSON.stringify({a:1,b:"2"}){"a":"1…

RuoYi-Vue3項目中Swagger接口測試404,端口問題解析排查

一 問題概述版本&#xff1a;ruoyi前后端分離版&#xff0c;ruoyi版本3.9.0 前端Vue3 后端Spring Boot 2.5.15 本地測試環境ruoyi界面中系統工具下的系統接口集成了Swagger&#xff0c;當對其頁面上的接口進行請求測試時卻發生了404報錯。具體表現如下圖二 問題排查 1、與Vue2進…