前面的几篇文章我们已经创建部署好了 智能合约 并且已经和元数据(metadata )做了关联 这篇文章我们主要是为 智能合约 做一些优化

在 Etherscan 上验证智能合约

智能合约 验证了之后 相当于把 Solidity 源代码直接公布在了 Etherscan 上 这样任何人都可以直接在 Etherscan 上调用智能合约里的方法了 (至于安全问题 文章最后会讲一下)

首先我们需要在 Etherscan 上注册一个账号 并且申请一个 API Key 注册地址 https://etherscan.io/register

注册登录后 在 https://etherscan.io/myapikey 里添加就可以了

添加好 API Key 之后 复制 API Key 添加到 env

ETHERSCAN_API_KEY = "你的 API Key"

然后修改 hardhat.config.js 完整代码如下

/**
* @type import('hardhat/config').HardhatUserConfig
*/

require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` });
require("@nomiclabs/hardhat-ethers");
require("./scripts/deploy.js");
require("./scripts/mint.js");
require("@nomiclabs/hardhat-etherscan");

const { ALCHEMY_KEY, ACCOUNT_PRIVATE_KEY, ETHERSCAN_API_KEY } = process.env;

module.exports = {
  solidity: "0.8.1",
  defaultNetwork: "rinkeby",
  networks: {
    hardhat: {},
    rinkeby: {
      url: `https://eth-rinkeby.alchemyapi.io/v2/${ALCHEMY_KEY}`,
      accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
    },
    ethereum: {
      chainId: 1,
      url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}`,
      accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
    },
  },
  etherscan: {
    apiKey: ETHERSCAN_API_KEY,
  },
}

修改完之后 在 package.json 中的 scripts 内添加两个脚本

"verify:rinkeby": "NODE_ENV=development npx hardhat verify",
"verify:ethereum": "NODE_ENV=production npx hardhat verify"

添加好之后 命令行执行下面的命令 进行验证 智能合约

npm run verify:rinkeby 你的智能合约地址

这里可能会报错 连接超时 或 连接异常 的错误 就算使用科学上网也没用 我的解决办法是直接在一台海外的服务器上进行验证 (国内开发真的好难啊)

打开 Etherscan https://rinkeby.etherscan.io/ 输入 智能合约 地址 就可以看到 Contract 后面会出现一个绿色的勾 代表已经验证成功了

设置NFT供应数量限制

一般来讲 我们发布 NFT 项目 都会设置一个最大数量 不然就可以无限铸造(mint)这样就会出现一些问题 比如元数据(metadata )数量不够了 就会无法显示图片等信息

我们只需要对现有的 智能合约mintTo 函数做一个小小的改造即可 下面是 contracts/NFTContract.sol 的完整代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFTContract is ERC721 {
  using Counters for Counters.Counter;

  // Constants
  uint256 public constant TOTAL_SUPPLY = 10_000;

  Counters.Counter private currentTokenId;

  /// @dev Base token URI used as a prefix by tokenURI().
  string public baseTokenURI;

  constructor() ERC721("NFTContract", "NFT") {
    baseTokenURI = "";
  }

  function mintTo(address recipient) public returns (uint256) {
    uint256 tokenId = currentTokenId.current();
    require(tokenId < TOTAL_SUPPLY, "Max supply reached");

    currentTokenId.increment();
    uint256 newItemId = currentTokenId.current();
    _safeMint(recipient, newItemId);
    return newItemId;
  }

  /// @dev Returns an URI for a given token ID
  function _baseURI() internal view virtual override returns (string memory) {
    return baseTokenURI;
  }

  /// @dev Sets the base token URI prefix.
  function setBaseTokenURI(string memory _baseTokenURI) public {
    baseTokenURI = _baseTokenURI;
  }
}

这里限制了最大数量为 10000requirefalse 的时候 下面的代码就不会执行 也不会收取 Gas fee (矿工费)

设置铸造(mint)NFT的价格

当你需要在 智能合约 中收取铸造(mint)费用时 只需要在现有的 智能合约mintTo 函数做一个小小的改造即可 下面是 contracts/NFTContract.sol 的完整代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFTContract is ERC721 {
  using Counters for Counters.Counter;

  // Constants
  uint256 public constant TOTAL_SUPPLY = 10_000;
  uint256 public constant MINT_PRICE = 0.01 ether;

  Counters.Counter private currentTokenId;

  /// @dev Base token URI used as a prefix by tokenURI().
  string public baseTokenURI;

  constructor() ERC721("NFTContract", "NFT") {
    baseTokenURI = "";
  }

  function mintTo(address recipient) public payable returns (uint256) {
    uint256 tokenId = currentTokenId.current();
    require(tokenId < TOTAL_SUPPLY, "Max supply reached");
    require(msg.value == MINT_PRICE, "Transaction value did not equal the mint price");

    currentTokenId.increment();
    uint256 newItemId = currentTokenId.current();
    _safeMint(recipient, newItemId);
    return newItemId;
  }

  /// @dev Returns an URI for a given token ID
  function _baseURI() internal view virtual override returns (string memory) {
    return baseTokenURI;
  }

  /// @dev Sets the base token URI prefix.
  function setBaseTokenURI(string memory _baseTokenURI) public {
    baseTokenURI = _baseTokenURI;
  }
}

这里声明了一个价格常量 并且在 mintTo 函数的 public 后面添加了一个 payable 修饰符

关于 ETH 单位换算 可以在这个地址查看 https://etherscan.io/unitconverter

添加完之后部署好 智能合约 并进行验证后 你会发现命令行执行下面的命令会无法铸造(mint)

npm run mint:xxx --address 你的钱包地址

原因就是因为我们添加了需要收取铸造(mint)费用 这里我们并没有传如具体给多少费用 默认是 0 所以会验证失败

想要使用命令也能铸造(mint)成功 只需要修改下 scripts 文件夹中的 mint.js 中名为 minttask 代码如下

task("mint", "Mints from the NFT contract").addParam("address", "The address to receive a token").setAction(async function (taskArguments, hre) {
  const contract = await getContract("NFTContract", hre);
  const mintPrice = await contract.MINT_PRICE();
  const transactionResponse = await contract.mintTo(taskArguments.address, {
    value: mintPrice,
    gasLimit: 500_000,
  });
  console.log(`Transaction Hash: ${transactionResponse.hash}`);
});

既然我们的 智能合约 已经验证了 那么就可以直接在 Etherscan 上直接使用我们的 智能合约

打开 Etherscan https://rinkeby.etherscan.io/ 输入 智能合约 地址 点击 Contract 然后 点击 Write Contract 点击 Connect to Web3 链接你的钱包后找到 mintTo 点开 输入铸造(mint)费用和你的钱包地址 点击 Write 就可以铸造(mint)了(MetaMask中需要手动点下确认)

Tips

  1. 智能合约一旦部署将无法修改 当然也有可以修改的办法 需要花费大量的 Gas fee(矿工费) 还有一种叫热替换合约的技术 这个我没有研究过
  2. 前面已经说了 智能合约一旦部署将无法修改 所以我们这里支付了铸造费用(mint)这个费用是支付到合约中的 并不是你的个人账户 需要提取出来才可以 所以在部署前 先写好提现函数

将智能合约中的ETH提取出来

上面的 Tips 说了在部署 智能合约 之前 要先写好 提现函数 其实实现 提现函数 很简单 OpenZeppelin 已经提供了很多种解决方案 我们只需要继承一个 PullPayment.sol 就可以了 下面是 contracts/NFTContract.sol 的完整代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/PullPayment.sol";

contract NFTContract is ERC721, PullPayment {
  using Counters for Counters.Counter;

  // Constants
  uint256 public constant TOTAL_SUPPLY = 10_000;
  uint256 public constant MINT_PRICE = 0.01 ether;

  Counters.Counter private currentTokenId;

  /// @dev Base token URI used as a prefix by tokenURI().
  string public baseTokenURI;

  constructor() ERC721("NFTContract", "NFT") {
    baseTokenURI = "";
  }

  function mintTo(address recipient) public payable returns (uint256) {
    uint256 tokenId = currentTokenId.current();
    require(tokenId < TOTAL_SUPPLY, "Max supply reached");
    require(msg.value == MINT_PRICE, "Transaction value did not equal the mint price");

    currentTokenId.increment();
    uint256 newItemId = currentTokenId.current();
    _safeMint(recipient, newItemId);
    return newItemId;
  }

  /// @dev Returns an URI for a given token ID
  function _baseURI() internal view virtual override returns (string memory) {
    return baseTokenURI;
  }

  /// @dev Sets the base token URI prefix.
  function setBaseTokenURI(string memory _baseTokenURI) public {
    baseTokenURI = _baseTokenURI;
  }
}

这样写就已经提供了一个 提现函数 但是这个 提现函数 提取的是一个类似 信用额度 的资金 账户中默认都是 0 所以你不管怎么执行 都提取不到资金的 所以我们还需要将 合约中的资金 转移到我们指定账户的 信用额度 中 下面是 contracts/NFTContract.sol 的完整代码

这里拓展一个问题 在编写 提现函数 的时候应该要避免一下 重入攻击(Reentrency Problem)
什么是 重入攻击(Reentrency Problem)可以参考这个文档 https://medium.com/coinmonks/protect-your-solidity-smart-contracts-from-reentrancy-attacks-9972c3af7c21
英文看不懂也没关系 可以看这个视频 https://www.bilibili.com/video/BV1dU4y1U78z

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/PullPayment.sol";

contract NFTContract is ERC721, Ownable, PullPayment {
  using Counters for Counters.Counter;

  // Constants
  uint256 public constant TOTAL_SUPPLY = 10_000;
  uint256 public constant MINT_PRICE = 0.01 ether;

  Counters.Counter private currentTokenId;

  /// @dev Base token URI used as a prefix by tokenURI().
  string public baseTokenURI;

  constructor() ERC721("NFTContract", "NFT") {
    baseTokenURI = "";
  }

  function mintTo(address recipient) public payable returns (uint256) {
    uint256 tokenId = currentTokenId.current();
    require(tokenId < TOTAL_SUPPLY, "Max supply reached");
    require(msg.value == MINT_PRICE, "Transaction value did not equal the mint price");

    currentTokenId.increment();
    uint256 newItemId = currentTokenId.current();
    _safeMint(recipient, newItemId);
    _asyncTransfer(owner(), msg.value);
    return newItemId;
  }

  /// @dev Returns an URI for a given token ID
  function _baseURI() internal view virtual override returns (string memory) {
    return baseTokenURI;
  }

  /// @dev Sets the base token URI prefix.
  function setBaseTokenURI(string memory _baseTokenURI) public {
    baseTokenURI = _baseTokenURI;
  }
}

上面我们又继承了一个 Ownable.sol 用于获取 当前 智能合约 的所有者 并且在每次铸造(mint)的时候 将铸造(mint)费用直接使用 _asyncTransfer 函数转移到 当前 智能合约 的所有者 信用额度

有能力的 也可以将资金转移的抽成一个函数

这样部署好 智能合约 并验证 之后 就可以 打开 Etherscan https://rinkeby.etherscan.io/ 输入 智能合约 地址 点击 Contract 然后 点击 Write Contract 里点击 Connect to Web3 链接你的钱包后找到 withdrawPayments 输入收款地址(一般是自己的钱包地址 也就是 当前 智能合约 的所有者地址)就可以将里面的 ETH 发送到这个地址上了(MetaMask中需要手动点下确认)

想要直接在命令行进行提现 也很简单 在 scripts 文件夹中新建 withdraw.js 文件 添加以下内容

const { task } = require("hardhat/config");
const { getAccount, getContract } = require("./helpers");

task("withdraw", "Transfer the ETH hold by the SC to the account").setAction(async function (taskArguments, hre) {
  const account = getAccount();
  const contract = await getContract("NFTContract", hre);
  const transactionResponse = await contract.withdrawPayments(account.address, {
    gasLimit: 500_000,
  });
  console.log(`Transaction Hash: ${transactionResponse.hash}`);
});

这里创建了一个 Hardhat 的 task 用来提现

然后将 scripts 文件夹中的 withdraw.js 引入到 hardhat.config.js

require("./scripts/withdraw.js");

将这个命令添加到 package.json 中的 scripts 内的脚本里

"withdraw:rinkeby": "NODE_ENV=development npx hardhat withdraw",
"withdraw:ethereum": "NODE_ENV=production npx hardhat withdraw"

命令行执行类似下面的命令 就可以进行提现了

npm run withdraw:xxx

添加函数的角色访问权限

到这里你应该会发现 智能合约 中所有的函数 无论是谁 都可以随意执行(当然有的是需要支付一点 Gas fee(矿工费)的) 非常的不安全

添加角色访问权限也非常简单 前面已经继承了 Ownable.sol 可以获取到 当前 智能合约 的所有者 我们拿 提现函数 举例 只需要重写 withdrawPayments 加一个 onlyOwner 的修饰符 就可以了 下面是 contracts/NFTContract.sol 的完整代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/PullPayment.sol";

contract NFTContract is ERC721, Ownable, PullPayment {
  using Counters for Counters.Counter;

  // Constants
  uint256 public constant TOTAL_SUPPLY = 10_000;
  uint256 public constant MINT_PRICE = 0.01 ether;

  Counters.Counter private currentTokenId;

  /// @dev Base token URI used as a prefix by tokenURI().
  string public baseTokenURI;

  constructor() ERC721("NFTContract", "NFT") {
    baseTokenURI = "";
  }

  function mintTo(address recipient) public payable returns (uint256) {
    uint256 tokenId = currentTokenId.current();
    require(tokenId < TOTAL_SUPPLY, "Max supply reached");
    require(msg.value == MINT_PRICE, "Transaction value did not equal the mint price");

    currentTokenId.increment();
    uint256 newItemId = currentTokenId.current();
    _safeMint(recipient, newItemId);
    _asyncTransfer(owner(), msg.value);
    return newItemId;
  }

  /// @dev Returns an URI for a given token ID
  function _baseURI() internal view virtual override returns (string memory) {
    return baseTokenURI;
  }

  /// @dev Sets the base token URI prefix.
  function setBaseTokenURI(string memory _baseTokenURI) public {
    baseTokenURI = _baseTokenURI;
  }

  /// @dev Overridden in order to make it an onlyOwner function
  function withdrawPayments(address payable payee) public override onlyOwner virtual {
    super.withdrawPayments(payee);
  }
}

这样修改完之后 除了 当前 智能合约 的所有者可以 执行 withdrawPayments 函数 其他人都会执行失败

setBaseTokenURI 函数 也可以按照同样的方式修改 这里我就不演示了

到这里我们的 智能合约 功能基本已经完成了 应该已经算是入门了 大家也可以参考别人已经发布的 智能合约 的代码 找一些灵感 后面有机会再做一些深入的扩展吧

本篇文章代码 Github 地址 https://github.com/stephenml/nft-contract/tree/day-04

最后修改日期: 2022年7月10日

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。