Brownie + Ganache Fork 合约开发
April 18th, 2022

简介

Brownie 是一个基于python语言智能合约开发框架,与hardhat类似。

Ganache的前身是TestRPC,Ganache可以帮助我们快速启动一个以太坊私链来做开发测试、执行命令、探测区块链状态等。Ganache模拟的是内存中的区块链,它在执行交易时是实时返回,而不等待默认的出块时间,这样我们就可以快速验证代码。它同时还是一个支持自动化测试的功能强大的客户端。

Brownie安装

python3 -m pip install eth-brownie
# brownie version 1.18.1
vim ~/.zshrc # or .bashrc
# add python bin to env
export PATH=/Users/[your username]/Library/Python/3.8/bin:$PATH

Ganache安装

npm install ganache --global

如果安装失败提示"Permission issue",加上"sudo"

安装需要依赖 node 环境

Brownie基本使用原则

创建文件夹后brownie init 命令生成目录

项目目录结构如下:

.
├── build
│   ├── contracts
│   │   ├── ERC20.json
│   │   ├── IERC20.json
│   │   ├── IERC20Metadata.json
│   ├── deployments
│   │   ├── 1337
│   │   │   └── 0x58014f69691c46eB3a077E5e90C0F6BFeF5FE46a.json
│   │   └── map.json
│   └── interfaces
├── contracts
│   └── M_token.sol
├── interfaces
├── reports
├── scripts
└── tests
  • contracts 目录下存放合约源码
  • build中存放合约的abi json,deployments中存放对应chainid的abi json
  • interfaces 存放合约接口定义
  • scripts 存放执行的python脚本
  • tests 存放用于测试的python脚本

brownie compile 命令编译所写的合约 (**注意:**这个命令会自动安装对应版本的solc编译器,但是amd64架构在mac不能run)

brownie networks list 显示已有的以太坊系区块链网络 (注意:经测试,其中brownie自带的Development中bsc-main-fork的主网fork功能是无法正常使用的,有些地址的链上数据无法找到,因此建议使用下文的方法自行添加节点进行fork)

brownie console 可以启动一个brownie命令行 解释执行python脚本

基于主网fork的brownie开发 测试 部署教程

主网fork功能

Ganache新增了一个非常好用的功能是一键fork主网当前的某个区块高度的状态到本地,进行私人开发。(这个功能在hardhat上也有)

首先我们先注册一个RPC Endpoint节点,我使用的是https://moralis.io/的节点服务,注册登录后,选择“Speedy Nodes”,然后点击BSC Network(以BSC主网fork为例),会出现很多个节点链接的弹窗,选择”Mainnet Archive“下的链接进行复制。

然后在本机命令行中,输入命令开启主网fork

ganache-cli --fork https://speedy-nodes-nyc.moralis.io/your_id/bsc/mainnet/archive

该fork功能有更多参数(助记词、区块高度、账户数等)可以参见--help进行查看

执行成功后 部分返回结果如下:

==================
Location:        https://speedy-nodes-nyc.moralis.io/your_id/bsc/mainnet/archive
Block:           17020664
Network ID:      56
Time:            Sun Apr 17 2022 02:03:54 GMT-0700 (Pacific Daylight Time)

Chain Id
==================
1337

RPC Listening on 127.0.0.1:8545

可以看到block height、network id、chainid、host等信息。

brownie连接

brownie添加网络

brownie networks add Ethereum local-bsc-fork host=http://127.0.0.1:8545 chainid=1337

其中local-bsc-fork是自定义命名,Ethereum是网络分类,host和chainid分别按照上条ganache命令的返回结果填入

添加成功后,brownie networks list命令查看是否在网络列表中

输入brownie console --network local-bsc-fork命令可以开启brownie命令行

也可以使用 brownie run your_script --network local-bsc-fork 命令连接本地环境跑脚本

brownie命令行

在brownie命令行中可以与自己的合约和区块链环境进行交互。

**注意:**交互时注意本机的代理模式,科学上网有可能导致区块链无法正常交互

