在以太坊上部署一个确定性的合约

如何在部署之前确定合约地址。

简介

基于EVM的协议上部署一个新的合约,通常会产生一个无法事先知道的合约地址。幸运的是,EIP-1014中介绍了一种预先计算合约地址的方法。

在这篇文章中,我们将探讨:

  • 合约地址通常是如何产生的。
  • 在部署一个新的合约之前,如何知道一个合约地址。
  • 确定性部署的优势和使用场景是什么。

本文适用于大多数基于EVM的协议--以太坊, Polygon, BNB Smart Chain, Avalanche C-Chain, Fantom, Harmony 以及其他EVM 兼容链。

合约地址如何产生

每当一个新的合约被部署到基于EVM的网络中时,有几个变量被用来生成合约地址,从而导致同一部署者和同一合约出现多个不同的地址。尽管每个合约地址都是确定部署的,但经典方式和我们后面要介绍的方法之间的主要区别是使用不同的创建函数。

传统上,智能合约的地址是使用部署者地址(发送者)和这个账户发送的交易数量(nonce)来计算的。部署者和nonce经过RLP编码,并用Keccak-256进行Hash 获的:

一个使用pyethereum函数计算地址的例子:

def mk_contract_address(sender, nonce): 
    return sha3(rlp.encode([normalize_address(sender), nonce]))[12:] 

这样一来,即使拥有相同的账户和相同的智能合约代码,如果我们选择重新部署,这个合约的地址也会改变。但是,现在还有一种方法可以预先计算出一个合约地址,并使用这个地址来执行交易,比如向其发送ETH,即使这个地址中还没有任何东西(还没有部署)。

经典方式部署合约

首先,让我们先写一个简单的智能合约,可获取其余额,并使用部署者地址作为构造器参数。该合约还可以存储资金,并允许合约所有者提取资金:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract SimpleWallet {
    address public owner;
    // Only owners can call transactions marked with this modifier
    modifier onlyOwner() {
        require(owner == msg.sender, "Caller is not the owner");
        _;
    }
    constructor(address _owner) payable {
        owner = _owner;
    }
    // Allows the owner to transfer ownership of the contract
    function transferOwnership(address _newOwner) external onlyOwner {
        owner = _newOwner;
    }
    // Returns ETH balance from this contract
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }  
    // Allows contract owner to withdraw all funds from the contract
    function withdraw() external onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }
    // Destroys this contract instance
    function destroy(address payable recipient) public onlyOwner {
        selfdestruct(recipient);
    }
}

如果我们像这样部署这个合约呢?为了简单起见,让我们使用Remix,在Goerli上部署合约。

首先,选择Injected Web3(确保已经安装一个钱包插件,如MetaMask)。你也可以使用代码库( remix-contract-deployer-plugin)在本地环境检查是否有 metamask:

img

一旦你完成了这些,在 MetaMask 选择 Goerli 测试网。

现在,让我们来部署合约,确保MetaMask上选择的网络是正确的:

img

另外,如果,你的账号没有 ETH,可以从水龙头获取一些,现在你可以部署合约了。

点击MetaMask上的账户地址,将其复制到剪贴板上,然后将其作为参数传递给Remix部署选项:

例如,部署0x06908fDbe4a6af2BE010fE3709893fB2715d61a6

img

img

一旦交易被出块,你可以从Remix查看输出。另外,也可以在MetaMask上检查交易,选择最新的一笔。它应该显示 合约部署(Contract Deployment):

img

点击它将显示交易的一些细节。我们点击在区块资源器上查看,将打开Etherscan,可以在浏览器深入检查我们创建的合约。

img

我们可以看到,在地址0x4388C588f2a28343dB614FFd3817eE5459f85760上创建了一个新的SimpleWallet合约实例。请注意,我们事先并不知道会产生什么地址,只有在合约创建和交易出块时才会提供。

部署合约--确定性的方法

有很多方法可以为智能合约生成一个确定性的地址--例如:一个旨在降低Gas成本的方法,以及通过使用汇编代码的老方法。然而,新方法仅仅通过使用智能合约函数和工厂合约就可以实现。

如果我们能在合约部署前预先计算出一个合约地址,并执行向其发送资金等操作,然后让某人在合约部署时才取回这些资金,那会怎样?我们可以通过使用CREATE2函数来实现这一点。

