合约部署合约
通过 new
创建合约 / create
使用关键字 new
可以创建一个新合约。待创建合约的完整代码必须事先知道,因此递归的创建依赖是不可能的。create
主要有以下三种表现形式。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract D {
uint x;
function D(uint a) payable {
x = a;
}
}
contract C {
// 1.将作为合约的一部分执行
D d = new D(4);
// 2.方法内创建
function createD1(uint arg) public {
D newD = new D(arg);
}
// 3.方法内创建,并转账
function createD2(uint arg, uint amount) public payable {
//随合约的创建发送 ether
D newD = (new D){value:amount}(arg);
}
}
如示例中所示,通过使用 value
选项创建 D
的实例时可以附带发送 Ether,但是不能限制 gas 的数量。 如果创建失败(可能因为栈溢出,或没有足够的余额或其他问题),会引发异常。
这种方式也被称为 Factory 创建。工厂合约部署,也被称为 create
,批量创建的时候使用,比如批量创建交易池,DeFi 类产品中批量创建借贷池等。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Account {
address public deployer;
address public owner;
constructor(address _owner) payable {
owner = _owner;
deployer = msg.sender;
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
contract AccountFactory {
Account[] public accounts;
function deploy(address _owner) external payable {
Account account = new Account{value: msg.value}(_owner);
accounts.push(account);
}
}
通过 salt
创建合约 / create2
在创建合约时,将根据创建合约的地址和每次创建合约交易时的 nonce
来计算合约的地址。如果你指定了一个可选的 salt
(一个 bytes32 值),那么合约创建将使用另一种机制(create2
)来生成新合约的地址:它将根据给定的 salt
,创建合约的字节码和构造函数参数来计算创建合约的地址。特别注意,这里不再使用 nonce
。
create2 的意义:可以在创建合约时提供更大的灵活性:你可以在创建新合约之前就推导出将要创建的合约地址。 甚至是还可以依赖此地址(即便它还不存在)来创建其他合约。一个主要用例场景是充当链下交互仲裁合约,仅在有争议时才需要创建。
案例演示 1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract D {
uint256 public x;
constructor(uint256 a) {
x = a;
}
}
contract C {
function createDSalted(bytes32 salt, uint256 arg) public {
// 最新的语法
D d = new D{salt: salt}(arg);
// 之前的写法
// 这个复杂的表达式只是告诉我们,如何预先计算地址。
// 这里仅仅用来说明。 实际上,现在仅需要使用 `new D{salt: salt}(arg)` 即可.
address predictedAddress = address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(
abi.encodePacked(type(D).creationCode, arg)
)
)
)
)
)
);
require(address(d) == predictedAddress);
}
}
使用 create2
创建合约还有一些特别之处。 合约销毁后可以在同一地址重新创建。不过,即使创建字节码(creation bytecode)相同(这是要求,因为否则地址会发生变化),该新创建的合约也可能有不同的部署字节码(deployed bytecode)。 这是因为构造函数可以使用两次创建合约之间可能已更改的外部状态,并在存储合约时将其合并到部署字节码中。
小结
这种也被称为操作码部署,create
可以通过加入 salt,来预测即将生成的地址。这种创建就能预测生成地址的方式也被称为 create2
创建。
加 salt ,salt 决定了合约地址,不能重复使用
除非之前 salt 生成的合约被销毁了。
即将部署的合约地址计算
uint160
格式就是地址格式了
下面是两者的简短总结:
普通合约的地址生成方式: 部署者的
地址
+地址 nonce
预测合约地址的方式:
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff), // 固定字符串
address(this), // 当前工厂合约地址,固定写法
_salt, // salt
keccak256(bytecode) //部署合约的 bytecode
)
);
return address(uint160(uint256(hash)));
案例代码 2
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DeployWithCreate2 {
address public deployer;
address public owner;
constructor(address _owner) payable {
owner = _owner;
deployer = msg.sender;
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
contract AccountFactory {
DeployWithCreate2[] public accounts;
function deploy(uint256 _salt) external payable {
DeployWithCreate2 account = new DeployWithCreate2{
salt: bytes32(_salt), // uint256 需要转为 bytes32
value: msg.value
}(msg.sender);
accounts.push(account);
}
// 获取即将部署的地址
function getAddress(bytes memory bytecode, uint256 _salt)
external
view
returns (address)
{
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff), // 固定字符串
address(this), // 当前工厂合约地址
_salt, // salt
keccak256(bytecode) //部署合约的 bytecode
)
);
return address(uint160(uint256(hash)));
}
// 获取合约的 bytecode
function getBytecode(address _owner) external pure returns (bytes memory) {
bytes memory bytecode = type(DeployWithCreate2).creationCode;
// 连接的参数使用 abi.encode
return abi.encodePacked(bytecode, abi.encode(_owner));
}
}
合约测试
address1
部署合约address1:
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
使用
address1
作为参数,获取getBytecode
返回值。调用 getAddress
bytecode 参数是
getBytecode
返回值salt 参数是 1
计算结果是:
0x0022172A008CEdf60B1770dDD987888e5663D1Cc
调用 deploy,salt 参数是 1
调用 accounts[0]
返回的合约地址是
0x0022172A008CEdf60B1770dDD987888e5663D1Cc
,和计算的完全一样。
再次调用 deploy,salt 参数是 1
返回失败
transact to AccountFactory.deploy errored: VM error: revert.
用 assembly 做 create
create 部署
Proxy: 部署合约的方法,和修改 owner
Helper: 生成部署用的 bytecode 和修改 owner 的 data
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test1 {
address public owner = msg.sender;
function setOwner(address _owner) public {
require(msg.sender == owner, "now owner");
owner = _owner;
}
}
contract Test2 {
address public owner = msg.sender;
uint256 public value = msg.value;
uint256 public x;
uint256 public y;
constructor(uint256 _x, uint256 _y) {
x = _x;
y = _y;
}
}
// contract Proxy {
// function depolyTest1() external {
// new Test1();
// }
// function depolyTest2() external payable {
// new Test2(1, 2);
// }
// }
// assembly 部署
contract Proxy {
event Depoly(address);
// fallback() external payable {}
function depoly(bytes memory _code)
external
payable
returns (address adds)
{
assembly {
// create(v,p,n);
// v 是 发送的ETH值
// p 是 内存中机器码开始的位置
// n 是 内存中机器码的大小
// msg.value 不能使用,需要用 callvalue()
adds := create(callvalue(), add(_code, 0x20), mload(_code))
}
require(adds != address(0), "Depoly Failed");
emit Depoly(adds);
}
// 跳用
function execute(address _target, bytes memory _data) external payable {
(bool success, ) = _target.call{value: msg.value}(_data);
require(success, "Failed");
}
}
contract Helper {
// 生成 type(contract).creationCode
function getBytescode1() external pure returns (bytes memory bytecode) {
bytecode = type(Test1).creationCode;
}
// 生成构造函数带有参数的 bytecode,参数连接后面就可以了
function getBytescode2(uint256 _x, uint256 _y)
external
pure
returns (bytes memory)
{
bytes memory bytecode = type(Test2).creationCode;
// abi 全局变量
return abi.encodePacked(bytecode, abi.encode(_x, _y));
}
// 调用合约方法的calldata,使用 abi.encodeWithSignature
function getCalldata(address _owner) external pure returns (bytes memory) {
return abi.encodeWithSignature("setOwner(address)", _owner);
}
}
测试部署
前提条件:部署 Helper 和 Proxy 合约。
通过 getBytescode1 ,获取 Test1 需要的 bytecode
部署 Test1
获取 Test1 合约地址
At Test1 Address
获取 Test1 owner 地址
通过 getCalldata ,获取 Test1 setOwner 需要的 bytecode。参数是想要设置的 Owner 地址。
执行 execute(),参数是 Test1 合约地址 和 getCalldata 返回值。
合约 2
通过 getBytescode2 ,获取 Test2 需要的 bytecode
部署 Test2,需要设置 x, y 的值,可以选择支付 ETH。
获取 Test2 合约地址
At Test2 Address
查看 Test2 的值
用 assembly 做 create2
UniswapV2Factory 的创建 pair 代码如下
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
// single check is sufficient
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
创建合约的扩展
可以通过以太坊交易从外部或从 Solidity 合约内部创建合约。
一些集成开发环境,例如 Remix, 通过使用一些 UI 用户界面使创建合约的过程更加顺畅。 在以太坊上通过编程创建合约最好使用 JavaScript API web3.js。 现在,我们已经有了一个叫做 web3.eth.Contract
的方法能够更容易的创建合约。
创建合约时, 合约的构造函数 (一个用关键字 constructor 声明的函数)会执行一次。 构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。构造函数执行完毕后,合约的最终代码将部署到区块链上。此代码包括所有公共和外部函数以及所有可以通过函数调用访问的函数。 部署的代码没有 包括构造函数代码或构造函数调用的内部函数。
在内部,构造函数参数在合约代码之后通过 ABI 编码 传递,但是如果你使用 web3.js 则不必关心这个问题。
问答题
通过
new
创建合约 /create
的方法1 将作为合约的一部分执行
D d = new D(4);
2 方法内创建
function createD(uint arg) public { D newD = new D(arg); }
2 方法内创建,并转账
function createD2(uint arg, uint amount) public payable { //随合约的创建发送 ether D newD = (new D){value:amount}(arg); }
create / create2 区别
在创建合约时,将根据创建合约的地址和每次创建合约交易时的
nonce
来计算合约的地址。如果你指定了一个可选的salt
(一个 bytes32 值),那么合约创建将使用另一种机制(create2
)来生成新合约的地址:它将根据给定的salt
,创建合约的字节码和构造函数参数来计算创建合约的地址。特别注意,这里不再使用nonce
。
create2 的意义
可以在创建合约时提供更大的灵活性:你可以在创建新合约之前就推导出将要创建的合约地址。甚至是还可以依赖此地址(即便它还不存在)来创建其他合约。一个主要用例场景是充当链下交互仲裁合约,仅在有争议时才需要创建。
create2 有什么需要注意的?
不能重复使用 salt
使用
create2
创建合约还有一些特别之处。 合约销毁后可以在同一地址重新创建。不过,即使创建字节码(creation bytecode)相同(这是要求,因为否则地址会发生变化),该新创建的合约也可能有不同的部署字节码(deployed bytecode)。 这是因为构造函数可以使用两次创建合约之间可能已更改的外部状态,并在存储合约时将其合并到部署字节码中。
如何预测 create2 合约地址
核心如下
bytes32 hash = keccak256( abi.encodePacked( bytes1(0xff), // 固定字符串 address(this), // 当前工厂合约地址,固定写法 _salt, // salt keccak256(bytecode) //部署合约的 bytecode ) ); return address(uint160(uint256(hash)));
精确写法
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract D { uint public x; constructor(uint a) { x = a; } } contract C { function createDSalted(bytes32 salt, uint arg) public { /// 这个复杂的表达式只是告诉我们,如何预先计算地址。 /// 这里仅仅用来说明。 /// 实际上,你仅仅需要 ``new D{salt: salt}(arg)``. address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( bytes1(0xff), // 固定字符串 address(this), // 当前工厂合约地址,固定写法 salt, // salt //部署合约的 bytecode keccak256(abi.encodePacked( type(D).creationCode, arg )) ))))); D d = new D{salt: salt}(arg); require(address(d) == predictedAddress); } }