智能合约:CREATE,CREATE2,CREATE3区别

  • 老道
  • 发布于 14小时前
  • 阅读 26

CREATE,CREATE2,CREATE3区别,里面有一些实验可以参考

CREATE 这里先给出Mytoken和创建Mytoken的工厂合约 (这里是用remix来实验的) // SPDX-License-Identifier: MIT pragma solidity ^0.8.20;

contract MyToken { address public factory; // 工厂合约地址 address public underlyingToken; // 该合约管理的底层代币地址

constructor() payable {
    factory = msg.sender;
}

// 将参数命名为 _token,使用下划线前缀是 Solidity 的通用惯例
function initialize(address _token) external {
    require(msg.sender == factory, "FORBIDDEN: Only factory can initialize"); 
    underlyingToken = _token;
}

} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "./MyToken.sol";

contract TokenFactory { // 通过代币地址查找其对应的合约地址 mapping(address => address) public getContractByToken; // 保存所有部署出来的合约地址列表 address[] public allContracts;

function create(address _token) external returns (address instanceAddr) {
    require(_token != address(0), "INVALID_ADDRESS");
    require(getContractByToken[_token] == address(0), "CONTRACT_EXISTS");

    // 创建新合约
    MyToken instance = new MyToken();

    // 调用初始化
    instance.initialize(_token);

    instanceAddr = address(instance);

    // 更新记录
    allContracts.push(instanceAddr);
    getContractByToken[_token] = instanceAddr;

    return instanceAddr;
}

function getDeployedContract(address _token) external view returns (address) {
    return getContractByToken[_token];
}

// 这里的 _instance 是指 MyToken 的地址
function getFactoryOf(address _instance) external view returns (address) {
    return MyToken(_instance).factory();
}

} 实验开始

  1. factory创建Mytoken合约(先depoly工厂合约:0x86ef4Ad0E7cdaDfF58FAB38291e3e226Ab0e7223)
  2. 选择一个代币地址0x617F2E2fD72FD9D5503197092aC168c91465E7f2(正常来说这里应该是个ERC20代币的地址,不过嘛,在remix人家有现成的,下面就理解成ERC20代币即可。)
  3. 然后调用create函数,参数就是这个地址,这时候就已经创建了一个Mytoken。
  4. 我们现在要找到用factory创建的Mytoken地址,调用getDeployedContract,继续使用前面这个ERC20的代币地址。
    1. 就会得到deploy的这个Mytoken地址0xBa6FE23f8aAd265276233cC93c746981118b783f 其实这个地址在上面那一步就出现了 [图片]
  5. 接下来我们来看这个Mytoken是属于谁创建的,使用Mytoken的地址,调用getFactoryOf
    1. 就会得到是这个factory的地址0x86ef4Ad0E7cdaDfF58FAB38291e3e226Ab0e7223 [图片]
  6. 然后我们使用0xBa6FE23f8aAd265276233cC93c746981118b783f来查看这个Mytoken的合约内容
  7. 在Mytoken合约调用factory函数,检查一下是不是跟创建的工厂的地址是一样的,正常来说是要一样的可以测试用别的ERC20代币地址来initialize这个Mytoken,就会得到下面内容,并且给出了原因。 [图片]
  8. 在终端输入一下代码,可以得到当前工厂合约的nonce是多少。 每创建一个新的Mytoken也就是说每调用一次create,就会+1 await web3.eth.getTransactionCount("factory_CONTRACT_ADDRESS") 而这个nonce跟生成Mytoken的地址是有巨大关系的,所以我们进到下面的evm的源码里面看一下,这是使用CREATE来创建合约的源码 // core/vm/evm.go // Create creates a new contract using code as deployment code. func (evm EVM) Create(caller common.Address, code []byte, gas uint64, value uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller)) return evm.create(caller, code, gas, value, contractAddr, CREATE) } 可以很明显的看到合约的地址跟caller和这个caller的nonce,evm.StateDB.GetNonce(caller)有关系,下面就是create完一个合约后nonce会增加的实现 // core/vm/evm.go // create creates a new contract using code as deployment code. func (evm EVM) create(...){ //... nonce := evm.StateDB.GetNonce(caller) if nonce+1 < nonce { return nil, common.Address{}, gas, ErrNonceUintOverflow } evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator) //... } PS:如果合约创建失败回滚之后这个nonce还是会增加,原因如下: // core/vm/evm.go func (evm EVM) create(...){ //... evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator) //...

    // Create a new account on the state only if the object was not present. // It might be possible the contract code is deployed to a pre-existent // account with non-zero balance. snapshot := evm.StateDB.Snapshot() if !evm.StateDB.Exist(address) { evm.StateDB.CreateAccount(address) } //... ret, err = evm.initNewContract(contract, address) if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) { evm.StateDB.RevertToSnapshot(snapshot) if err != ErrExecutionReverted { contract.UseGas(contract.Gas, evm.Config.Tracer, tracing.GasChangeCallFailedExecution) } } } 因为snapshot储存是在nonce增加之后,然后revert是到snapshot的地方,所以nonce还是增加了 CREATE2

  9. 使用new 先给出工厂合约代码和Mytoken2的代码 // SPDX-License-Identifier: MIT pragma solidity ^0.8.20;