让我们创建一个工厂合约,它也有 SimpleWallet合约,它将使用Solidity文档中所说的CREATE2操作码:加“盐”的合约创建 / create2

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleWallet {
    address public owner;
    // Only owners can call transactions marked with this modifier
    modifier onlyOwner() {
        require(owner == msg.sender, "Caller is not the owner");
        _;
    }
    constructor(address _owner) payable {
        owner = _owner;
    }
    // Allows the owner to transfer ownership of the contract
    function transferOwnership(address _newOwner) external onlyOwner {
        owner = _newOwner;
    }
    // Returns ETH balance from this contract
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
    // Allows contract owner to withdraw all funds from the contract
    function withdraw() external onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }
    // Destroys this contract instance
    function destroy(address payable recipient) public onlyOwner {
        selfdestruct(recipient);
    }
}
contract Factory {
    // Returns the address of the newly deployed contract
    function deploy(
        uint _salt
    ) public payable returns (address) {
        // 不在使用 assembly的新语法调用 create2 ,  仅仅传递 salt 就可以
        // 参考文档:https://learnblockchain.cn/docs/solidity/control-structures.html#create2
        return address(new SimpleWallet{salt: bytes32(_salt)}(msg.sender));
    }

    // 1. 获取待部署合约字节码
    function getBytecode()
        public
        view
        returns (bytes memory)
    {
        bytes memory bytecode = type(SimpleWallet).creationCode;
        return abi.encodePacked(bytecode, abi.encode(msg.sender));
    }
    /** 2. 计算待部署合约地址
        params:
            _salt: 随机整数,用于预计算地址
    */ 
    function getAddress(uint256 _salt)
        public
        view
        returns (address)
    {
        // Get a hash concatenating args passed to encodePacked
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff), // 0
                address(this), // address of factory contract
                _salt, // a random salt
                keccak256(getBytecode()) // the wallet contract bytecode
            )
        );
        // Cast last 20 bytes of hash to address
        return address(uint160(uint256(hash)));
    }
}

让我们再次使用Remix在Goerli上以经典的方式部署这个工厂合约,这样我们以后就可以用它来预先计算部署地址。

同样,在Remix中选择部署选项,并将要部署的合约切换到 Factory,点击部署:

img

部署被确认后,选择已部署的合约,展开合约可用功能(参考部署工厂合约的链上地址)。

我们现在要做的是部署一个新的 SimpleWallet合约实例,但要提前知道它的合约地址。现在Factory合约允许我们预先计算这个地址:

img

getAddress函数返回一个新的SimpleWallet实例的预计算的地址。传递一个salt参数,就可返回这个地址。为了简单,我们将使用123作为盐,但它可以是任何uint256值。值得注意的是,如果你使用你自己部署的工厂合约,地址会是不同的,因为getAddress函数利用工厂合约实例地址来计算新的SimpleWallet实例地址。然而,你仍然可以使用本教程中已经部署的工厂来得到相同的地址(如果使用相同的盐)。

让我们把123作为参数传给getAddress函数,并在Remix中执行:

img

在这个特殊的例子中,预先计算的地址是0xf49521d876d1add2D041DC220F13C7D63c10E736

现在我们预先知道了 SimpleWallet将通过我们的 Factory合约被部署在哪个合约地址,让我们向它发送一些资金。如果里面还没有代码存在,不要担心,我们以后会取回资金。

进入MetaMask,输入由getAddress函数返回的合约地址,然后发送一些ETH。

现在,让我们实际部署 SimpleWallet合约,检查它是否正确部署到之前预先计算的地址上。在Remix中,在Factory合约实例中找到Deploy函数,并传递123作为盐。等待交易,并前往Etherscan确认它是否正确部署:

img

在交易细节部分(在Etherscan上)选择 Internal Txns标签:

img

在页面上,我们看到CREATE2函数被我们的工厂合约调用,一个新的SimpleWallet合约被创建。点击创建的合约的地址,检查地址是否与之前预先计算的相同。

img

注意,由于Etherscan的Goerli网络似乎是运行在OpenEthereum节点上,CREATE2在Etherscan界面中被渲染成CREATE。(参考 OpenEthereum Issue 10922)。

img

如果一切正确,我们应该能够提取之前发送至SimpleWallet合约的ETH金额。但首先,我们需要一种方式与之交互。

为了简单,你也可以在Etherscan上验证该合约(例子合约代码。),使用MetaMask连接到它并提取资金。

与合约交互

为了能够取回资金,我们需要一种方法来与我们的 SimpleWallet合约交互。

首先,让我们开始一个新的Node.js项目并安装一些必要的软件包。

mkdir deterministic-deployment-factory && cd deterministic-deployment-factory
npm init --y
npm i ethers

创建一个名为SimpleWalletAbi.js的新文件并粘贴以下内容。这个ABI文件将作为合约的接口,这样就可以使用ethers.js库与之交互:

