使用Brownie开发Fund Me智能合约

使用Brownie开发Fund Me智能合约_第1张图片

在「Solidity入门-开发众筹智能合约」一文中,实现了名为Fund Me的众筹智能合约,但开发过程还是比较粗糙的,本文使用Brownie框架将其完善一下,主要涉及内容如下:

  • 1.Brownie基本使用

  • 2.实现单元测试

  • 3.Brownie添加区块链网络

  • 4.使用Mock功能

  • 5.使用Fork功能

因为智能合约发布后,便不可以更改,所以在发布前,通常需要进行大量的测试,测试代码一般比业务代码多,所以如何优雅的实现单元测试就很重要。

此外,相比于传统的开发,智能合约在开发时也会更加麻烦一些,比如我们需要使用外部的智能合约,而外部的智能合约却在主网上,此时就需要使用Mock或Fork的方式来解决,从而实现本地的调试。

项目代码:https://github.com/ayuLiao/brownie-fund-me

玩得开心

Brownie基本使用

创建名为【brownie-fund-me】的文件夹,然后brownie init初始化brownie项目,将Github中FundMe.sol中代码复制到contracts目录中(这里就不再提FundMe.sol中代码,相关细节可看Solidity入门-开发众筹智能合约)。

通过brownie compile编译一下,此时会出现第一个问题:因为FundMe合约中使用了外部合约,brownie无法获取。

使用Brownie开发Fund Me智能合约_第2张图片

与Remix在线IDE不同,Remix看到代码中有@符号的import会自动从npm中下载,但brownie不能,但brownie可以从github中下载相应的内容,通常外部的智能合约除了上传到npm还会上传到github中,我们可以新增brownie-config.yaml配置文件,新增如下内容:

dependencies:
  # - @
  - smartcontractkit/[email protected]
compiler:
  solc:
    remappings:
      - '@chainlink=smartcontractkit/[email protected]'

配置中,通过remappings关联@chainlike与对于的github项目。

后续brownie遇到@chainlink便会将其替换成smartcontractkit/[email protected]

随后,我们在对合约进行编译,便可以正常编译。

使用Brownie开发Fund Me智能合约_第3张图片

使用brownie部署合约时,brownie默认会部署到本地的区块链网络中(由ganache-cli提供),但FundMe合约使用了外部的智能合约,这些外部的智能合约必然不会在本地区块链网络中,所以这里直接部署到rinkeby。

跟之前一样,我们使用infer作为我们的节点提供商,从而快速实现将合约部署到不同网络的操作,infer使用的细节可以看「Brownie 开发智能合约(入门使用)」一文。

简单而言,注册infer,在infer上新建项目,从而获得相应的project_id和对应的私钥。

将需要使用私钥,我们已经放在.env文件中,在brownie-config.yaml中使用一下:

dependencies:
  # - @
  - smartcontractkit/[email protected]
compiler:
  solc:
    remappings:
      - '@chainlink=smartcontractkit/[email protected]'
dotenv: .env
wallets:
  from_key: ${PRIVATE_KEY}

yaml配置文件通常是要被git托管的,而私钥等信息,我们不希望被传到github中,所以我们创建.env然后将配置写到.env中,而yarm配置文件中也相应的填写了「dotenv:.env」

.env如下:

PRIVATE_KEY=<在Rinkeby测试网络中你钱包账号的私钥>
WEB3_INFURA_PROJECT_ID=

在scripts目录中,创建deploy.py文件,写入如下代码,进行合约的部署。

部署代码:

from brownie import FundMe
from scripts.utils import get_account


def deploy_fund_me():
    account = get_account()
    # 部署是交易行为
    fund_me = FundMe.deploy({'from': account})
    print(f'Contract deployed to {fund_me.address}')


def main():
    deploy_fund_me()

代码逻辑非常简单,通过get_account()方法获得部署合约时需要使用的账户,然后调用FundMe的deploy方法,部署合约是改变区块的行为,属于交易行为,需要通过account进行签名。

