本文详细介绍了以太坊中智能合约地址的推导方法,包括EOA、CREATE和CREATE2三种方式。文章深入探讨了RLP编码在地址生成中的应用,通过Solidity代码示例展示了如何预测合约地址,并解释了nonce在EOA和合约账户中的作用。此外,还介绍了使用CREATE2预计算合约地址的方法,并通过Foundry脚本演示了如何预先计算地址并部署相互依赖的合约。
在以太坊上,智能合约可以通过以下三种方式部署:
to
字段设置为 null
,data
字段包含合约的初始化代码。CREATE
操作码。CREATE2
操作码。在本文中,我们将探讨如何预测在每种情况下将创建的合约的地址。
对于由 EOA 或 CREATE 操作码(而不是 CREATE2)部署的合约,地址是从 RLP 编码 的 sender
地址和 nonce
的 Keccak-256 哈希计算得出的。生成的合约地址是该哈希的最后 20 个字节(160 位)。
address=keccak256(RLP([deployer,nonce]))[:20]
如上式所示,这种地址计算方法仅取决于部署者的地址和他们的 nonce。它不考虑合约的字节码、构造函数参数或任何其他内容。
在较高层面上,RLP 连接正在发送的数据项。除了 [0x00, 0x7f] 范围内的单个字节外,每个项目都以一个或多个字节作为前缀,这些字节指示该项目是字符串还是列表,以及其有效负载的长度。感兴趣的读者可以查阅上面链接的文档。
为了了解 RLP 编码如何在合约地址预测中使用,让我们来看一个实际的例子。
在下面的 RLPDemo
合约中,predictContractAddress
函数实现了与 CREATE 操作码的地址派生相同的逻辑。它通过将 RLP 编码应用于发送者的地址和 nonce 来计算预期的部署地址。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract RLPDemo {
// Function to predict the address of a contract that would be deployed by a given address
// 用于预测将由给定地址部署的合约地址的函数
function predictContractAddress(
address deployer,
uint nonce
) public pure returns (address) {
// This implements the same logic as the CREATE opcode's address derivation
// 这实现了与 CREATE 操作码的地址派生相同的逻辑
// For the CREATE opcode, the address is derived as:
// 对于 CREATE 操作码,地址派生为:
// keccak256(rlp([sender_address, sender_nonce]))
bytes memory rlpEncoded;
// RLP encoding rules:
// RLP 编码规则:
// - For nonce = 0, the RLP encoding is [0x80] (empty byte string)
// - 对于 nonce = 0,RLP 编码为 [0x80](空字节字符串)
// - For nonce = 1 to 127, the RLP encoding is the single byte itself (0x01 to 0x7f)
// - 对于 nonce = 1 到 127,RLP 编码是单个字节本身(0x01 到 0x7f)
// - For nonce = 128 to 255, the RLP encoding is [0x81, nonce]
// - 对于 nonce = 128 到 255,RLP 编码是 [0x81, nonce]
// where 0x81 indicates a single-byte length prefix for the following byte
// 其中 0x81 表示以下字节的单字节长度前缀
// Note: Full RLP spec supports encoding arbitrary-length integers using a dynamic length prefix,
// 注意:完整的 RLP 规范支持使用动态长度前缀对任意长度的整数进行编码,
// but this function only supports nonces up to 255.
// 但此函数仅支持高达 255 的 nonce。
if (nonce == 0) {
// For nonce = 0
// 对于 nonce = 0
rlpEncoded = abi.encodePacked(
bytes1(0xd6), // RLP prefix for a list
// 列表的 RLP 前缀
bytes1(0x94), // RLP prefix for a 20-byte address
// 20 字节地址的 RLP 前缀
deployer, // 20 bytes of the deployer's address
// 部署者地址的 20 字节
bytes1(0x80) // RLP encoding for the nonce 0 is 0x80
// nonce 0 的 RLP 编码为 0x80
);
} else if (nonce < 128) {
// For nonce = 1-127
// 对于 nonce = 1-127
rlpEncoded = abi.encodePacked(
bytes1(0xd6), // RLP prefix for a list
// 列表的 RLP 前缀
bytes1(0x94), // RLP prefix for a 20-byte address
// 20 字节地址的 RLP 前缀
deployer, // 20 bytes of the deployer's address
// 部署者地址的 20 字节
uint8(nonce) // Single byte for nonce
// nonce 的单字节
);
} else if (nonce < 256) {
// For nonce = 128-255
// 对于 nonce = 128-255
rlpEncoded = abi.encodePacked(
bytes1(0xd7), // RLP prefix for a list (one byte longer)
// 列表的 RLP 前缀(长一个字节)
bytes1(0x94), // RLP prefix for a 20-byte address
// 20 字节地址的 RLP 前缀
deployer, // 20 bytes of the deployer's address
// 部署者地址的 20 字节
bytes1(0x81), // RLP prefix for a single byte
// 单个字节的 RLP 前缀
uint8(nonce) // The nonce as a single byte
// 作为单个字节的 nonce
);
} else {
revert("Nonce too large for this demo");
// nonce 对于此演示来说太大了
}
bytes32 hash = keccak256(rlpEncoded);
return address(uint160(uint256(hash)));
}
}
为了验证 predictContractAddress
是否按预期工作,我们使用 EOA 0x17F6AD8Ef982297579C203069C1DbfFE4348c372
部署了 RLPDemo
(与上面的合约相同),最终得到的合约地址为 0xE2DFC07f329041a05f5257f27CE01e4329FC64Ef
。
上面描述的部署结果如下图右侧所示:
如上图左侧所示,我们使用部署者的地址 0x17F6AD8Ef982297579C203069C1DbfFE4348c372
和 nonce 0
调用了 predictContractAddress
,并正确预测了先前部署的合约地址:0xE2DFC07f329041a05f5257f27CE01e4329FC64Ef
。
接下来,让我们研究一下如何为外部账户(EOA)和合约账户解释 nonce 。
首先,让我们了解一下 nonce 在以太坊中是如何定义的。根据 以太坊黄皮书,账户的 nonce 定义为:
nonce:一个标量值,等于从此地址发送的交易数量,或者,对于具有关联代码的账户,等于此账户进行的合约创建数量。对于状态 σ 中地址为 a 的账户,这将正式表示为 σ[a]n。
从这个定义可以看出,nonce 是一个归属于发起交易或部署合约的地址的值。由于 EOA 可以直接发起和签名交易,因此 nonce 计数可以反映 ETH/代币转账、合约调用和合约部署等交易。重要的是,即使交易回滚,nonce 也会递增。回滚的交易仍然包含在区块中,这会计入 nonce 的递增。
相比之下,智能合约无法自行发起交易;它们仅在由 EOA 或另一个合约调用时执行。因此,合约账户的 nonce 仅反映由合约发起的合约创建。
注意:在交易中发生的内部调用、消息调用、事件和其他操作永远不会用于递增账户的 nonce 计数。
现在,让我们看看如何初始化 nonce 并使用它来预测 EOA 和合约帐户。
新 EOA 的 nonce 值从 0
开始,并且该值在每次交易时递增 1
。如果新的 EOA 部署合约,则 0
将用作 nonce 来预测地址。但是,如果该账户已经发送了交易,例如以太币转移或先前的部署,则 nonce 将大于 0
。
对于合约帐户,如EIP-161 中指定的那样,nonce 在创建时初始化为 1
。当合约使用 CREATE
或 CREATE2
创建其他合约时,合约的 nonce 会递增 1
。
例如,假设合约 A 刚刚部署。
1
。如果合约 A 继续创建另一个合约,例如,合约 B, 此创建将使用 nonce = 1 来计算合约 B 的地址。2
。nonce = 2
进行此部署。在合约 C 创建完成后,合约 A 的 nonce 变为 3
,依此类推。nonce = 1
开头。没有 EVM 操作码可以获取帐户的 nonce。但是,eth_getTransactionCount
RPC 方法返回给定帐户的帐户 nonce,如上所述。
此方法返回从指定地址发送的交易数量,该数量对应于帐户的 nonce。对于 EOA,这包括 ETH/代币转账、合约调用和合约部署。对于智能合约,eth_getTransactionCount
反映了合约地址创建的合约数量。
下图显示了只有合约部署才会增加合约地址的 eth_getTransactionCount
nonce。
以下是如何在 JavaScript 中使用 eth_getTransactionCount
方法获取 nonce 的示例。
// 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)
// 获取交易计数 (nonce)
const transactionCount = await publicClient.getTransactionCount({
address: '0xYourContractAddress'
});
console.log(transactionCount);
对于测试,我们可以使用 Foundry 中的 vm.getNonce
作弊码。
getNonce
方法在 Foundry 中,vm.getNonce
作弊码 允许我们检索 EVM 上给定帐户或钱包的当前 nonce。
以下是 Foundry 环境中可用的 getNonce
方法:
// Returns the nonce of a given account.
// 返回给定帐户的 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 {
// Could have logic here if needed
// 如果需要,可以在此处添加逻辑
}
contract CreateAddrTest is Test {
address userEOA = address(0xA11CEB0B);
SomeContract public newContract;
function setUp() public {
// Fund the EOA with 10 ether
// 向 EOA 提供 10 个以太币
vm.deal(userEOA, 10 ether);
// Deploy SomeContract which will deploy Dummy in its constructor
// 部署 SomeContract,它将在其构造函数中部署 Dummy
newContract = new SomeContract();
}
function test_eoaAndContractNonces() public view{
// 1. EOA nonce should be 0 initially
// 1. EOA nonce 最初应为 0
uint256 eoaNonce = vm.getNonce(userEOA);
console.log("EOA nonce:", eoaNonce);
assertEq(eoaNonce, 0);
// 2. Contract nonce should be 1
// 2. 合约 nonce 应为 1
uint256 contractNonce = vm.getNonce(address(newContract));
console.log("SomeContract contract nonce:", contractNonce);
assertEq(contractNonce, 1);
}
}
终端结果:
Solady 提供了一个名为 LibRLP 的实用程序,其中包括一个 computeAddress
函数,该函数使用其内部 RLP 编码实现来计算地址。此助手抽象了编码细节并直接返回将由 EOA 或 CREATE
部署生成的合约地址。
function computeAddress(address deployer, uint256 nonce)
internal
pure
returns (address deployed)
{
/// @solidity memory-safe-assembly
assembly {
for {} 1 {} {
// The integer zero is treated as an empty byte string,
// 整数零被视为空字节字符串,
// and as a result it only has a length prefix, 0x80,
// 因此它只有一个长度前缀 0x80,
// computed via `0x80 + 0`.
// 通过“0x80 + 0”计算。
// A one-byte integer in the [0x00, 0x7f] range uses its
// [0x00, 0x7f] 范围内的单字节整数使用其
// own value as a length prefix,
// 自身值作为长度前缀,
// there is no additional `0x80 + length` prefix that precedes it.
// 没有额外的“0x80 + length”前缀位于其前面。
if iszero(gt(nonce, 0x7f)) {
mstore(0x00, deployer)
// Using `mstore8` instead of `or` naturally cleans
// 使用“mstore8”而不是“or”自然会清除
// any dirty upper bits of `deployer`.
// “deployer”的任何脏高位。
mstore8(0x0b, 0x94)
mstore8(0x0a, 0xd6)
// `shl` 7 is equivalent to multiplying by 0x80.
// “shl”7 相当于乘以 0x80。
mstore8(0x20, or(shl(7, iszero(nonce)), nonce))
deployed := keccak256(0x0a, 0x17)
break
}
let i := 8
// Just use a loop to generalize all the way with minimal bytecode size.
// 只需使用一个循环来概括所有内容,并使用最小的字节码大小。
for {} shr(i, nonce) { i := add(i, 8) } {}
// `shr` 3 is equivalent to dividing by 8.
// “shr”3 相当于除以 8。
i := shr(3, i)
// Store in descending slot sequence to overlap the values correctly.
// 以递减的槽顺序存储,以正确地重叠值。
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;
// Importing LibRLP, which contains the computeAddress function shown above.
// 导入 LibRLP,其中包含上面显示的 computeAddress 函数。
import {LibRLP} from "contracts/LibRLP.sol";
contract CreateAddressPredictor {
// contract embeds Solady’s address computation logic and exposes it through addrWithLibRLP.
// 该合约嵌入了 Solady 的地址计算逻辑,并通过 addrWithLibRLP 公开它。
function addrWithLibRLP(
address _deployer,
uint256 _nonce
) public pure returns (address deployed) {
return LibRLP.computeAddress(_deployer, _nonce);
}
}
使用 EOA 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
,我们在 Remix 上以地址 0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95
部署了 CreateAddressPredictor
。
这是终端结果。
当我们调用 addrWithLibRLP
时,传入用于部署 CreateAddressPredictor
的相同 EOA 和 nonce 0
,返回的地址与预期的实际部署地址匹配。
如下图所示,实际部署的合约地址与此预测地址匹配。
注意:如果在此示例中将 nonce 设置为任何非零值,则解码的输出将返回不正确的地址,因为我们正在使用新的 EOA 帐户进行测试。
如前所述,无论部署者是 EOA 还是合约,已部署合约的地址派生都是相同的。我们只需要正确设置部署者地址和 nonce。
在下面的测试中,Deployer
合约演示了从 computeAddress
方法返回的地址如何对应于由另一个合约部署的合约。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "contracts/LibRLP.sol";
contract C {}
contract Deployer {
// Note: nonce is not stored on-chain — this is just for tracking purposes
// 注意: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
操作码)。
在上面的示例中,为了方便起见,我们使用 contractNonce
来存储部署的数量,因为从智能合约内部进行 RPC 调用将需要一个 oracle。由于 contractNonce
初始化为 1 并在每次部署后更新,因此预测的地址将始终与实际部署的地址匹配。因此,deploy()
调用不会回滚。
我们的示例使用 nonce
存储部署的数量以方便起见,因为从智能合约进行 RPC 调用将需要一个 oracle。
require(c == predicted, "Address mismatch");
// If this condition is not met, deploy() call will revert
// 如果不满足此条件,deploy() 调用将回滚
假设我们想在从部署者的合约成功首次部署后部署第二个合约。到那时,在发生第二次部署之前,contractNonce
将递增到 2。
这是第二次部署后 deploy()
调用的结果。
下图显示了上面部署的地址与从 predictAddress
调用(从 LibRLP 调用 computeAddress
)返回的地址匹配。
Create2 在 EIP-1014 中引入。
使用 CREATE2
操作码部署合约时,其地址取决于三个组成部分:部署合约的地址、用户提供的 salt 以及合约的创建(初始化)字节码的哈希。
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
是一个常数,用于区分 CREATE2
和 CREATE
。salt
是一个用户定义的值(32 字节),以确保唯一性。keccak256(createCode)
是合约初始化代码的哈希。CREATE2
预先添加 1 个 0xff
字节keccak256
输入中的 0xff
是区分字节,可确保 CREATE
和 CREATE2
操作码生成的地址之间没有冲突。
回想一下,CREATE
使用 RLP 对地址计算的两个元素的列表( [deployer, nonce]
) 进行编码。对于上下文,部署者的地址始终为 20 个字节,而 nonce 的字节长度可能会因其值而异(实际上为 0-8 个字节,但在理论上是不受限制的)。
由于 RLP 列表前缀由有效负载的总长度决定,因此增加 nonce 值可以增加有效负载长度,这反过来会影响前缀。例如,如果有效负载长度 ≤ 55 字节,则前缀将在以下范围内:0xc0 + payload_length
。
如果 nonce 足够大,以至于其 RLP 编码的表示形式超过 34 字节,则会推动整个 [deployer, nonce]
有效负载超过 55 字节的阈值。因此,RLP 列表前缀将以范围 [0xf8, 0xff]
中的字节开头。也就是说,这种情况在现实中不会发生,因为 34 字节的 nonce 意味着超过 170 亿笔交易 - 这个数字远远超出了任何可能的用法。
此外,EIP-2681 将 nonce 的硬上限定义为 8 字节(64 位),这意味着任何 nonce ≥ 2^64-1
的交易都无效。因此,rlp.encode([deployer, nonce])
的列表前缀将始终落在范围 [0xc0, 0xf7]
内。
//Here, 0xd6 indicates an RLP list of length 22 bytes.
// 在这里,0xd6 表示长度为 22 字节的 RLP 列表。
rlp.encode([deployer, nonce]) = 0xd6 94 <20-byte deployer> <nonce>
因此,如果 CREATE2
不使用 0xff
前缀进行预先添加,而只是对原始串联进行哈希处理,如 deployer ++ salt ++ keccak256(init_code)
,那么在理论上(尽管非常罕见)存在一些选择的值可能会产生一个字节字符串,该字符串的开头与 RLP 编码的 [deployer, nonce]
具有相同的前缀的风险。虽然实际上是不可能的,但这些领域将不会被可证明地不相交。
通过预先添加单个 0xff
字节,CREATE2
确保哈希输入始终以一个值 (0xff
) 开头,该值永远不会出现在具有实际 nonce 的帐户的有效 RLP 编码的开头。这可以在计算哈希之前实现完全的域分离。
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 和创建(初始化)字节码的哈希。
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);
}
}
注意:只有在最终执行部署的合约是 DeployNewAddr
本身时,getAddress()
才会返回正确的 CREATE2
地址。如果另一个合约使用相同的字节码和 salt 执行部署,则生成的地址将不同,因为计算中的部署者地址 ( address(this)
) 将不匹配。在实际部署上下文之外使用 getAddress()
时,请确保计算中使用的部署者地址与将执行部署的地址匹配。
现在,让我们考虑合约 A 具有带参数的构造函数的情况。
getContractABytecode
方法中处理带有构造函数参数的合约当合约(由 EOA 或合约)部署时,EVM 执行合约的创建代码,该代码由与 ABI 编码的构造函数参数连接的 creationCode
(编译的初始化字节码)组成。因此,此行为并非 CREATE2
所特有。
回想一下,在上一节中,我们选择使用一个名为 getContractABytecode
的辅助函数来获取 getAddress
的 createCode
参数。因此,对于具有构造函数参数的合约 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 具有多个构造函数参数,如下所示,则部署字节码必须包括以正确顺序编码的所有参数,如下所示: abi.encodePacked(bytecode, abi.encode(arg1, arg2, ...))
。
contract A {
address public owner;
address public artMaster;
constructor(address _owner, address _artMaster) payable {
owner = _owner;
artMaster = _artMaster;
}
///*************other logic*************///
///*************其他逻辑*************///
}
对于具有构造函数参数 _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 的本机合约实例化语法进行隐式字节码部署。
CREATE2
) 合约地址而无需手动传递创建字节码这种 CREATE2
方法使用 Solidity 的内置 new
关键字以及 salt 参数来部署合约并返回其地址。编译器会自动处理创建字节码的生成和构造函数参数的编码,因此无需手动传递或构造它们。
此方法在下面的 DeployNewAddr1
中显示。
contract DeployNewAddr1 {
// Returns the address of the newly deployed contract
// 返回新部署的合约的地址
//DeployNewAddr1, shows a basic deployment with no constructor arguments (A()).
// DeployNewAddr1,展示了没有构造函数参数 (A()) 的基本部署。
function deploy(uint _salt) external returns (address x) {
A Create2NewAddr = new A{salt: bytes32(_salt)};
return address(Create2NewAddr);
}
}
上面的代码中的 deploy
函数显示了此方法的变体,当合约 A 没有构造函数参数时。
下面的 DeployNewAddr2
和 DeployNewAddr3
分别显示了在部署合约 A
时如何处理构造函数参数,该合约分别具有一个和两个构造函数参数。
contract DeployNewAddr2 {
// DeployNewAddr2 includes a single constructor argument _owner,
// DeployNewAddr2 包括一个构造函数参数 _owner,
// passed to the constructor of contract A.
// 传递给合约 A 的构造函数。
// Solidity automatically encodes constructor arguments and appends them to the creation bytecode.
// Solidity 会自动编码构造函数参数并将它们附加到创建字节码中。
function deploy(uint _salt, address _owner) external returns (address x) {
A Create2NewAddr = new A{salt: bytes32(_salt)}(_owner);
return address(Create2NewAddr);
}
}
contract DeployNewAddr3 {
// In DeployNewAddr3, two constructor arguments (msg.sender and _artMaster) are passed to contract A.
// 在 DeployNewAddr3 中,两个构造函数参数(msg.sender 和 _artMaster)被传递给合约 A。
// As in DeployNewAddr2, these arguments are encoded and included in the creation bytecode.
// 与 DeployNewAddr2 类似,这些让我们通过展示地址预测如何降低合约部署成本的例子来结束本教程。
如果我们想部署两个智能合约(`A` 和 `B`),并且每个合约都需要引用对方的地址。此外,它们的地址永远不应该改变(即,它们必须是**不可变的**)。
这种设置引入了几个必须解决的挑战:
- 首先部署 `A` 会阻止它引用 `B`,因为 `B` 尚未存在。
- 首先部署 `B` 会产生相反的问题——`B` 无法在部署之前引用 `A`。
- 部署后,地址必须是不可变的;不应允许设置器函数或外部更新。
解决这个问题的一种方法是使用工厂合约地址预先计算 `A` 和 `B` 的地址。然后,部署 `A`,并将 `B` 的预计算地址作为构造函数参数——以及 `B`,并将 `A` 的地址作为构造函数参数。
即使这种方法在技术上是正确的,但它也有一些权衡。工厂合约将被部署并存储在链上,这增加了总体的 **bytecode** 足迹。此外,这种方法会产生额外的 gas 成本——既有部署工厂本身产生的,也有执行其逻辑来部署目标合约产生的。
为了避免这种开销,我们可以使用正常的合约部署,并使用我们在本文中讨论的技术来预测地址。
#### **通过 Foundry 脚本使用 RLP 方法预计算合约地址**
在下面的步骤中,我们记录了我们的工厂账户(EOA)的地址,检索其当前的 nonce,预计算合约地址(基于 EOA 的 nonce),并在引用彼此的同时部署它们,使用 [foundry script](https://book.getfoundry.sh/guides/scripting-with-solidity)。
**步骤 1:编写一个脚本,记录工厂(部署者)地址,如下所示。**
```solidity hljs
// 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: With vm.envUint, the private key is loaded in cleartext into memory
//警告:使用 vm.envUint,私钥以明文形式加载到内存中
//NEVER use this pattern in production or with private keys managing real funds.
//永远不要在生产环境中使用这种模式,或者使用私钥来管理真实的资金。
//Assume any key kept in .env will eventually be stolen
//假设保存在 .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
//这是部署者的地址:
步骤 2:计算合约 A 和 B 的地址。
现在我们有了来自私钥的部署者地址,我们可以使用命令 cast compute-address <address> --nonce <value>
确定性地生成地址。
请参阅下面部署者地址 0x8768C6FB71815b2e8Ab6dD31b67a926781aC8f1A
的 nonce 0 和 nonce 1 的结果:
castcompute−address0x8768C6FB71815b2e8Ab6dD31b67a926781aC8f1A−−nonce0ComputedAddress:0x9b4393C60f2408de53F04d93aD178ffBAF25b202user@DESKTOP−QOJ9UFFMINGW64 /Desktop/testFile(master) cast compute-address 0x8768C6FB71815b2e8Ab6dD31b67a926781aC8f1A --nonce 1
Computed Address: 0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D
//计算地址:
注意: 使用错误的 nonce 将导致错误的地址。例如,在上面的脚本中,一旦 new A(address(0))
被部署(使用 EOA),部署者的 nonce 从 0
递增到 1
。
在那次部署之后,使用 nonce 0
计算地址将导致合约地址不匹配。
或者,我们可以使用 vm.getNonce
cheatcode 和 computeAddress
来确定地址 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));
//Deploys a new instance of contract A, passing in address(0) as a constructor argument.
//部署合约 A 的新实例,传入 address(0) 作为构造函数参数。
// after this, nonce = 1.
//此后,nonce = 1。
// compute the current nonce for the address
//计算地址的当前 nonce
uint256 currentNonce = vm.getNonce(dep);
console.log("This is the current nonce: %s", currentNonce);
//这是当前的 nonce:%s
address predicted_a = LibRLP.computeAddress(dep, currentNonce);
address predicted_b = LibRLP.computeAddress(dep, currentNonce + 1);
console.log("predicted_a: %s", predicted_a);
//predicted_a:%s
console.log("predicted_b: %s", predicted_b);
//predicted_b:%s
vm.stopBroadcast();
}
}
这是运行脚本后的终端结果:
Script ran successfully.
//脚本运行成功。
== Logs ==
//日志
This is the deployer's address: 0x8768C6FB71815b2e8Ab6dD31b67a926781aC8f1A
//这是部署者的地址:
This is the current nonce: 1
//这是当前的 nonce:1
predicted_a: 0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D
//predicted_a:0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D
predicted_b: 0xca3fF2a864026daC337312142Aa71D57c7D8Dde3
//predicted_b:0xca3fF2a864026daC337312142Aa71D57c7D8Dde3
步骤 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);
// compute the current nonce for the address
//计算地址的当前 nonce
uint256 currentNonce = vm.getNonce(dep);
console.log("This is the current nonce: %s", currentNonce);
//这是当前的 nonce:%s
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));
//address(a):%s
console.log("predicted_a: %s", predicted_a);
//predicted_a:%s
console.log("address(b): %s", address(b));
//address(b):%s
console.log("predicted_b: %s", predicted_b);
//predicted_b:%s
vm.stopBroadcast();
}
}
这是终端结果:
Script ran successfully.
//脚本运行成功。
== Logs ==
//日志
This is the deployer's address: 0x8768C6FB71815b2e8Ab6dD31b67a926781aC8f1A
//这是部署者的地址:
This is the current nonce: 1
//这是当前的 nonce:1
address(a): 0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D
//address(a):0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D
predicted_a: 0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D
//predicted_a:0x20cf99233e5B16Fba6B0E7bA70768d6EDe75789D
address(b): 0xca3fF2a864026daC337312142Aa71D57c7D8Dde3
//address(b):0xca3fF2a864026daC337312142Aa71D57c7D8Dde3
predicted_b: 0xca3fF2a864026daC337312142Aa71D57c7D8Dde3
//predicted_b:0xca3fF2a864026daC337312142Aa71D57c7D8Dde3
我们可以在终端结果中看到,已部署的地址 a 和 b 分别与预测的地址 predicted_a 和 predicted_b 相对应。
在本文中,我们探讨了以太坊合约地址如何在不同的部署方法中进行预测。对于使用 CREATE
操作码部署的合约,我们表明结果地址仅取决于部署者的地址和 nonce——构造函数参数——bytecode 不起作用。对于 CREATE2
,我们解释了地址预测如何结合 salt 和完整创建 bytecode 的 keccak256 hash,包括构造函数参数。最后,我们描述了如何使用 Foundry 脚本和 computeAddress
有效地预先计算和部署链下两个相互依赖的合约。
EIP-161:定义了账户创建交易,引入了“空账户”的概念,nonce 处理以及清理规则。
EIP-1014:引入了 CREATE2
操作码。
EIP-2681:将帐户 nonce 的限制定义为介于 0
和 2^64-1
之间。
reCAPTCHA
Recaptcha requires verification. //Recaptcha 需要验证。
protected by reCAPTCHA
- 原文链接: rareskills.io/post/ether...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!