const abi =  [
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "_owner",
                "type": "address"
            }
        ],
        "stateMutability": "payable",
        "type": "constructor"
    },
    {
        "inputs": [
            {
                "internalType": "address payable",
                "name": "recipient",
                "type": "address"
            }
        ],
        "name": "destroy",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "getBalance",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "owner",
        "outputs": [
            {
                "internalType": "address",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "_newOwner",
                "type": "address"
            }
        ],
        "name": "transferOwnership",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "withdraw",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
];
module.exports = {
    abi
}

现在,创建index.js文件,并添加与合约交互所需的初始配置:

const { ethers } = require("ethers");
const { abi } = require("./SimpleWalletAbi");
const PRIVATE_KEY =
  "<YOUR_PRIVATE_KEY>";
const simpleWalletAddress = "<YOUR_INSTANCE_ADDRESS>";
const main = async () => {
  // Inits a new ethers object with a provider
  const provider = ethers.getDefaultProvider(
    "<YOUR_GOERLI_PROVIDER>"
  );
  // Inits a new ethers wallet to send transactions
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
  // 初始化合约实例
  const simpleWallet = new ethers.Contract(simpleWalletAddress, abi, signer);
}
main();

你可以从Chainstack获得一个免费的公共节点,作为Goerli Provider 节点。 一旦初始配置完成,可以通过运行脚本来检查是否一切正常并且没有错误。

node index.js

现在,添加一个简单的查询来获取合约的余额。在这个例子中,我们向合约发送了0.2 GoerliETH,所以它应该正确显示发送的金额。

const { ethers } = require("ethers");
const { abi } = require("./SimpleWalletAbi");
const PRIVATE_KEY = "<YOUR_PRIVATE_KEY>";
const simpleWalletAddress = "<YOUR_INSTANCE_ADDRESS>";
const main = async () => {
  // Inits a new ethers object with a provider
  const provider = ethers.getDefaultProvider("<YOUR_GOERLI_PROVIDER>");
  // Inits a new ethers wallet to send transactions
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
  // 初始化合约实例
  const simpleWallet = new ethers.Contract(simpleWalletAddress, abi, signer);
  // 调用getBalance获取合约余额
  provider.getBalance(simpleWalletAddress).then((balance) => {
 // convert a currency unit from wei to ether
 const balanceInEth = ethers.utils.formatEther(balance)
 console.log(`Current balance in SimpleWallet: ${balanceInEth} ETH`)
})
};
main();

再次运行该脚本,现在应该输出了SimpleWallet中的当前余额。

现在,取回存储在合约中的资金。在脚本中添加以下代码并再次运行。我们应该能收到存储在合约中的ETH,并更新了合约余额:

const { ethers } = require("ethers");
const { abi } = require("./SimpleWalletAbi");
const PRIVATE_KEY = "<YOUR_PRIVATE_KEY>";
const simpleWalletAddress = "<YOUR_INSTANCE_ADDRESS>";
const main = async () => {
  // Inits a new ethers object with a provider
  const provider = ethers.getDefaultProvider("<YOUR_GOERLI_PROVIDER>");
  // Inits a new ethers wallet to send transactions
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
  // 初始化合约实例
  const simpleWallet = new ethers.Contract(simpleWalletAddress, abi, signer);
  // Withdraw funds from the contract
  try {
    console.log("Attempting to withdraw funds...");
    const receipt = await simpleWallet.withdraw();
    console.log("Funds withdrawn! :)");
    console.log(receipt);
  } catch (error) {
    console.log("Funds can't be withdrawn");
    console.error(error);
  }

  // 调用getBalance获取合约余额
  provider.getBalance(simpleWalletAddress).then((balance) => {
 // convert a currency unit from wei to ether
 const balanceInEth = ethers.utils.formatEther(balance)
 console.log(`Current balance in SimpleWallet: ${balanceInEth} ETH`)
})
};
main();

最后一次运行该脚本。一旦交易成功,资金将被提取回你的地址,合约余额将显示为0

如果你需要的话,完整的代码可以在Github仓库

总结

预先计算合约的地址可以增加去中心化应用的安全性和可靠性,因为智能合约的代码(一般)是相同的,不会改变。它还允许在合约被销毁后将其重新创建部署到同一地址,以防事情被搞砸。更重要的是,它允许实现反事实交互的使用场景,这些代码是由特定的init代码创建的,甚至可以在链外生成。

在这篇文章中,主要介绍如何为我们的智能合约设置一个确定的地址,以及介绍了:

  • 合约地址通常是如何产生的。
  • 在部署一个新的合约实例之前,我们怎样才能知道一个合约地址。
  • 确定性部署的优势和使用场景是什么。

本翻译由 Duet Protocol 赞助支持。

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

  • 发表于 2022-07-20 11:53
  • 阅读 ( 816 )
  • 学分 ( 67 )
  • 分类:以太坊

0 条评论

请先 登录 后评论
翻译小组
翻译小组

首席翻译官

146 篇文章, 23104 学分