get_account()方法代码如下:

def get_account():
    if network.show_active() == 'development':
        return accounts[0]
    else:
        return accounts.add(config["wallets"]["from_key"])

get_account()方法逻辑简单,如果当前区块链网络环境为development(brownie默认使用的本地环境)则直接返回accounts中的账号。brownie默认会通过ganache-cli在本地构建区块链网络,并默认初始化10个账号供于测试使用。

通过brownie run scripts/deploy.py --network rinkeby将合约部署到rinkeby中。

使用Brownie开发Fund Me智能合约_第4张图片

部署完后,可以看到合约的地址

通过https://rinkeby.etherscan.io/可查看,点击contract部分,发现都是字节码,目前很多项目所谓的智能合约开源只是开源字节码,通过字节码,可以获得操作码(Opcodes),但Opcodes与源码在阅读理解上,还是有很大的差异的,所以研究区块链安全,逆向solidity是必不可少的(后面会写文章分享,已经在学了)。

使用Brownie开发Fund Me智能合约_第5张图片

这里,我们将FundMe.sol的合约源码直接放到etherscan中,方便他人浏览。

放源码最简单的方式,就是在etherscan中直接操作,点击 Verify and Publish,将合约代码以可阅读的方式开源到Etherscan上。

使用Brownie开发Fund Me智能合约_第6张图片

通过上图的形式,我们可以手动将合约的明文代码直接复制到etherscan中(当然,这个过程需要账户授权,所以通常只有合约创建者才有权限修改)。

因为solidity中,import操作其实是将相应的solidity代码复制到当前文件的过程,所以在复制代码到etherscan时,我们也需要将import对于的solidity文件复制进去。

通过brownie可以自动实现上述操作。

在etherscan.io上注册账号,然后进入账号设置页,点击API-KEYs,然后创建一个api-keys, brownie可以通过这个keys1与etherscan交互,从而实现将合约代码明文部署上去的操作。

使用Brownie开发Fund Me智能合约_第7张图片

配置一下.env文件,其中新增的ETHERSCAN_TOKEN,其实就是我们在Etherscan中创建的Api-Key。

PRIVATE_KEY=<在Rinkeby测试网络中你钱包账号的私钥>
WEB3_INFURA_PROJECT_ID=
ETHERSCAN_TOKEN=

然后我们修改一个deploy.py中代码,如下:

from brownie import FundMe
from scripts.utils import get_account


def deploy_fund_me():
    account = get_account()
    # 新增了publish_source=True
    fund_me = FundMe.deploy({'from': account}, publish_source=True)
    print(f'Contract deployed to {fund_me.address}')


def main():
    deploy_fund_me()

在使用FundMe.deploy方法时,新增publish_source参数,便可以实现将合约源码提交到Etherscan中,再次部署,如下图:

使用Brownie开发Fund Me智能合约_第8张图片

从上图可以看出,brownie与api-rinkeby.etherscan.io交互的过程。在rinkeby.etherscan可看到源码

使用Brownie开发Fund Me智能合约_第9张图片

此外,还给出了读合约和写合约的方法。

使用Brownie开发Fund Me智能合约_第10张图片

Mocks

相比于将智能合约发布到rinkeby进行调试的方式,本地调试更快也更方便,但FundMe.sol中的问题便是,外部智能合约无法在本地区块链网络中使用。

我们可以通过Mock来解决。

chainlink无法在本地区块链网络中使用,可以通过mock功能解决。

Mock在传统的前后端开发中也很常见,在后端没有提供完整功能的接口前,前端可以通过Mock的方式模拟后端完整接口返回的数据,从而实现前端功能的开发。这里也是类似的,我们可以在本地区块链网络中实现与外部智能合约返回相同结果的方法,然后在本地使用时,直接替换成Mock的合约。

需要注意,Mock智能合约并不是要你完全实现一个具有相同功能的合约,而是确保同名函数可以返回与真实函数具有相同格式的值则可。