区块链账号交互

  • 命令 accounts或者a可以访问本地帐号

    >>> accounts
    ['0xC0BcE0346d4d93e30008A1FE83a2Cf8CfB9Ed301','0xf414d65808f5f59aE156E51B97f98094888e7d92']
    >>> accounts[0]
    <Account object '0xC0BcE0346d4d93e30008A1FE83a2Cf8CfB9Ed301'>
    
  • accounts.add() 增加账号

    >>> accounts.add()
    mnemonic: 'rice cement vehicle ladder end engine tiger gospel toy inspire steel teach'
    <LocalAccount '0x7f1eCD32aF08635A3fB3128108F6Eb0956Efd532'>
    

    也可以通过accounts.add(your_private_key) 来导入特定账户

    >>> accounts.add('0xca751356c37a98109fd969d8e79b42d768587efc6ba35e878bc8c093ed95d8a9')
    <LocalAccount '0xf6c0182eFD54830A87e4020E13B8E4C82e2f60f0'>
    

    在测试环境中,如果没有某账户的私钥,也可以通过accounts.at方法强行引入,适合冒充其他特定账户行为进行测试

  • 交易替换

    transaction.replace() 使用更高gas price替换正在pending的交易

    >>> tx = accounts[0].transfer(accounts[1], 100, required_confs=0, gas_price="1 gwei")
    Transaction sent: 0xc1aab54599d7875fc1fe8d3e375abb0f490cbb80d5b7f48cedaa95fa726f29be
        Gas price: 13.0 gwei   Gas limit: 21000   Nonce: 3
    <Transaction object '0xc1aab54599d7875fc1fe8d3e375abb0f490cbb80d5b7f48cedaa95fa726f29be'>
    
    >>> tx.replace(1.1)
    Transaction sent: 0x9a525e42b326c3cd57e889ad8c5b29c88108227a35f9763af33dccd522375212
        Gas price: 14.3 gwei   Gas limit: 21000   Nonce: 3
    <Transaction '0x9a525e42b326c3cd57e889ad8c5b29c88108227a35f9763af33dccd522375212'>
    

合约交互

  • 合约部署

    >>> t = Token.deploy("Test Token", "TST", 18, 1e23, {'from': accounts[1]})
    
    Transaction sent: 0x2e3cab83342edda14141714ced002e1326ecd8cded4cd0cf14b2f037b690b976
    Transaction confirmed - block: 1   gas spent: 594186
    Contract deployed at: 0x5419710735c2D6c3e4db8F30EF2d361F70a4b380
    <Token Contract object '0x5419710735c2D6c3e4db8F30EF2d361F70a4b380'>
    
    >>> t
    <Token Contract object '0x5419710735c2D6c3e4db8F30EF2d361F70a4b380'>
    >>> Token
    [<Token Contract object '0x5419710735c2D6c3e4db8F30EF2d361F70a4b380'>]
    
  • 合约调用

    合约调用有两种方式,一种是上链消耗gas的交易,一种是静态调用,返回结果是 TransactionReceipt对象,代码示例如下

    >>> Token[0].transfer(accounts[1], 1e18, {'from': accounts[0]})
    
    Transaction sent: 0x6e557594e657faf1270235bf4b3f27be7f5a3cb8a9c981cfffb12133cbaa165e
    Token.transfer confirmed - block: 4   gas used: 51019 (33.78%)
    <Transaction object '0x6e557594e657faf1270235bf4b3f27be7f5a3cb8a9c981cfffb12133cbaa165e'>
    
    >>> Token[0].transfer.call(accounts[1], 1e18, {'from': accounts[0]})
    True # 交易结果
    

    Transaction参数,除了from,其他参数具体可参见https://eth-brownie.readthedocs.io/en/stable/core-contracts.html

    另外如果调用的函数是不发生状态改变的静态函数,使用上述方法只返回结果,均不消耗gas,也不上链。如果有上链需求,使用ContractCall.transact方法,示例如下

    # 不上链,静态调用
    >>> Token[0].balanceOf(accounts[0])
    1000000000000000000000
    
    # 上链
    >>> tx = Token[0].balanceOf.transact(accounts[0])
    
    Transaction sent: 0xe803698b0ade1598c594b2c73ad6a656560a4a4292cc7211b53ffda4a1dbfbe8
    Token.balanceOf confirmed - block: 3   gas used: 23222 (18.85%)
    <Transaction object '0xe803698b0ade1598c594b2c73ad6a656560a4a4292cc7211b53ffda4a1dbfbe8'>
    >>> tx.return_value
    1000000000000000000000
    
  • 利用接口实例化合约对象

    只要在contracts和interfaces中声明的合约接口,都可以通过interface对象进行调用和实例化。

    示例如下

    >>> interface.Dai
    <InterfaceConstructor 'Dai'>
    
    # 填入地址参数,可以实例化Dai合约
    >>> interface.Dai("0x6B175474E89094C44Da98b954EedeAC495271d0F")
    <Dai Contract object '0x6B175474E89094C44Da98b954EedeAC495271d0F'>
    

    也可以通过abi实例化合约,使用Contract.from_abi方法

    >>> Contract.from_abi("Token", "0x79447c97b6543F6eFBC91613C655977806CB18b0", abi)
    <Token Contract object '0x79447c97b6543F6eFBC91613C655977806CB18b0'>
    

    对于ethereum上的合约可以通过Contract.from_explorer方法示例化合约

    >>> Contract.from_explorer("0x6b175474e89094c44da98b954eedeac495271d0f")
    Fetching source of 0x6B175474E89094C44Da98b954EedeAC495271d0F from api.etherscan.io...
    <Dai Contract '0x6B175474E89094C44Da98b954EedeAC495271d0F'>
    