contract MyToken2 { address public factory; address public underlyingToken;

// constructor需要接收参数,才能在 new MyToken2{salt: salt}(_token) 中传
constructor(address _token) payable {
    factory = msg.sender;
    underlyingToken = _token;
}

} // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "./MyToken2.sol";

// 使用create2创建MyToken2 contract TokenFactory2 {

function createMytokenSalted(bytes32 salt, address _token) public returns (address) {
    // bytecodeHash 必须包含构造参数的编码
    bytes32 bytecodeHash = keccak256(
        abi.encodePacked(
            type(MyToken2).creationCode, //这是MyToken2的基础代码
            abi.encode(_token) // constructor参数在这里
        )
    );

    // 预测地址
    address predictedAddress = address(uint160(uint(keccak256(
        abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            bytecodeHash
        )
    ))));

    // 使用CREATE2
    MyToken2 mytoken2 = new MyToken2{salt: salt}(_token);

    require(address(mytoken2) == predictedAddress, "Address mismatch");

    return address(mytoken2);
}

}

  1. MyToken2 mytoken2 = new MyToken2{salt: salt}(_token); 这里使用的是CREATE2来进行创建合约,这个{salt: salt}是用来告诉编译器使用 CREATE2 ,而不是默认的 CREATE 会做下面几件事

    • initcode(包含constructor的参数 _token)
    • 将 initcode 存入内存
    • 将参数压栈
    • 执行 CREATE2的opcode (0xf5) 我们进到evm源码看一下CREATE2在哪 // core/vm/interpreter.go // 这里执行工厂合约的时候会进来的地方 func (evm EVM) Run(...){ //... // Get the operation from the jump table and validate the stack to ensure there are // enough stack items available to perform the operation. op = contract.GetOp(pc) operation := jumpTable[op]
      //... // execute the operation // CREATE2的opcode (0xf5) res, err = operation.execute(&pc, evm, callContext)
      } 接下来进到CREATE2的opcode // core/vm/instructions.go func opCreate2(pc
      uint64, evm EVM, scope ScopeContext) ([]byte, error) { if evm.readOnly { return nil, ErrWriteProtection } // 这里就是将压栈的参数pop出来的地方 var ( endowment = scope.Stack.pop() offset, size = scope.Stack.pop(), scope.Stack.pop() salt = scope.Stack.pop() input = scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) gas = scope.Contract.Gas ) // ...

    // 调用 evm.Create2的地方 res, addr, returnGas, suberr := evm.Create2(scope.Contract.Address(), input, gas, &endowment, &salt) // ... } 然后正式进入CREATE2阶段 // core/vm/evm.go // The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:] // instead of the usual sender-and-nonce-hash as the address where the contract is initialized at. func (evm EVM) Create2(caller common.Address, code []byte, gas uint64, endowment uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { inithash := crypto.HashData(evm.hasher, code) contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:]) return evm.create(caller, code, gas, endowment, contractAddr, CREATE2) }

// crypto/crypto.go // CreateAddress2 creates an ethereum address given the address bytes, initial // contract code hash and a salt. func CreateAddress2(b common.Address, salt [32]byte, inithash []byte) common.Address { return common.BytesToAddress(Keccak256([]byte{0xff}, b.Bytes(), salt[:], inithash)[12:]) } 通过common.BytesToAddress调用来计算地址,下面这个是公示,这个对应EIP-1014的公式 address = keccak256(0xff || caller || salt || keccak256(initcode))[12:] 从这个公式可以看出来CREATE2创建的地址是跟nonce无关的。 下面是用CREATE2来实现的工厂合约创建实验结果 参数:

  • salt(任何 32 字节的十六进制数):0x0000000000000000000000000000000000000000000000000000000000000001
  • _token:0x617F2E2fD72FD9D5503197092aC168c91465E7f2 [图片] [图片] [图片] CREATE3 [图片] 步骤如下
    1. 假设多条链上都已经存在了CREATE3 的工厂合约且各鏈的地址都相同
    2. 然后开发deploy交易到这个CREATE3 Factory,交易内容包含salt和新合约的init_code
    3. 然后会先用CREATE2来部署fixed_init_code的合约,这个被叫做CREATE2 Proxy。因為 sender_address(CREATE3 Factory)、 salt 和写死的 init_code 都相同,所以各链的 CREATE2 Proxy 地址也是相同的。
    4. 然后CREATE3 Factory就会call刚部署好的CREATE2 Proxy,因为 sender_address(CREATE2 Proxy) 和 sender_nonce (从 1 开始) 都相同,所以各链新合约的地址也是相同的。需要注意的是,此 CREATE2 Proxy 只会用于此部署交易时,也就是下次要部署其他新合约时会带不同的 salt 并部署另一个 CREATE2 Proxy。 步骤1是可以修改的: 一个作法是使用新的 EOA,并在各链拿到 native token 以支付 gas,接着在各链送出第一笔交易 (nonce = 0) 去部署 CREATE3 Factory,达成各链的 CREATE3 Fatcory 地址都相同。 另一个作法是用别人已在各链部署好的 CREATE3 Factory,像是 https://github.com/ZeframLou/create3-factory。其会用 msg.sender 和 salt 再做一次 keccak256,因此可以保证不同人使用也不会算出相同新地址。缺点是想部署到没有 CREATE3 Factory 的新链时,只能使用原本的 deployer 了。

在orderly中是使用CREATE3来创建地址的 address contractAddress = CREATE3.deployDeterministic(creationCode, salt); 根据上面的步骤 首先要用CREATE2部署一个proxy:let proxy := create2(0, 0x10, 0x10, salt) 然后用这个proxy来调用CREATE部署目标合约,下面这行是先预测地址 deployed := keccak256(0x1e, 0x17) 然后会通过proxy来真正调用CREATE来进行合约的创建 call(gas(), proxy, value, add(initCode, 0x20), mload(initCode), 0x00, 0x00) 调用之后机会EVM 开始执行 proxy 合约的字节码 uint256 private constant _PROXY_INITCODE = 0x67363d3d37363d34f03d5260086018f3; 这个字节码的解释在这边,可以看到f0就是CREATE操作 [图片] 调用之后就会执行CREATE,读取内存中的 initCode 计算地址 keccak256(RLP(proxy, nonce=1))[12:] 然后部署合约到该地址并返回新合约地址 参考文献: https://github.com/ethereum/go-ethereum https://www.jianshu.com/p/f319c78e9714 https://blog.csdn.net/RuRu_Bai/article/details/136820594 https://blog.csdn.net/ak19920601/article/details/135065045https://ithelp.ithome.com.tw/articles/10287334 https://learnblockchain.cn/article/9696 https://learnblockchain.cn/article/14032

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
老道
老道
0xbf65...58d8
江湖只有他的大名,没有他的介绍。