回到FundMe.sol,我们需要在本地Mock出AggregatorV3Interface.sol中的功能,从而提供假数据

幸运的是,我们可以直接使用chainlink-mix项目为我们写好的mock

项目代码:https://github.com/smartcontractkit/chainlink-mix/blob/master/contracts/test/MockV3Aggregator.sol

使用Brownie开发Fund Me智能合约_第11张图片

在contracts目录中创建test命令,然后将MockV3Aggregator.sol复制过去。

deploy.py修改后的代码为:

from brownie import FundMe, MockV3Aggregator, network, config
from scripts.utils import *


def deploy_fund_me():
    account = get_account()
    if network.show_active() not in LOCAL_BLOCKCHAIN_ENVIRONMENTS:
        price_feed_address = config["networks"][network.show_active()]["eth_usd_price_feed"]
    else:
        deploy_mocks()
        # 部署后,最新的MockV3Aggregator
        price_feed_address = MockV3Aggregator[-1].address
    fund_me = FundMe.deploy(
        price_feed_address,
        {'from': account},
        publish_source=config["networks"][network.show_active()].get('verify'))
    print(f'Contract deployed to {fund_me.address}')
    return fund_me


def main():
    deploy_fund_me()

deploy_fund_me()方法会判断当前的网络情况,如果不是本地区块网络,则通过deploy_mocks()方法部署MockV3Aggregator.sol。

部署完后,直接通过MockV3Aggregator[-1].address取出部署的合约地址,然后在部署时传入。

这里有点奇怪的是,在本文前面的内容中,FundMe.deploy方法是不需要传入外部合约地址的,这因为我们在FundMe.sol中已经硬编码写死了,而此时因为我们Mock了外部的合约,所以不能使用硬编码的形式,而是需要我们使用外部传参的形式,通过修改FundMe.sol合约构造函数的方式做到这点:

使用Brownie开发Fund Me智能合约_第12张图片

接着,看一下utils.py中deploy_mocks方法的代码:

from brownie import network, config, accounts, MockV3Aggregator

def deploy_mocks():
    print(f'The active network is {network.show_active()}')
    print('Deploying Mocks...')
    # 先判断此前有没有部署过,没有再部署
    if len(MockV3Aggregator) <= 0:
        MockV3Aggregator.deploy(DECIMALS, STARTING_PRICE, {"from": get_account()})
    print('Mocks Deployed')

因为Mock后的外部合约,其内容是不会改变的,所以只需要部署一次,部署过就不再需要部署了。

实现Mock后,通过brownie run scripts/deploy.py部署合约,从下图可知,合约部署成功:

使用Brownie开发Fund Me智能合约_第13张图片

添加新网络

brownie network list 可以查看当前所有的network,我们可以新增任何EVM的网络到brownie的network中。

新增区块链网络的原因:每次运行brownie,brownie都会通过ganache-cli在本地构建一个新的区块链网络,这样,之前的操作在brownie重新运行时,就会消失。可以通过新增本地网络的形式,通过ganache-cli构建本地网络,然后新增的网络会去链接它,这样就避免brownie每次运行都去新建。

首先,通过ganache-cli启动一个本地区块链网络。

ganache-cli --port 8545 --gasLimit 12000000 --accounts 10 --hardfork istanbul --mnemonic brownie

效果如图:

使用Brownie开发Fund Me智能合约_第14张图片

然后将这个网络添加到Ethereum中,新的网络名为ganache-local,命令如下(注意这是个错误命令,我在这里爬了一段时间坑):

brownie networks add Ethereum ganache-local host=127.0.0.1:8545 chainid=1337

命令效果如下:

使用Brownie开发Fund Me智能合约_第15张图片

然后看networks,可以发现Ethereum中多了一个ganache-local,似乎一切都很正常。

使用Brownie开发Fund Me智能合约_第16张图片

然后我们让brownie使用ganache-local时,命令如下:

brownie run scripts/deploy.py --network ganache-local

