Skip to content

Hardhat开发流程

作者:Kevin Neilson & Erin Shaben

概览

在本教程中,我们将从头至尾详细演示在Hardhat开发环境中启动pooled staking DAO(汇集质押DAO)合约的典型开发流程。

我们将组装staking DAO的组件并编译必要合约。然后,使用staking DAO相关的各类测试用例构建测试套件并在本地开发节点上运行。最后,我们将staking DAO部署至Moonbase Alpha和Moonbeam并通过Hardhat Etherscan插件验证合约。如果您尚未了解Hardhat,建议您先阅读Hardhat教程

此处所有信息由第三方提供,仅供参考之用。Moonbeam不为Moonbeam文档网站(https://docs.moonbeam.network/)上列出和描述的任何项目背书。

查看先决条件

开始之前,您需要准备以下内容:

创建Hardhat项目

如果您尚未拥有Hardhat项目,您可以通过完成以下步骤创建一个:

  1. 为您的项目创建一个目录

    mkdir stakingDAO && cd stakingDAO
    
  2. 初始化将要创建一个package.json文件的项目

    npm init -y
    
  3. 安装Hardhat

    npm install hardhat
    
  4. 创建项目

    npx hardhat init
    

    注意事项

    npx用于运行项目中本地安装的可执行文件。Hardhat虽然可以全局安装,但是建议在每个项目本地安装,这样可以逐个项目地控制版本。

  5. 随后会显示菜单,允许您创建新项目或使用示例项目。在本示例中,您可以选择Create an empty hardhat.config.js

Create an empty Hardhat project.

这将在您的项目目录中创建一个Hardhat配置文件(hardhat.config.js)。

添加智能合约

本教程中使用的智能合约比Hardhat概览中的更为复杂,但是此合约的特性更适合展示Hardhat的一些高级功能。DelegationDAO.sol是一个pooled staking DAO,当其达到特定的阈值时使用StakingInterface.sol来自动委托给收集人。像DelegationDAO.sol这样的质押合约允许低于最低协议保证金的委托人联合起来通过将资金放在同一个委托池中赚取部分质押奖励。

注意事项

DelegationDAO.sol未经过审核和审计,仅用于演示目的。其中可能会包含漏洞或者逻辑错误而导致资产损失,请勿在生产环境中使用。

要开始操作,请执行以下步骤:

  1. 创建contracts目录以存放项目的智能合约

    mkdir contracts
    
  2. 创建名为DelegationDAO.sol的新文件

    touch contracts/DelegationDAO.sol
    
  3. DelegationDAO.sol中的内容复制并粘贴至DelegationDAO.sol

    DelegationDAO.sol
    // SPDX-License-Identifier: GPL-3.0-only
    // This is a PoC to use the staking precompile wrapper as a Solidity developer.
    pragma solidity >=0.8.0;
    
    import "./StakingInterface.sol";
    import "@openzeppelin/contracts/access/AccessControl.sol";
    import "@openzeppelin/contracts/utils/Address.sol";
    
    
    contract DelegationDAO is AccessControl {
    
        // Role definition for contract members
        bytes32 public constant MEMBER = keccak256("MEMBER");
    
        // Possible states for the DAO to be in:
        // COLLECTING: the DAO is collecting funds before creating a delegation once the minimum delegation stake has been reached
        // STAKING: the DAO has an active delegation
        // REVOKING: the DAO has scheduled a delegation revoke
        // REVOKED: the scheduled revoke has been executed
        enum daoState {
            COLLECTING,
            STAKING,
            REVOKING,
            REVOKED
        }
    
        // Current state that the DAO is in
        daoState public currentState;
    
        // Member stakes (doesnt include rewards, represents member shares)
        mapping(address => uint256) public memberStakes;
    
        // Total Staking Pool (doesnt include rewards, represents total shares)
        uint256 public totalStake;
    
        // The ParachainStaking wrapper at the known pre-compile address. This will be used to make
        // all calls to the underlying staking solution
        ParachainStaking public staking;
    
        // Minimum Delegation Amount
        uint256 public constant minDelegationStk = 5 ether;
    
        // Moonbeam Staking Precompile address
        address public constant stakingPrecompileAddress =
            0x0000000000000000000000000000000000000800;
    
        // The collator that this DAO is currently nominating
        address public target;
    
        // Event for a member deposit
        event deposit(address indexed _from, uint _value);
    
        // Event for a member withdrawal
        event withdrawal(address indexed _from, address indexed _to, uint _value);
    
        // Initialize a new DelegationDao dedicated to delegating to the given collator target.
        constructor(address _target, address admin) {
            // Directly grant roles
            _grantRole(DEFAULT_ADMIN_ROLE, admin);
            _grantRole(MEMBER, admin);
    
            //Sets the collator that this DAO nominating
            target = _target;
    
            // Initializes Moonbeam's parachain staking precompile
            staking = ParachainStaking(stakingPrecompileAddress);
    
            //Initialize the DAO state
            currentState = daoState.COLLECTING;
        }
    
        // Simple getter to return the target collator of the DAO
        function getTarget() public view returns (address) {
            return target;
        }
    
        // Grant a user the role of admin
        function grant_admin(
            address newAdmin
        ) public onlyRole(DEFAULT_ADMIN_ROLE) onlyRole(MEMBER) {
            grantRole(DEFAULT_ADMIN_ROLE, newAdmin);
            grantRole(MEMBER, newAdmin);
        }
    
        // Grant a user membership
        function grant_member(
            address newMember
        ) public onlyRole(DEFAULT_ADMIN_ROLE) {
            grantRole(MEMBER, newMember);
        }
    
        // Revoke a user membership
        function remove_member(
            address payable exMember
        ) public onlyRole(DEFAULT_ADMIN_ROLE) {
            revokeRole(MEMBER, exMember);
        }
    
        // Increase member stake via a payable function and automatically stake the added amount if possible
        function add_stake() external payable onlyRole(MEMBER) {
            if (currentState == daoState.STAKING) {
                // Sanity check
                if (!staking.isDelegator(address(this))) {
                    revert("The DAO is in an inconsistent state.");
                }
                memberStakes[msg.sender] = memberStakes[msg.sender] + msg.value;
                totalStake = totalStake + msg.value;
                emit deposit(msg.sender, msg.value);
                staking.delegatorBondMore(target, msg.value);
            } else if (currentState == daoState.COLLECTING) {
                memberStakes[msg.sender] = memberStakes[msg.sender] + msg.value;
                totalStake = totalStake + msg.value;
                emit deposit(msg.sender, msg.value);
                if (totalStake < minDelegationStk) {
                    return;
                } else {
                    //initialiate the delegation and change the state
                    staking.delegate(
                        target,
                        address(this).balance,
                        staking.candidateDelegationCount(target),
                        staking.delegatorDelegationCount(address(this))
                    );
                    currentState = daoState.STAKING;
                }
            } else {
                revert("The DAO is not accepting new stakes in the current state.");
            }
        }
    
        // Function for a user to withdraw their stake
        function withdraw(address payable account) public onlyRole(MEMBER) {
            require(
                currentState != daoState.STAKING,
                "The DAO is not in the correct state to withdraw."
            );
            if (currentState == daoState.REVOKING) {
                bool result = execute_revoke();
                require(result, "Schedule revoke delay is not finished yet.");
            }
            if (
                currentState == daoState.REVOKED ||
                currentState == daoState.COLLECTING
            ) {
                //Sanity checks
                if (staking.isDelegator(address(this))) {
                    revert("The DAO is in an inconsistent state.");
                }
                require(totalStake != 0, "Cannot divide by zero.");
                //Calculate the withdrawal amount including staking rewards
                uint amount = address(this).balance * memberStakes[msg.sender] / totalStake;
                require(
                    check_free_balance() >= amount,
                    "Not enough free balance for withdrawal."
                );
                Address.sendValue(account, amount);
                totalStake = totalStake - (memberStakes[msg.sender]);
                memberStakes[msg.sender] = 0;
                emit withdrawal(msg.sender, account, amount);
            }
        }
    
        // Schedule revoke, admin only
        function schedule_revoke() public onlyRole(DEFAULT_ADMIN_ROLE) {
            require(
                currentState == daoState.STAKING,
                "The DAO is not in the correct state to schedule a revoke."
            );
            staking.scheduleRevokeDelegation(target);
            currentState = daoState.REVOKING;
        }
    
        // Try to execute the revoke, returns true if it succeeds, false if it doesn't
        function execute_revoke() internal onlyRole(MEMBER) returns (bool) {
            require(
                currentState == daoState.REVOKING,
                "The DAO is not in the correct state to execute a revoke."
            );
            staking.executeDelegationRequest(address(this), target);
            if (staking.isDelegator(address(this))) {
                return false;
            } else {
                currentState = daoState.REVOKED;
                return true;
            }
        }
    
        // Check how much free balance the DAO currently has. It should be the staking rewards if the DAO state is anything other than REVOKED or COLLECTING.
        function check_free_balance()
            public
            view
            onlyRole(MEMBER)
            returns (uint256)
        {
            return address(this).balance;
        }
    
        // Change the collator target, admin only
        function change_target(
            address newCollator
        ) public onlyRole(DEFAULT_ADMIN_ROLE) {
            require(
                currentState == daoState.REVOKED ||
                    currentState == daoState.COLLECTING,
                "The DAO is not in the correct state to change staking target."
            );
            target = newCollator;
        }
    
        // Reset the DAO state back to COLLECTING, admin only
        function reset_dao() public onlyRole(DEFAULT_ADMIN_ROLE) {
            currentState = daoState.COLLECTING;
        }
    
        // Override _setupRole to use grantRole as _setupRole does not exist in AccessControl anymore
        function _setupRole(bytes32 role, address account) internal virtual {
            grantRole(role, account);
        }
    }
    
  4. contracts目录中创建名为StakingInterface.sol的新文件

    touch contracts/StakingInterface.sol
    
  5. StakingInterface.sol内容复制并粘贴至StakingInterface.sol

    StakingInterface.sol
    // SPDX-License-Identifier: GPL-3.0-only
    pragma solidity >=0.8.3;
    
    /// @dev The ParachainStaking contract's address.
    address constant PARACHAIN_STAKING_ADDRESS = 0x0000000000000000000000000000000000000800;
    
    /// @dev The ParachainStaking contract's instance.
    ParachainStaking constant PARACHAIN_STAKING_CONTRACT = ParachainStaking(
        PARACHAIN_STAKING_ADDRESS
    );
    
    /// @author The Moonbeam Team
    /// @title Pallet Parachain Staking Interface
    /// @dev The interface through which Solidity contracts will interact with Parachain Staking
    /// We follow this same interface including four-byte function selectors, in the precompile that
    /// wraps the pallet
    /// @custom:address 0x0000000000000000000000000000000000000800
    interface ParachainStaking {
        /// @dev Check whether the specified address is currently a staking delegator
        /// @custom:selector fd8ab482
        /// @param delegator the address that we want to confirm is a delegator
        /// @return A boolean confirming whether the address is a delegator
        function isDelegator(address delegator) external view returns (bool);
    
        /// @dev Check whether the specified address is currently a collator candidate
        /// @custom:selector d51b9e93
        /// @param candidate the address that we want to confirm is a collator andidate
        /// @return A boolean confirming whether the address is a collator candidate
        function isCandidate(address candidate) external view returns (bool);
    
        /// @dev Check whether the specifies address is currently a part of the active set
        /// @custom:selector 740d7d2a
        /// @param candidate the address that we want to confirm is a part of the active set
        /// @return A boolean confirming whether the address is a part of the active set
        function isSelectedCandidate(
            address candidate
        ) external view returns (bool);
    
        /// @dev Total points awarded to all collators in a particular round
        /// @custom:selector 9799b4e7
        /// @param round the round for which we are querying the points total
        /// @return The total points awarded to all collators in the round
        function points(uint256 round) external view returns (uint256);
    
        /// @dev Total points awarded to a specific collator in a particular round.
        /// A value of `0` may signify that no blocks were produced or that the storage for that round has been removed
        /// @custom:selector bfea66ac
        /// @param round the round for which we are querying the awarded points
        /// @param candidate The candidate to whom the points are awarded
        /// @return The total points awarded to the collator for the provided round
        function awardedPoints(
            uint32 round,
            address candidate
        ) external view returns (uint32);
    
        /// @dev The amount delegated in support of the candidate by the delegator
        /// @custom:selector a73e51bc
        /// @param delegator Who made this delegation
        /// @param candidate The candidate for which the delegation is in support of
        /// @return The amount of the delegation in support of the candidate by the delegator
        function delegationAmount(
            address delegator,
            address candidate
        ) external view returns (uint256);
    
        /// @dev Whether the delegation is in the top delegations
        /// @custom:selector 91cc8657
        /// @param delegator Who made this delegation
        /// @param candidate The candidate for which the delegation is in support of
        /// @return If delegation is in top delegations (is counted)
        function isInTopDelegations(
            address delegator,
            address candidate
        ) external view returns (bool);
    
        /// @dev Get the minimum delegation amount
        /// @custom:selector 02985992
        /// @return The minimum delegation amount
        function minDelegation() external view returns (uint256);
    
        /// @dev Get the CandidateCount weight hint
        /// @custom:selector a9a981a3
        /// @return The CandidateCount weight hint
        function candidateCount() external view returns (uint256);
    
        /// @dev Get the current round number
        /// @custom:selector 146ca531
        /// @return The current round number
        function round() external view returns (uint256);
    
        /// @dev Get the CandidateDelegationCount weight hint
        /// @custom:selector 2ec087eb
        /// @param candidate The address for which we are querying the nomination count
        /// @return The number of nominations backing the collator
        function candidateDelegationCount(
            address candidate
        ) external view returns (uint32);
    
        /// @dev Get the CandidateAutoCompoundingDelegationCount weight hint
        /// @custom:selector 905f0806
        /// @param candidate The address for which we are querying the auto compounding
        ///     delegation count
        /// @return The number of auto compounding delegations
        function candidateAutoCompoundingDelegationCount(
            address candidate
        ) external view returns (uint32);
    
        /// @dev Get the DelegatorDelegationCount weight hint
        /// @custom:selector 067ec822
        /// @param delegator The address for which we are querying the delegation count
        /// @return The number of delegations made by the delegator
        function delegatorDelegationCount(
            address delegator
        ) external view returns (uint256);
    
        /// @dev Get the selected candidates for the current round
        /// @custom:selector bcf868a6
        /// @return The selected candidate accounts
        function selectedCandidates() external view returns (address[] memory);
    
        /// @dev Whether there exists a pending request for a delegation made by a delegator
        /// @custom:selector 3b16def8
        /// @param delegator the delegator that made the delegation
        /// @param candidate the candidate for which the delegation was made
        /// @return Whether a pending request exists for such delegation
        function delegationRequestIsPending(
            address delegator,
            address candidate
        ) external view returns (bool);
    
        /// @dev Whether there exists a pending exit for candidate
        /// @custom:selector 43443682
        /// @param candidate the candidate for which the exit request was made
        /// @return Whether a pending request exists for such delegation
        function candidateExitIsPending(
            address candidate
        ) external view returns (bool);
    
        /// @dev Whether there exists a pending bond less request made by a candidate
        /// @custom:selector d0deec11
        /// @param candidate the candidate which made the request
        /// @return Whether a pending bond less request was made by the candidate
        function candidateRequestIsPending(
            address candidate
        ) external view returns (bool);
    
        /// @dev Returns the percent value of auto-compound set for a delegation
        /// @custom:selector b4d4c7fd
        /// @param delegator the delegator that made the delegation
        /// @param candidate the candidate for which the delegation was made
        /// @return Percent of rewarded amount that is auto-compounded on each payout
        function delegationAutoCompound(
            address delegator,
            address candidate
        ) external view returns (uint8);
    
        /// @dev Join the set of collator candidates
        /// @custom:selector 1f2f83ad
        /// @param amount The amount self-bonded by the caller to become a collator candidate
        /// @param candidateCount The number of candidates in the CandidatePool
        function joinCandidates(uint256 amount, uint256 candidateCount) external;
    
        /// @dev Request to leave the set of collator candidates
        /// @custom:selector b1a3c1b7
        /// @param candidateCount The number of candidates in the CandidatePool
        function scheduleLeaveCandidates(uint256 candidateCount) external;
    
        /// @dev Execute due request to leave the set of collator candidates
        /// @custom:selector 3867f308
        /// @param candidate The candidate address for which the pending exit request will be executed
        /// @param candidateDelegationCount The number of delegations for the candidate to be revoked
        function executeLeaveCandidates(
            address candidate,
            uint256 candidateDelegationCount
        ) external;
    
        /// @dev Cancel request to leave the set of collator candidates
        /// @custom:selector 9c76ebb4
        /// @param candidateCount The number of candidates in the CandidatePool
        function cancelLeaveCandidates(uint256 candidateCount) external;
    
        /// @dev Temporarily leave the set of collator candidates without unbonding
        /// @custom:selector a6485ccd
        function goOffline() external;
    
        /// @dev Rejoin the set of collator candidates if previously had called `goOffline`
        /// @custom:selector 6e5b676b
        function goOnline() external;
    
        /// @dev Request to bond more for collator candidates
        /// @custom:selector a52c8643
        /// @param more The additional amount self-bonded
        function candidateBondMore(uint256 more) external;
    
        /// @dev Request to bond less for collator candidates
        /// @custom:selector 60744ae0
        /// @param less The amount to be subtracted from self-bond and unreserved
        function scheduleCandidateBondLess(uint256 less) external;
    
        /// @dev Execute pending candidate bond request
        /// @custom:selector 2e290290
        /// @param candidate The address for the candidate for which the request will be executed
        function executeCandidateBondLess(address candidate) external;
    
        /// @dev Cancel pending candidate bond request
        /// @custom:selector b5ad5f07
        function cancelCandidateBondLess() external;
    
        /// @notice DEPRECATED use delegateWithAutoCompound instead for lower weight and better UX
        /// @dev Make a delegation in support of a collator candidate
        /// @custom:selector 829f5ee3
        /// @param candidate The address of the supported collator candidate
        /// @param amount The amount bonded in support of the collator candidate
        /// @param candidateDelegationCount The number of delegations in support of the candidate
        /// @param delegatorDelegationCount The number of existing delegations by the caller
        function delegate(
            address candidate,
            uint256 amount,
            uint256 candidateDelegationCount,
            uint256 delegatorDelegationCount
        ) external;
    
        /// @dev Make a delegation in support of a collator candidate
        /// @custom:selector 4b8bc9bf
        /// @param candidate The address of the supported collator candidate
        /// @param amount The amount bonded in support of the collator candidate
        /// @param autoCompound The percent of reward that should be auto-compounded
        /// @param candidateDelegationCount The number of delegations in support of the candidate
        /// @param candidateAutoCompoundingDelegationCount The number of auto-compounding delegations
        /// in support of the candidate
        /// @param delegatorDelegationCount The number of existing delegations by the caller
        function delegateWithAutoCompound(
            address candidate,
            uint256 amount,
            uint8 autoCompound,
            uint256 candidateDelegationCount,
            uint256 candidateAutoCompoundingDelegationCount,
            uint256 delegatorDelegationCount
        ) external;
    
        /// @dev Request to revoke an existing delegation
        /// @custom:selector 1a1c740c
        /// @param candidate The address of the collator candidate which will no longer be supported
        function scheduleRevokeDelegation(address candidate) external;
    
        /// @dev Bond more for delegators with respect to a specific collator candidate
        /// @custom:selector 0465135b
        /// @param candidate The address of the collator candidate for which delegation shall increase
        /// @param more The amount by which the delegation is increased
        function delegatorBondMore(address candidate, uint256 more) external;
    
        /// @dev Request to bond less for delegators with respect to a specific collator candidate
        /// @custom:selector c172fd2b
        /// @param candidate The address of the collator candidate for which delegation shall decrease
        /// @param less The amount by which the delegation is decreased (upon execution)
        function scheduleDelegatorBondLess(
            address candidate,
            uint256 less
        ) external;
    
        /// @dev Execute pending delegation request (if exists && is due)
        /// @custom:selector e98c8abe
        /// @param delegator The address of the delegator
        /// @param candidate The address of the candidate
        function executeDelegationRequest(
            address delegator,
            address candidate
        ) external;
    
        /// @dev Cancel pending delegation request (already made in support of input by caller)
        /// @custom:selector c90eee83
        /// @param candidate The address of the candidate
        function cancelDelegationRequest(address candidate) external;
    
        /// @dev Sets an auto-compound value for a delegation
        /// @custom:selector faa1786f
        /// @param candidate The address of the supported collator candidate
        /// @param value The percent of reward that should be auto-compounded
        /// @param candidateAutoCompoundingDelegationCount The number of auto-compounding delegations
        /// in support of the candidate
        /// @param delegatorDelegationCount The number of existing delegations by the caller
        function setAutoCompound(
            address candidate,
            uint8 value,
            uint256 candidateAutoCompoundingDelegationCount,
            uint256 delegatorDelegationCount
        ) external;
    
        /// @dev Fetch the total staked amount of a delegator, regardless of the
        /// candidate.
        /// @custom:selector e6861713
        /// @param delegator Address of the delegator.
        /// @return Total amount of stake.
        function getDelegatorTotalStaked(
            address delegator
        ) external view returns (uint256);
    
        /// @dev Fetch the total staked towards a candidate.
        /// @custom:selector bc5a1043
        /// @param candidate Address of the candidate.
        /// @return Total amount of stake.
        function getCandidateTotalCounted(
            address candidate
        ) external view returns (uint256);
    }
    
  6. DelegationDAO.sol依赖于几个标准OpenZeppelin合约。使用以下命令添加库:

    npm install @openzeppelin/contracts
    

Hardhat配置文件

设置hardhat.config.js文件时,我们需要导入一些我们将在本教程中使用的插件。因此,我们首先需要Hardhat Toolbox插件,它可以方便地将Hardhat插件捆绑在一起,用于使用Ethers部署合约并与之交互、使用Mocha和Chai测试合约、使用Etherscan验证合约等等。您可以运行以下命令来安装插件:

npm install --save-dev @nomicfoundation/hardhat-toolbox

如果您需要其他的Hardhat插件,请访问官方Hardhat插件完整列表

对于本教程中的范例,您需要为Moonbase Alpha上的两个账户添加私钥。由于一些测试将在开发节点上完成,因此您还需要添加两个具有预备资金的开发节点账户的私钥,在本例中,我们可以使用Alice和Bob。此外,您还需要添加Moonscan API密钥,该密钥可用于Moonbase Alpha和Moonbeam。

接下来,您可以遵循以下步骤修改hardhat.config.js文件并将Moonbase Alpha添加为网络:

  1. 导入插件
  2. 为您的私钥创建变量

    请记住

    请勿将您的私钥存储至JavaScript文件中。

  3. module.exports中,您需要提供Solidity版本

  4. 添加Moonbase Alpha网络配置。 您可以修改hardhat.config.js文件,使其可用于任何Moonbeam网络:

    moonbeam: {
      url: 'INSERT_RPC_API_ENDPOINT', // Insert your RPC URL here
      chainId: 1284, // (hex: 0x504)
      accounts: [privateKey]
    },
    
    moonriver: {
      url: 'INSERT_RPC_API_ENDPOINT', // Insert your RPC URL here
      chainId: 1285, // (hex: 0x505)
      accounts: [privateKey]
    },
    
    moonbase: {
      url: 'https://rpc.api.moonbase.moonbeam.network',
      chainId: 1287, // (hex: 0x507)
      accounts: [privateKey]
    },
    
    dev: {
      url: 'http://127.0.0.1:9944',
      chainId: 1281, // (hex: 0x501)
      accounts: [privateKey]
    },
    
  5. 导入Moonscan API密钥,用于本教程后续验证部分

// 1. Import the Ethers, Hardhat Toolbox, and Etherscan plugins 
// required to interact with our contracts
require('@nomicfoundation/hardhat-toolbox');

// 2. Create variables for your private keys from your pre-funded Moonbase Alpha 
// testing accounts and your Moonscan API key
const privateKey = 'INSERT_PRIVATE_KEY';
const privateKey2 = 'INSERT_ANOTHER_PRIVATE_KEY';
const moonscanAPIKey = 'INSERT_MOONSCAN_API_KEY';

module.exports = {
  // 3. Specify the Solidity version
  solidity: '0.8.20',
  networks: {
    // 4. Add the Moonbase Alpha network specification
    moonbase: {
      url: 'https://rpc.api.moonbase.moonbeam.network',
      chainId: 1287, // 0x507 in hex
      accounts: [privateKey, privateKey2]
    },
    dev: {
      url: 'http://127.0.0.1:9944',
      chainId: 1281, // 0x501 in hex
      accounts: [
        '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133', // Alice's PK
        '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b' // Bob's PK
      ]
    },
    moonbeam: {
      url: 'https://rpc.api.moonbeam.network', // Or insert your own RPC URL here
      chainId: 1284, // 0x504 in hex
      accounts: [privateKey, privateKey2]
    },
  },
  // 5. Set up your Moonscan API key for contract verification
  // Moonbeam and Moonbase Alpha Moonscan use the same API key
  etherscan: {
    apiKey: {
      moonbaseAlpha: moonscanAPIKey, // Moonbase Moonscan API Key
      moonbeam: moonscanAPIKey, // Moonbeam Moonscan API Key     
    },
  },
};

注意事项

任何发送至Alice和Bob开发账户的真实资产将会马上丢失。采取预防措施,切勿将主网资产发送至公开的开发账户。

现在您可以开始编译并测试合约。

编译合约

您可以简单运行以下命令以编译合约:

npx hardhat compile

Learn how to compile your Solidity contracts with Hardhat.

编译合约后,会创建一个artifacts目录,该目录包含合约的字节码和元数据,即.json文件。建议您将此目录添加至.gitignore

测试

测试套件是一个完整且强大的智能合约开发流程不可或缺的。Hardhat有一系列的工具,可以轻松助您编写和运行测试。在这一部分,您将学习测试智能合约的基本知识和一些高级技术。

Hardhat测试通常使用Mocha和Chai编写,Mocha是一个JavaScript测试框架,Chai是一个BDD/TDD JavaScript断言库。BDD/TDD分别代表行为和测试驱动开发。有效的BDD/TDD需要在编写智能合约代码之前编写测试。本教程的结构没有严格遵循这些准则,但您可能需要在您的开发流程中采用这些原则。Hardhat建议使用Hardhat工具箱,它是一个插件,会将所有开始使用Hardhat所需的插件打包在一起,包括Mocha和Chai。

因为我们最初将在本地Moonbeam节点上运行测试,所以需要指定Alice的地址作为目标收集人的地址(Alice的账户是本地开发节点的唯一收集人)。

0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac

相反,如果您想要通过Moonbase Alpha运行测试,则可以选择以下收集人,或者任何您想要DAO委托给的Moonbase Alpha上的其他收集人

0x12E7BCCA9b1B15f33585b5fc898B967149BDb9a5

配置测试文件

要设置测试文件,请执行以下步骤:

  1. 创建tests目录

    mkdir tests
    
  2. 创建一个名为Dao.js的新文件

    touch tests/Dao.js
    
  3. 然后复制并粘贴以下内容以设置测试文件的初始结构。请仔细阅读注释,因为它们阐明了每行代码的目的

    // 导入Ethers
    const { ethers } = require('hardhat');
    
    // 在此处导入Chai以使用其断言函数
    const { expect } = require('chai');
    
    // 指定Alice的地址作为本地开发节点上的目标收集人
    const targetCollator = '0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac';
    

部署Staking DAO用于测试

在运行任何用例之前,我们需要启动一个带有初始配置的staking DAO,我们此处的设置相对简单,我们将用单个管理员(部署者)部署staking DAO,然后添加新成员至DAO。此简单设置非常适合用于演示,您也很容易想象您想要测试的更复杂的配置,例如有100个DAO成员的场景或者有多个DAO管理员的场景。

Mocha的describe函数使您能够组织您的测试。多个describe函数可以嵌套在一起。它完全是可选的,但在有大量测试用例的复杂项目中极其有用。您可以在Mocha文档网站查阅关于构建测试和Mocha入门操作的更多信息。

我们将定义一个名为deployDao的函数,这将包含我们staking DAO的设置步骤。要配置测试文件,请添加以下代码段:

// The describe function receives the name of a section of your test suite, and a
// callback. The callback must define the tests of that section. This callback
// can't be an async function
describe('Dao contract', function () {
  async function deployDao() {
    // Get the contract factory and signers here
    const [deployer, member1] = await ethers.getSigners();
    const delegationDao = await ethers.getContractFactory('DelegationDAO');

    // Deploy the staking DAO and wait for the deployment transaction to be confirmed
    const deployedDao = await delegationDao.deploy(targetCollator, deployer.address);
    await deployedDao.waitForDeployment();

    // Add a new member to the DAO
    await deployedDao.grant_member(member1.address);

    // Return the deployed DAO and the first member of the DAO to allow the tests to 
    // access and interact with them
    return { deployedDao, member1 };
  };

  // The test cases should be added here

});

编写第一个测试用例

首先,您要创建一个名为Deployment的子部分以分类管理测试文件。其将包含在Dao contract describe函数中。接下来,您将使用it Mocha函数定义您的第一个测试用例。第一个测试只是简单地检查staking DAO是否正确存储了目标收集人的地址。

请将以下代码段添加至Dao contract函数的末尾。

// You can nest calls to create subsections
describe('Deployment', function () {
  // Mocha's it function is used to define each of your tests.
  // It receives the test name, and a callback function.
  // If the callback function is async, Mocha will await it
  it('should store the correct target collator in the DAO', async function () {

    // Set up our test environment by calling deployDao
    const { deployedDao } = await deployDao();

    // The expect function receives a value and wraps it in an assertion object.
    // This test will pass if the DAO stored the correct target collator
    expect(await deployedDao.targetCollator()).to.equal(targetCollator);
  });

  // The following test cases should be added here
});

现在,添加另一个测试用例。当staking DAO启动时,不会有任何资金。该测试验证确实如此。请将以下测试用例添加到Dao.js文件中:

it('should initially have 0 funds in the DAO', async function () {
  const { deployedDao } = await deployDao();

  // This test will pass if the DAO has no funds as expected before any contributions
  expect(await deployedDao.totalStake()).to.equal(0);
});

函数还原(Revert)

现在,您将使用稍微不同的架构来实现更复杂的测试用例。在上述示例中,您已验证函数返回预期值。在这一部分中,您将验证函数是否会还原。您将更改调用者的地址以测试仅限管理员的功能。

staking DAO合约中,只有管理员有权限添加新成员至DAO。我们可以编写一个测试查看管理员是否有权限添加新成员,但是可能更重要的测试是确保非管理员不能添加新成员。要在不同的账户下运行此测试用例,您需要在调用ethers.getSigners()时请求另一个地址,并在断言中使用connect(member1)指定调用者。最后,在函数调用后,您将附加.to.be.reverted以指示如果函数还原则测试用例成功。反之则代表测试失败。

it('should not allow non-admins to grant membership', async function () {
  const { deployedDao, member1 } = await deployDao();

  // We use connect to call grant_member from member1's account instead of admin.
  // This test will succeed if the function call reverts and fails if the call succeeds
  await expect(
    deployedDao
      .connect(member1)
      .grant_member('0x0000000000000000000000000000000000000000')
  ).to.be.reverted;
});

从其他账户签署交易

在本示例中,您将验证新添加的DAO成员是否可以调用staking DAO的check_free_balance()函数,该函数有访问修饰符,仅限成员访问。

it('should only allow members to access member-only functions', async function () {
  const { deployedDao, member1 } = await deployDao();

  // Add a new member to the DAO
  const transaction = await deployedDao.grant_member(member1.address);
  await transaction.wait();

  // This test will succeed if the DAO member can call the member-only function.
  // We use connect here to call the function from the account of the new member
  expect(await deployedDao.connect(member1).check_free_balance()).to.equal(0);
});

这样就可以了!您现在可以运行测试了!

运行测试

如果您已遵循上述部分,则您的Dao.js测试文件应该都准备好了。

Dao.js
// Import Ethers
const { ethers } = require('hardhat');
// Import Chai to use its assertion functions here
const { expect } = require('chai');

// Indicate the collator the DAO wants to delegate to
// For Moonbase Local Node, use: 0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac
// For Moonbase Alpha, use: 0x12E7BCCA9b1B15f33585b5fc898B967149BDb9a5
const targetCollator = 'INSERT_COLLATOR_ADDRESS';

// The describe function receives the name of a section of your test suite, and a
// callback. The callback must define the tests of that section. This callback
// can't be an async function
describe('Dao contract', function () {
  let wallet1, wallet2;

  before(async function () {
    // Get signers we defined in Hardhat config
    const signers = await ethers.getSigners();
    wallet1 = signers[0];
    wallet2 = signers[1];
  });

  async function deployDao() {
    const delegationDaoFactory = await ethers.getContractFactory(
      'DelegationDAO',
      wallet2
    );

    // Deploy the staking DAO and wait for the deployment transaction to be confirmed
    try {
      const deployedDao = await delegationDaoFactory.deploy(
        targetCollator,
        wallet2.address
      );
      await deployedDao.waitForDeployment(); // Correct way to wait for the transaction to be mined
      return { deployedDao };
    } catch (error) {
      console.error('Failed to deploy contract:', error);
      return null; // Return null to indicate failure
    }
  }

  describe('Deployment', function () {
    // Test case to check that the correct target collator is stored
    it('should store the correct target collator in the DAO', async function () {
      const deployment = await deployDao();
      if (!deployment || !deployment.deployedDao) {
        throw new Error('Deployment failed; DAO contract was not deployed.');
      }
      const { deployedDao } = deployment;
      expect(await deployedDao.getTarget()).to.equal(
        '0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac'
      );
    });

    // Test case to check that the DAO has 0 funds at inception
    it('should initially have 0 funds in the DAO', async function () {
      const { deployedDao } = await deployDao();
      expect(await deployedDao.totalStake()).to.equal(0);
    });

    // Test case to check that non-admins cannot grant membership
    it('should not allow non-admins to grant membership', async function () {
      const { deployedDao } = await deployDao();
      // Connect the non-admin wallet to the deployed contract
      const deployedDaoConnected = deployedDao.connect(wallet1);
      const tx = deployedDaoConnected.grant_member(
        '0x0000000000000000000000000000000000000000'
      );

      // Check that the transaction reverts, not specifying any particular reason
      await expect(tx).to.be.reverted;
    });

    // Test case to check that members can access member only functions
    it('should only allow members to access member-only functions', async function () {
      const { deployedDao } = await deployDao();

      // Connect the wallet1 to the deployed contract and grant membership
      const deployedDaoConnected = deployedDao.connect(wallet2);
      const grantTx = await deployedDaoConnected.grant_member(wallet1.address);
      await grantTx.wait();

      // Check the free balance using the member's credentials
      const checkTx = deployedDaoConnected.check_free_balance();

      // Since check_free_balance() does not modify state, we expect it not to be reverted and check the balance
      await expect(checkTx).to.not.be.reverted;
      expect(await checkTx).to.equal(0);
    });
  });
});

由于我们的测试用例主要包含staking DAO的配置和设置,不涉及实际委托操作,因此我们将在Moonbeam开发节点(本地节点)上运行我们的测试。请注意,Alice(0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac)是本地开发节点上唯一的收集人。您可以使用标志--network moonbase来使用Moonbase Alpha运行测试。在这种情况下,请确保您的部署者地址有足够的DEV token。

挑战

尝试创建一个额外的测试用例,以验证满足minDelegationStk时staking DAO是否成功委托给收集人。您需要在Moonbase Alpha而不是本地开发节点上进行测试。

首先,遵循启动本地开发节点的指南确保您的本地Moonbeam节点正在运行。请采取预防措施,因为您可能会无意中将真实资金发送到Alice和Bob开发账户,这会导致这些资金损失。

您可以使用以下命令运行测试:

npx hardhat test --network dev tests/Dao.js

如果设置无误,您将看到以下输出:

Run your test suite of test cases with Hardhat.

部署至Moonbase Alpha

在以下步骤中,我们将部署DelegationDAO至Moonbase Alpha测试网。在部署至Moonbase Alpha或Moonbeam之前,请确认您所使用的账户不是Alice和Bob账户,这两个账户仅用于本地开发节点。

另外,请注意DelegationDAO依赖于 StakingInterface.sol,这是Moonbeam网络独有的基于Substrate的产品。Hardhat网络和分叉网络是模拟的EVM环境,不包括像StakingInterface.sol的基于Substrate的预编译。因此,如果将DelegationDAO部署到本地默认的Hardhat网络或一个分叉网络,它将无法正常工作。

要部署DelegationDAO.sol,您可以编写一个简单的脚本。您可以为脚本创建一个新的目录,并命名为scripts

mkdir scripts

然后,添加名为deploy.js的新文件:

touch scripts/deploy.js

接下来,您需要编写部署脚本,可通过使用ethers完成此步骤。由于您将使用Hardhat运行,因此无需导入任何库:

接着,执行以下步骤:

  1. 指定DAO要委托的活跃收集人地址。在本示例中,我们将使用PS-1收集人的地址(注意:这与本地开发节点上的Alice收集人的地址不同)
  2. 将部署者地址指定为DAO的管理员。部署者作为DAO管理员这点很重要,用以确保后续测试按预期工作
  3. 使用getContractFactory函数创建一个合约的本地实例
  4. 使用已存在于此实例中的deploy函数来实例化智能合约
  5. 部署后,您可以使用合约实例获取合约地址

一切无误,您的部署脚本应类似于以下内容:

// 1. The PS-1 collator on Moonbase Alpha is chosen as the DAO's target
const targetCollator = '0x12E7BCCA9b1B15f33585b5fc898B967149BDb9a5';

async function main() {
  // 2. Get the address of the deployer to later be set as the admin of the DAO
  const [deployer] = await ethers.getSigners();
  console.log('Deploying contracts with the account:', deployer.address);

  // 3. Get an instance of DelegationDAO
  const delegationDao = await ethers.getContractFactory('DelegationDAO');

  // 4. Deploy the contract specifying two params: the desired collator to
  // delegate to and the address of the deployer (the initial DAO admin)
  const deployedDao = await delegationDao.deploy(
    targetCollator,
    deployer.address
  );
  await deployedDao.waitForDeployment();

  // 5. Print out the address of the deployed staking DAO contract
  console.log('DAO address:', deployedDao.target);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

确保您已在账户中存入Moonbase Alpha DEV Token。现在您可以使用run命令部署DelegationDAO.sol,并指定moonbase作为网络(已在hardhat.config.js文件中配置):

npx hardhat run --network moonbase scripts/deploy.js

几秒钟后,合约会成功部署,您将在终端看到地址。

Deploy a Contract to Moonbase Alpha with Hardhat.

恭喜您,您的合约已上线Moonbase Alpha!请保存地址,这将在后续步骤中用于与此合约实例交互。

验证在Moonbase Alpha上的合约

合约验证是任何开发流程中必不可少的一步,尤其是对于staking DAO的理论示例来说。DAO中的潜在参与者需要确保智能合约按预期工作,并且验证合约以允许任何人可以查看和分析已部署的智能合约。

虽然可以在Moonscan网站上验证智能合约,但是Hardhat Etherscan插件提供更快速且简单的方式以验证staking DAO。毫不夸张地说,该插件极大地简化了合约验证的流程,尤其是对于包含多个Solidity文件或库的项目。

在开始合约验证流程之前,您需要先获取Moonscan API密钥。注意:Moonbeam和Moonbase Alpha使用相同的Moonbeam Moonscan API密钥,而Moonriver需要不同的API密钥。

要验证合约,您可以运行verify命令并传入部署DelegationDao合约的网络、合约地址,以及在deploy.js文件中给出的两个构造函数参数,即目标收集人地址和部署智能合约的部署者地址(来源于hardhat.config.js文件)。

npx hardhat verify --network moonbase INSERT_CONTRACT_ADDRESS 0x12E7BCCA9b1B15f33585b5fc898B967149BDb9a5 INSERT_DEPLOYER_ADDRESS

注意事项

如果您在没有任何更改的情况下逐字部署DelegationDAO.sol,您可能会收到Already Verified的错误提示,因为Moonscan会自动识别并验证具有匹配字节码的智能合约。您的合约仍将显示为已验证,因此您无需执行任何其他操作。但是,如果您想要验证自己的DelegationDAO.sol,您可以对合约稍作修改(例如更改注释)并重复编译、部署和验证步骤。

在您的终端中,您会看到合约源代码已成功提交以用于验证。如果验证成功,您会看到Successfully verified contract并且会有一个指向Moonscan for Moonbase Alpha上的合约代码的链接。如果插件返回错误,请仔细检查您的API密钥配置是否正确,以及您是否已在验证命令中指定所有必要的参数。您可以通过Hardhat Etherscan插件指南获取更多信息。

Verify contracts on Moonbase Alpha using the Hardhat Etherscan plugin.

部署到Moonbeam主网上的生产环境

注意事项

DelegationDAO.sol未经过审核和审计,仅用于演示目的。其中可能会包含漏洞或者逻辑错误而导致资产损失,请勿在生产环境中使用。

在以下步骤中,我们将部署DelegationDAO合约至Moonbeam主网。如果您尚未设置网络,请先将Moonbeam网络添加到您的hardhat.config.js中并使用您在Moonbeam上的帐户私钥更新您。在将DelegationDAO部署到Moonbeam之前,由于我们在Moonbase Alpha上的目标收集人在Moonbeam上并不存在,我们需要更改目标收集人的地址。前往您的部署脚本并将目标收集人更改为0x1C86E56007FCBF759348dcF0479596a9857Ba105或您选择的另一个Moonbeam收集人。因此,您的deploy.js脚本应如下所示:

// 1. The moonbeam-foundation-03 collator on Moonbeam is chosen as the DAO's target
const targetCollator = '0x1C86E56007FCBF759348dcF0479596a9857Ba105';

async function main() {
  // 2. Get the address of the deployer to later be set as the admin of the DAO
  const [deployer] = await ethers.getSigners();
  console.log('Deploying contracts with the account:', deployer.address);

  // 3. Get an instance of DelegationDAO
  const delegationDao = await ethers.getContractFactory('DelegationDAO');

  // 4. Deploy the contract specifying two params: the desired collator to delegate
  // to and the address of the deployer (synonymous with initial DAO admin)
  const deployedDao = await delegationDao.deploy(
    targetCollator,
    deployer.address
  );
  await deployedDao.waitForDeployment();

  console.log('DAO address:', deployedDao.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

现在您可以使用run命令部署DelegationDAO.sol并将moonbeam指定为网络:

npx hardhat run --network moonbeam scripts/deploy.js

如果您使用的是另一个Moonbeam网络,请确保您已指定正确的网络。网络名称需要与hardhat.config.js中定义的保持一致。

几秒钟后,合约会成功部署,您将在终端看到地址。

Deploy a Contract to Moonbeam with Hardhat.

恭喜您,您的合约已上线Moonbeam!请保存地址,其将在后续步骤中用于与合约实例交互。

验证在Moonbeam上的合约

在这一部分,我们将验证刚刚部署至Moonbeam的合约。在开始合约验证流程之前,您需要先获取Moonscan API密钥。注意:Moonbeam和Moonbase Alpha使用相同的Moonbeam Moonscan API密钥,而Moonriver需要不同的API密钥。

要验证合约,您可以运行verify命令并传入部署DelegationDao合约的网络、合约地址,以及在deploy.js文件中给出的两个构造函数参数,即目标收集人地址和部署智能合约的部署者地址(来源于secrets.json文件)。请注意:Moonbeam上的staking DAO的目标收集人与Moonbase Alpha上的staking DAO的目标收集人不同。

npx hardhat verify --network moonbeam INSERT_CONTRACT_ADDRESS 0x1C86E56007FCBF759348dcF0479596a9857Ba105 INSERT_DEPLOYER_ADDRESS

注意事项

如果您在没有任何更改的情况下逐字部署DelegationDAO.sol,您可能会收到Already Verified的错误提示,因为Moonscan会自动识别并验证具有匹配字节码的智能合约。您的合约仍将显示为已验证,因此您无需执行任何其他操作。但是,如果您想要验证自己的DelegationDAO.sol,您可以对合约稍作修改(例如更改注释)并重复编译、部署和验证步骤。

在您的终端中,您会看到合约源代码已成功提交以用于验证。如果验证成功,您会看到Successfully verified contract并且会有一个指向Moonbeam Moonscan上的合约代码的链接。如果插件返回错误,请仔细检查您的API密钥配置是否正确,以及您是否已在验证命令中指定所有必要的参数。您可以通过Hardhat Etherscan插件指南获取更多信息。

Verify contracts on Moonbeam using Hardhat Etherscan plugin.

这样就可以了!我们在本教程中介绍了很多基础知识。如果您想深入了解,可以访问以下链接获取更多信息:

本教程仅用于教育目的。 因此,不应在生产环境中使用本教程中创建的任何合约或代码。

本网站的所有信息由第三方提供,仅供参考之用。Moonbeam文档网站(https://docs.moonbeam.network/)上列出和描述的任何项目与Moonbeam立场无关。Moonbeam Foundation不保证网站信息的准确性、完整性或真实性。如使用或依赖本网站信息,需自行承担相关风险,Moonbeam Foundation不承担任何责任和义务。这些材料的所有陈述和/或意见由提供方个人或实体负责,与Moonbeam Foundation立场无关,概不构成任何投资建议。对于任何特定事项或情况,应寻求专业权威人士的建议。此处的信息可能会包含或链接至第三方提供的信息与/或第三方服务(包括任何第三方网站等)。这类链接网站不受Moonbeam Foundation控制。Moonbeam Foundation对此类链接网站的内容(包括此类链接网站上包含的任何信息或资料)概不负责也不认可。这些链接内容仅为方便访客而提供,Moonbeam Foundation对因您使用此信息或任何第三方网站或服务提供的信息而产生的所有责任概不负责。
Last update: January 25, 2024
| Created: April 14, 2023