Привет, Хабр.
В прошлых статьях мы научились генерировать 10 000 изображений для нашей NFT коллекции с помощью Golang, а также загрузили все сгенерированные изображения в децентрализованное хранилище IPFS.
В этой статье мне хотелось бы поделиться знаниями и опытом, а также о подводных камнях, с которыми мне пришлось столкнуться при разработке смарт-контрактов для NFT коллекций на блокчейне Ethereum.
Мы создадим типовой смарт-контракт для нашей NFT коллекции, протестируем и загрузим созданный смарт-контракт в тестовую сеть Ethereum. Но прежде, чем мы приступим к кодингу, мне хотелось бы остановиться на ERC-721 стандарте, данный стандарт описывает спецификацию NFT токенов.
Давайте подробней рассмотрим, какие методы должны быть у нашего смарт-контракта:
function balanceOf(address _owner) external view returns (uint256); function ownerOf(uint256 _tokenId) external view returns (address); function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; function transferFrom(address _from, address _to, uint256 _tokenId) external payable; function approve(address _approved, uint256 _tokenId) external payable; function setApprovalForAll(address _operator, bool _approved) external; function getApproved(uint256 _tokenId) external view returns (address); function isApprovedForAll(address _owner, address _operator) external view returns (bool);
Полную спецификацию по ERC-721 стандарту можно посмотреть тут.
Нам не обязательно реализовывать весь стандарт самим, более оптимальным подходом является переиспользовать готовые библиотеки: OpenZeppelin - это библиотека для разработки безопасных смарт-контрактов и именно с ней мы будем работать.
Плюсы такого решения очевидны:
Готовый код из под коробки, прошедший аудит безопасности
Аудит нашего смарт-контракта займет гораздо меньше денег и времени
Создание смарт-контракта
Давайте создадим типовой смарт-контракт для нашей NFT коллекции, назовем её MonkeyNFT. Наш смарт-контракт наследует стандартные OpenZeppelin библиотеки, а именно:
ERC721 / ERC721Enumerable - контрактные модули, которые обеспечивают базовые функциии для нашего NFT токена
Ownable - контрактный модуль, который обеспечивает базовый механизм контроля доступа
Разрабатывать смарт-контракт, мы будем с помощью такого инструмента, как hardhat, очень крутой инструмент для разработки, тестирования и деплоя, особенно для тех, кто устал от Truffle и его бесконечного количества багов.
Запускаем команду npx hardhat init, для создания нового шаблона для нашего смарт-контракта, далее переходим в директорию contracts и пишем код:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/Context.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; contract MonkeyNFT is ERC721, ERC721Enumerable, Ownable { using SafeMath for uint256; uint public constant maxPurchase = 10; uint256 public constant MAX_MONKEYS = 10000; uint256 private _monkeyPrice = 80000000000000000; //0.08 ETH string private baseURI; bool public saleIsActive = true; constructor() ERC721("The Monkey NFT", "MNK") { } function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) { super._beforeTokenTransfer(from, to, tokenId); } function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) { return super.supportsInterface(interfaceId); } function withdraw() public onlyOwner { uint256 balance = address(this).balance; payable(msg.sender).transfer(balance); } function setPrice(uint256 _newPrice) public onlyOwner() { _monkeyPrice = _newPrice; } function getPrice() public view returns (uint256){ return _monkeyPrice; } function mintMonkeys(uint numberOfTokens) public payable { require(saleIsActive, "Sale must be active to mint Monkeys"); require(numberOfTokens <= maxPurchase, "Can only mint 10 tokens at a time"); require(totalSupply().add(numberOfTokens) <= MAX_MONKEYS, "Purchase would exceed max supply of Monkeys"); require(_monkeyPrice.mul(numberOfTokens) <= msg.value, "Ether value sent is not correct"); for(uint i = 0; i < numberOfTokens; i++) { uint mintIndex = totalSupply(); if (totalSupply() < MAX_MONKEYS) { _safeMint(msg.sender, mintIndex); } } } function _baseURI() internal view override returns (string memory) { return baseURI; } function setBaseURI(string memory newBaseURI) public onlyOwner { baseURI = newBaseURI; } function flipSaleState() public onlyOwner { saleIsActive = !saleIsActive; } }
Из интересного:
Флаг
saleIsActive- говорит, о том, что наша коллекция либо готова к продаже, либо нет. Данная функция очень полезна на начальном этапе, когда по каким то причинам необходимо остановить продажи.Переменная
maxPurchaseхранит кол-во токенов, который пользователь может купить за один раз. Некоторая защита от ботов, чтобы не выкупили всю коллекцию сразу.Функция
mintMonkey- это основная функция, через которую пользователи могут купить наш NFT токен. Модификаторpayable, как раз говорит о том, что вызов данной функции для пользователя платный.
Для того, чтобы скомпилировать наш смарт-контракт, запускаем команду npx hardhat compile, после чего создадутся abi-артифакты в директории artifacts.
Тестирование смарт-контракта
Т.к. загруженный смарт-контракт нельзя модифицировать, нам обязательно необходимо его протестировать до деплоя в блокчейн Ethereum. Переходим в директорию test, удаляем дефолтные тесты и пишем свои:
const { expect } = require("chai"); describe("Token contract", () => { let contract; let owner; let addr1; let addr2; let addrs; let baseURI; beforeEach(async () => { const Token = await ethers.getContractFactory("MonkeyNFT"); [owner, addr1, addr2, ...addrs] = await ethers.getSigners(); contract = await Token.deploy(); baseURI = "https://hardhat.org/test/" await contract.setBaseURI(baseURI) }); it("Should initialize contract", async () => { expect(await contract.MAX_MONKEYS()).to.equal(10000); }); it("Should set the right owner", async () => { expect(await contract.owner()).to.equal(await owner.address); }); it("Should mint", async () => { const price = await contract.getPrice(); const tokenId = await contract.totalSupply(); expect( await contract.mintMonkeys(1, { value: price, }) ).to.emit(contract, "Transfer").withArgs(ethers.constants.AddressZero, owner.address, tokenId); expect(await contract.tokenURI(tokenId)).to.equal(baseURI+"0"); }); });
Запускаем тестирование смарт-контракта, командой npx hardhat test. Если все тесты пройдены, то мы увидем:
Compiling 1 file with 0.8.4 Compilation finished successfully Token contract ✓ Should initialize contract ✓ Should set the right owner ✓ Should mint (41ms) 3 passing (1s)
Только, что мы успешно протестировали:
Создание смарт-контракта
Успешно установили владельца смарт-контракта
Успешно вызвали платную функцию
mintMonkeysдля продажи NFT токена
Деплой смарт-контракта
Деплоить мы будем в тестовую сеть Ethereum. Переходим в директорию scripts и удаляем дефолтные скрипты, они нам больше не понадобяться и пишем новый скрипт деплоя:
async function main() { const [deployer] = await ethers.getSigners(); console.log("Deploying contracts with the account:", deployer.address); console.log("Account balance:", (await deployer.getBalance()).toString()); const Token = await ethers.getContractFactory("MonkeyNFT"); console.log("Deploying contract..."); const token = await Token.deploy(); await token.deployed(); console.log("Token address:", token.address); } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); });
Так же нам необходимо изменить файл hardhat.config следующим образом:
require("@nomiclabs/hardhat-waffle"); require("@nomiclabs/hardhat-etherscan"); module.exports = { solidity: "0.8.4", networks: { rinkeby: { url: `https://eth-rinkeby.alchemyapi.io/v2/${YOUR_API_KEY}`, }, }, };
Отлично все готово для деплоя, запускаем скрипт: npx hardhat run scripts/deploy.js --network rinkeby
В следующей статье я расскажу как взаимодействовать с нашим созданным смарт-контрактом с помощью web3.js.
