以太坊智能合约地址派生方式: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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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进…