运行命令后,会报RPC或HTTP无法连接的错误,这是因为ganache-local的host不是一个完整的连接,我们需要修改一下:

brownie networks modify ganache-local host=http://127.0.0.1:8545

注意host等于http://127.0.0.1:8545,不能缺少http前缀。

使用Brownie开发Fund Me智能合约_第17张图片

再次让brownie使用ganache-local运行区块链网络,效果就正常了。

使用Brownie开发Fund Me智能合约_第18张图片

ganache-cli不要关闭,不然上述命令会报错,我们看到ganache-cli中也会有相应的日志输出。

使用Brownie开发Fund Me智能合约_第19张图片

部署后,build/deployments会有1337目录,存放着abi。

使用Brownie开发Fund Me智能合约_第20张图片

使用合约

在FundMe.sol中新增getEntranceFee方法

function getEntranceFee() public view returns (uint256) {
        // mimimumUSD
        uint256 mimimumUSD = 50 * 10 ** 18;
        uint256 price = getPrice();
        uint256 precision = 1 * 10 ** 18;
        return (mimimumUSD * precision) / price;
    }

使用Brownie开发Fund Me智能合约_第21张图片

智能合约改变后需要再次编译和部署,不然我们使用的依旧是旧合约,而旧合约中是没有getEntranceFee方法的:

brownie compile
brownie run scripts/deploy.py --network ganache-local

在scripts中创建fund_and_withdraw.py,在其中写入与FundMe.sol合约进行交互的逻辑,代码如下:

from brownie import FundMe
from scripts.utils import get_account


def fund():
    fund_me = FundMe[-1]
    account = get_account()
    entrance_fee = fund_me.getEntranceFee()
    print(entrance_fee)
    print(f"The current entry fee is {entrance_fee}")
    print("Funding")
    fund_me.fund({"from": account, "value": entrance_fee})


def withdraw():
    fund_me = FundMe[-1]
    account = get_account()
    fund_me.withdraw({"from": account})


def main():
    fund()
    withdraw()

通过brownie运行

brownie run scripts/fund_and_withdraw.py --network ganache-local

使用Brownie开发Fund Me智能合约_第22张图片

单元测试

智能合约部署后,便无法修改,出现bug了,通常也是切换到新的智能合约上,而旧的合约会一直留在相应的区块中(这有利于我们学习恶意bug,因为旧合约一直都在,我们可以基于这些有问题的旧合约进行学习和恶意bug的利用复现)

为了尽量避免合约有bug,被恶意利用,我们需要写大量的单元测试代码,通常单元测试代码会多于合约的逻辑代码。

在test命令下创建新的py文件,然后写下如下测试代码:

import pytest
from brownie import network, accounts, exceptions
from scripts.utils import *
from scripts.deploy import deploy_fund_me


def test_can_fund_and_withdraw():
    """测试"""
    account = get_account()
    fund_me = deploy_fund_me()
    # entrace fee 入场费
    entrance_fee = fund_me.getEntranceFee() + 100
    tx = fund_me.fund({"from": account, "value": entrance_fee})
    tx.wait(1)
    assert fund_me.addressToAmountFunded(account.address) == entrance_fee
    tx2 = fund_me.withdraw({"from": account})
    tx2.wait(1)
    assert fund_me.addressToAmountFunded(account.address) == 0


def test_only_owner_can_withdraw():
    """测试是否只有合约创建则才能转账"""
    if network.show_active() not in LOCAL_BLOCKCHAIN_ENVIRONMENTS:
        # 不是本地网络,则跳过测试
        pytest.skip("only for local testing")
    fund_me = deploy_fund_me()
    bad_actor = accounts.add()
    # 非合约创建者,转账会失败,而失败是我们期望的结果,所以使用pytest.raises将其包裹
    with pytest.raises(exceptions.VirtualMachineError):
        fund_me.withdraw({"from": bad_actor})

智能合约的单元测试与常规软件开发的单元测试没有太大差别。

