Привет, Хабр.
В прошлых статьях мы научились генерировать 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.
korsetlr473
1) почему ERC721Enumerable , а не
ERC721URIStorage ?
2) почему вы используете какието свои контукции для метаданных , а не
IERC721Metadata?
3) как можно установить картинку для контакта? я видел на etherscan когда заходишь у некоторых картинка отображается
4) как можно сжешь токен?
5) от вопроса 4. как то можно соединить два nft в один? или это просто сжечь две штуки и сминтить новый?
6) вы указали цену 80000000000000000 , они перемещаются на кошелек кто создал смартконтакт?
7) для чего нужны supportsInterface и beforetransfer ?
monomoto Автор
1) Если у NFT коллекции baseURL общий, то нет необходимости хранить URL для каждого токена отдельно, т.к. его можно получить по формуле: baseURL/${TOKEN_ID}. В нашем случае, базовый URL был общий, поэтому это был самый оптимальный вариант.
2) Нам было очень важно торговаться на бирже opensea, у них есть документация, в каком формате должны быть аттрибуты.
3) Нужно выполнить верификацию на ehterscan, например с помощью hardhat достаточно запустить скрипт npx hardhat verify --network mainnet YOU_CONTRACT_ID
4) Нужно реализовать OpenZeppelin ERC721Burnable
5) Да, именно так и делают большинство NFT проектов
6) Сумма перемещается на адрес смарт-контракта, перевести на свой кошелек может только владелец смарт-контракта. Или например можно создать метод в смарт-контракте, который будет переводить равные суммы всем основателям.