交易Inspecting & Debugging

brownie中发送一个交易后会返回一个TransactionReceipt对象,这个交易收据对象有很多用于inspect和debug的函数。

  • info方法

    查看交易信息

    >>> tx = Token[0].transfer(accounts[1], 1e18, {'from': accounts[0]})
    
    Transaction sent: 0xa7616a96ef571f1791586f570017b37f4db9decb1a5f7888299a035653e8b44b
    Token.transfer confirmed - block: 2   gas used: 51019 (33.78%)
    
    >>> tx.info()
    
    Transaction was Mined
    ---------------------
    Tx Hash: 0xa7616a96ef571f1791586f570017b37f4db9decb1a5f7888299a035653e8b44b
    From: 0x4FE357AdBdB4C6C37164C54640851D6bff9296C8
    To: 0xDd18d6475A7C71Ee33CEBE730a905DbBd89945a1
    Value: 0
    Function: Token.transfer
    Block: 2
    Gas Used: 51019 / 151019 (33.8%)
    
    Events In This Transaction
    --------------------------
    Transfer
        from: 0x4fe357adbdb4c6c37164c54640851d6bff9296c8
        to: 0xfae9bc8a468ee0d8c84ec00c8345377710e0f0bb
        value: 1000000000000000000
    
  • events成员

    查看交易触发的事件

    >>> tx.events
    {
        'CountryModified': [
            {
                'country': 1,
                'limits': (0, 0, 0, 0, 0, 0, 0, 0),
                'minrating': 1,
                'permitted': True
            },
            {
                'country': 2,
                'limits': (0, 0, 0, 0, 0, 0, 0, 0),
                'minrating': 1,
                'permitted': True
            }
        ],
        'MultiSigCallApproved': [
            {
                'callHash': "0x0013ae2e37373648c5161d81ca78d84e599f6207ad689693d6e5938c3ae4031d",
                'callSignature': "0xa513efa4",
                'caller': "0xF9c1fd2f0452FA1c60B15f29cA3250DfcB1081b9",
                'id': "0x8be1198d7f1848ebeddb3f807146ce7d26e63d3b6715f27697428ddb52db9b63"
            }
        ]
    }
    

    events可以list或者dict的方式去访问。

  • 查看内部转账和合约创建

    TransactionReceipt.internal_transfers 查看内部ether(bnb)转账

    >>> tx.internal_transfers
    [
        {
            "from": "0x79447c97b6543F6eFBC91613C655977806CB18b0",
            "to": "0x21b42413bA931038f35e7A5224FaDb065d297Ba3",
            "value": 100
        }
    ]
    

    TransactionReceipt.new_contracts查看新合约创建

    >>> tx = deployer.deployNewContract() # 调用deployer合约的deployNewContract方法
    Transaction sent: 0x6c3183e41670101c4ab5d732bfe385844815f67ae26d251c3bd175a28604da92
      Gas price: 0.0 gwei   Gas limit: 79781
      Deployer.deployNewContract confirmed - Block: 4   Gas used: 79489 (99.63%)
    
    >>> tx.new_contracts
    ["0x1262567B3e2e03f918875370636dE250f01C528c"]
    
    >>> Token.at(tx.new_contracts[0]) # 实例化合约
    <Token Contract object '0x1262567B3e2e03f918875370636dE250f01C528c'>
    
  • 交易debug

    TransactionReceipt.revert_msg查看交易revert信息

    >>> tx.revert_msg
    'Insufficient Balance'
    

    TransactionReceipt.traceback以类python风格显示报错点

    >>> tx.traceback()
    Traceback for '0xd31c1c8db46a5bf2d3be822778c767e1b12e0257152fcc14dcf7e4a942793cb4':
    Trace step 169, program counter 3659:
        File "contracts/SecurityToken.sol", line 156, in SecurityToken.transfer:
        _transfer(msg.sender, [msg.sender, _to], _value);
    Trace step 5070, program counter 5666:
        File "contracts/SecurityToken.sol", lines 230-234, in SecurityToken._transfer:
        _addr = _checkTransfer(
            _authID,
            _id,
            _addr
        );
    Trace step 5197, program counter 9719:
        File "contracts/SecurityToken.sol", line 136, in SecurityToken._checkTransfer:
        require(balances[_addr[SENDER]] >= _value, "Insufficient Balance")
    

    TransactionReceipt.call_trace可以展示该失败交易的整个过程中的跳转map

    >>> tx.call_trace()
    Call trace for '0x7824c6032966ca2349d6a14ec3174d48d546d0fb3020a71b08e50c7b31c1bcb1':
    Initial call cost  [21228 gas]
    LiquidityGauge.deposit  0:3103  [64010 / 128030 gas]
    ├── LiquidityGauge._checkpoint  83:1826  [-6420 / 7698 gas]
    │   ├── GaugeController.get_period_timestamp  [STATICCALL]  119:384  [2511 gas]
    │   ├── ERC20CRV.start_epoch_time_write  [CALL]  411:499  [1832 gas]
    │   ├── GaugeController.gauge_relative_weight_write  [CALL]  529:1017  [3178 / 7190 gas]
    │   │   └── GaugeController.change_epoch  697:953  [2180 / 4012 gas]
    │   │       └── ERC20CRV.start_epoch_time_write  [CALL]  718:806  [1832 gas]
    │   └── GaugeController.period  [STATICCALL]  1043:1336  [2585 gas]
    ├── LiquidityGauge._update_liquidity_limit  1929:2950  [45242 / 54376 gas]
    │   ├── VotingEscrow.balanceOf  [STATICCALL]  1957:2154  [2268 gas]
    │   └── VotingEscrow.totalSupply  [STATICCALL]  2180:2768  [6029 / 6866 gas]
    │       └── VotingEscrow.supply_at  2493:2748  [837 gas]
    └── ERC20LP.transferFrom  [CALL]  2985:3098  [1946 gas]
    

    每一行的显示规则为

    ContractName.functionName (external call opcode) start:stop [internal / total gas used]
    

    如果该方法中填入True参数,可以展示更详细的视图

    >>> history[-1].call_trace(True)
    
    Call trace for '0x7824c6032966ca2349d6a14ec3174d48d546d0fb3020a71b08e50c7b31c1bcb1':
    Initial call cost  [21228 gas]
    LiquidityGauge.deposit  0:3103  [64010 / 128030 gas]
    ├── LiquidityGauge._checkpoint  83:1826  [-6420 / 7698 gas]
    │   │
    │   ├── GaugeController.get_period_timestamp  [STATICCALL]  119:384  [2511 gas]
    │   │       ├── address: 0x0C41Fc429cC21BC3c826efB3963929AEdf1DBb8e
    │   │       ├── input arguments:
    │   │       │   └── p: 0
    │   │       └── return value: 1594574319
    ...
    

    通过访问TransactionReceipt.subcalls成员可以展示子调用的详细信息

    >>> history[-1].subcalls
    [
        {
            'from': "0x5AE569698C5F986665018B6e1d92A71be71DEF9a",
            'function': "get_period_timestamp(int128)",
            'inputs': {
                'p': 0
            },
            'op': "STATICCALL",
            'return_value': (1594574319,),
            'to': "0x0C41Fc429cC21BC3c826efB3963929AEdf1DBb8e"
        },
    ...
    
  • 更多详细的命令,可以查看brownie官方手册....

参考链接

Subscribe to TXXM
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from TXXM

Skeleton

Skeleton

Skeleton