上述代码中的test_only_owner_can_withdraw方法需要注意,我们希望测试时出现异常,所以使用pytest.raises捕获了相应的异常。

进行单元测试时,将本地运行着的ganache-cli停掉,使用brownie test时,会默认使用development网络进行测试,而运行中的ganache-cli可能会一些有数据,在测试时,可能会因数据不符而失败。

关停ganache-cli,然后再测试,如下图,图中使用了-k参数,指定只测试其中的一个方法:

使用Brownie开发Fund Me智能合约_第23张图片

测试所有单元测试:

使用Brownie开发Fund Me智能合约_第24张图片

在rinkeby测试链上测试合约:

使用Brownie开发Fund Me智能合约_第25张图片

brownie默认运行的网络是development,你可以通过修改brownie-config.yaml来修改brownie默认运行的网络,这里还是选择development。

使用Brownie开发Fund Me智能合约_第26张图片

fork

因为DeFi的兴起,现在很多智能合约都需要调用外部的合约,这就给本地开发智能合带来了一点麻烦,因为你本地的区块链是没有这些外部的智能合约的。在前面,我们使用Mock的方式解决过这个问题,但如果你调用的外部智能合约比较多,Mock起来也会有点麻烦。

解决这个问题的最佳方式是使用ganache-cli的fork功能,brownie也兼容了ganache-cli的fork功能,让我们在开发时,可以无感的使用。

fork的另外一个好处是,我们调用的外部合约与主网的情况是一致的,因为很多智能合约在测试网络中的情况与主网是有差异的,此时如果你直接上线,结果就很坑。

通过fork,我们可以构建一条与主网相同的区块链网络,它具有主网中的数据,这也就意味着我们fork的区块链网络已经自带了这个我们需要的外部智能合约了,而且与真实的合约是一致的。

与我们熟知的fork不同,比如我们在Github fork一个项目,这个项目的数据会完全同步一份到自己的github账号,而区块链主网数据其实挺大的,如果fork时将数据完全同步到本地,也挺麻烦的。

infer之类的服务帮我们简化了这步,即通过这类服务,它知道我们需要fork一份主网,但它会帮我们做完数据的fork操作,我们只需要通过http或rpc的方式与之交互便可以实现与主网交互同样的效果,而且这类交互还不需要花费真实的金钱。

这样,我们操作本地区块链中数据时,与操作真实的区块链没有啥区别。

这里我们使用https://www.alchemy.com/,alchemy与infer类似,都是节点提供商,两者拥有相似的功能。我们在alchemy中创建账号,然后创建一个app,主要创建的app其network选择以太坊的主网。

使用Brownie开发Fund Me智能合约_第27张图片

创建完后,我们就会获得HTTP的链接地址。

使用Brownie开发Fund Me智能合约_第28张图片

我们通过brownie将这个地址以fork形式添加到networks中,具体命令如下:

brownie networks add development mainnet-fork-dev cmd=ganache-cli host=http://127.0.0.1 port=8545 accounts=10 mnemonic=brownie fork=https://eth-mainnet.alchemyapi.io/v2/<在alchemy中获得>

上述命令添加名为mainnet-fork-dev的区块链网络到development中,我们使用ganache-cli作为创建本地区块链的后端,其原理就是利用了ganache-cli的fork功能。

使用Brownie开发Fund Me智能合约_第29张图片

在mainnet-fork-dev网络中进行部署。

brownie run scripts/deploy.py --network mainnet-fork-dev

使用Brownie开发Fund Me智能合约_第30张图片

在mainnet-fork-dev网络中进行单元测试

brownie test --network mainnet-fork-dev

使用Brownie开发Fund Me智能合约_第31张图片

在brownie中使用fork功能获得一个与主网相同的开发环境还是很简单的。

结尾

后续我会将业余时间花在智能合约安全以及dApp开发上,我们下篇文章见。

你可能感兴趣的:(python,java,区块链,大数据,人工智能)