前面的几篇文章我们已经创建部署好了 智能合约
并且已经和元数据(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;
}
}
这里限制了最大数量为 10000
当 require
为 false
的时候 下面的代码就不会执行 也不会收取 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
中名为 mint
的 task
代码如下
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
- 智能合约一旦部署将无法修改 当然也有可以修改的办法 需要花费大量的 Gas fee(矿工费) 还有一种叫热替换合约的技术 这个我没有研究过
- 前面已经说了 智能合约一旦部署将无法修改 所以我们这里支付了铸造费用(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
留言