SWF 해커톤에서 기부증서로서 NFT를 민팅해주는 기능을 구현하기 위해 썼던 코드들이다.
사실 증서로서는 양도불가능이라는 특성을 가진 SBT가 적합하나, web3 입문자로서 당시에는 SBT가 생소하기도 했고 ERC721 관련 자료가 많았기 때문에 ERC721 표준을 바탕으로 구현했다.
그러나 기부증서와 같은 역할이므로 이 NFT를 다른 사람에게 transfer하면 안된다.
이 문제의 해결책으로 단순히 transfer 코드를 제외하기로 했다....!!
그래서 ERC721 표준을 완전히 따른 것이라고는 말할 수 없을 듯하다.
나중에 알아보니 SBT는 ERC-4671나 ERC-4973 표준을 통해 구현한다고 한다.
해커톤 당시 이 코드를 완전히 이해하고 작성한 것은 아니다..
글을 작성하면서 코드 파일을 지금부터 하나씩 분석해보려 한다.
!!! 해커톤 당일 작성했던 코드들에 대한 회고 목적에서 작성된 글이며, 잘못된 부분이 있을 수 있습니다(분명 있습니다..) 공부하는 입장에서, 작성했던 코드를 더 이해해보고 코드를 개선해보고자 하는 방향에서 작성하였습니다.
\CONTRACTS
│ ERC165.sol
│ ERC721.sol
│ ERC721Connector.sol
│ ERC721Enumerable.sol
│ ERC721Metadata.sol
│ Meoww.sol
│ Migrations.sol
│
└─interfaces
IERC165.sol
IERC721.sol
IERC721Enumerable.sol
IERC721Metadata.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
ERC165의 인터페이스다. ERC165에는 해당 컨트랙트의 인터페이스 지원 여부를 확인하는 supportsInterface라는 함수가 필요하다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import './interfaces/IERC165.sol';
contract ERC165 is IERC165 {
mapping(bytes4 => bool) private _supportedInterfaces;
constructor() {
_registerInterface(bytes4(keccak256('supportsInterface(bytes4)')));
}
function supportsInterface(bytes4 interfaceID) external view override returns (bool) {
return _supportedInterfaces[interfaceID];
}
function _registerInterface(bytes4 interfaceId) internal {
require(interfaceId != 0xffffffff, 'Invalid interface request');
_supportedInterfaces[interfaceId] = true;
}
}
EIP(https://eips.ethereum.org/EIPS/eip-721)에 들어가보면 아래와 같이 써있다.
Every ERC-721 compliant contract must implement the ERC721 and ERC165 interfaces.
그 이유를 ChatGPT한테 물어봤다.
ERC165 is a standard interface detection mechanism that allows smart contracts to declare which interfaces they support. By inheriting ERC165, the ERC721 contract is essentially stating that it supports the ERC721 standard and implements the functions specified by the IERC721 interface.
ERC165는 스마트컨트랙트가 어느 인터페이스를 지원하는지 알 수 있게 해준다.
ex. ERC165를 상속받는 컨트랙트에서 supportsInterface(0x80ac58cd)을 호출하면, 해당 컨트랙트가 IERC721 를 지원하고 있음을 알아낼 수 있다. (여기서 0x80ac58cd는 ERC721의 인터페이스 ID)
ERC165는 IERC165의 supportsInterface 함수를 오버라이딩 한다.
keccack은 다목적 암호 함수로, 인풋에 대해 Keccack-256 해시를 수행해서 unique ID를 만들어준다.
keccak256('supportsInterface(bytes4)')를 통해 supportsInterface(bytes4)를 해싱한 뒤 bytes4 데이터 타입으로 변환한다. 결과값(ERC165의 인터페이스 ID)은 0x01ffc9a7이다.
-> 즉 어떤 컨트랙트에서 supportsInterface('0x01ffc9a7')를 호출했을 때 true 값을 반환하면 IERC165를 따르고 있는 것이다.
그런데 보통 컨트랙트에서 해시값을 직접 계산하면 가스비가 많이 나와서
아래처럼 인터페이스ID를 프로퍼티로 미리 선언한다고 한다.
bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;
constructor () public {
// ERC165를 통한 ERC721의 확인을 위한 지원 인터페이스 등록
_registerInterface(_INTERFACE_ID_ERC721);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
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;
// 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);
}
강의를 보면서 작성한 코드인데 초급자를 위한 강의였던지라 주석처리한 함수가 많다.
해당 강의에서는 transfer 기능도 구현했기 때문에 transferFrom은 원래 주석처리 되어있지 않았다.
내가 원하는 컨트랙트는 transfer가 불가능한 컨트랙트이기 때문에
Transfer event와 Approval event도 주석처리 했어야하는게 맞지 않나 싶다.
그런데 아래 ERC721.sol 코드를 보면 민팅 시에 Transfer event를 emit해주고 있기 때문에 Transfer는 주석처리 안하는게 맞다.
이게 ERC721 표준을 바탕으로 한 것이기 때문에 Transfer event를 emit 했지만,
Mint라는 event를 생성해서 emit하는게 더 알맞을 듯하다.
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import './interfaces/IERC721.sol';
import './ERC165.sol';
/*
a. nft to point to an address
b. keep track of the token ids
c. keep track of the owner of the token ids
-> NFT가 누구의 소유인지 알 수 있음
d. keep track of how many tokens an owner address has
e. create an event that emits a transfer log
- contract address, where it is being minted to, the id
*/
contract ERC721 is ERC165, IERC721 {
// event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
// mapping in solidity creates a hash table of key pair values
// Mapping from token id to the owner
// 토큰 ID를 소유자에게 매핑하고자 하는 경우
mapping(uint256 => address) private _tokenOwner;
// Mapping from owner to number of owned token
mapping(address => uint256) private _OwnedTokensCount;
constructor() {
_registerInterface(bytes4(keccak256('balanceOf(bytes4)')^
keccak256('ownerOf(bytes4)')));
}
/// @notice Count all NFTs assigned to an owner
/// @dev NFTs assigned to the zero address are considered invalid, and this
/// function throws for queries about the zero address.
/// @param _owner An address for whom to query the balance
/// @return The number of NFTs owned by `_owner`, possibly zero
// 토큰의 소유자가 가지고 있는 토큰의 개수를 반환하는 함수
function balanceOf(address _owner) public override view returns(uint256) {
require(_owner != address(0), "ERC721: balance query for the zero address");
return _OwnedTokensCount[_owner];
}
/// @notice Find the owner of an NFT
/// @dev NFTs assigned to zero address are considered invalid, and queries
/// about them do throw.
/// @param _tokenId The identifier for an NFT
/// @return The address of the owner of the NFT
// 토큰의 소유자를 반환하는 함수
function ownerOf(uint256 _tokenId) external override view returns (address) {
address owner = _tokenOwner[_tokenId];
require(owner != address(0), "ERC721: owner query for nonexistent token");
return owner;
}
function _exists(uint256 tokenId) internal view returns (bool) {
// setting the address of nft owner to check the mapping
// of the address from tokenOwner at the tokenId
address owner = _tokenOwner[tokenId];
// return truthiness the address is not the zero
return owner != address(0);
}
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: minting to the zero address"); // 주소가 0이 아니라는걸 증명
require(!_exists(tokenId), "ERC721: token already minted"); // 토큰이 존재하지 않는다는걸 증명
_tokenOwner[tokenId] = to; // 해당 토큰아이디를 to에게 매핑
_OwnedTokensCount[to] += 1; // to의 토큰개수 세기
emit Transfer(address(0), to, tokenId);
}
}
// enumerable -> 한 집합 내 모든 항목이 완전히 순서가 매겨진 것
/*
* bytes4(keccak256('balanceOf(address)')) == 0x70a08231
* bytes4(keccak256('ownerOf(uint256)')) == 0x6352211e
* bytes4(keccak256('approve(address,uint256)')) == 0x095ea7b3
* bytes4(keccak256('getApproved(uint256)')) == 0x081812fc
* bytes4(keccak256('setApprovalForAll(address,bool)')) == 0xa22cb465
* bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c
* bytes4(keccak256('transferFrom(address,address,uint256)')) == 0x23b872dd
* bytes4(keccak256('safeTransferFrom(address,address,uint256)')) == 0x42842e0e
* bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) == 0xb88d4fde
*
* => 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^
* 0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde == 0x80ac58cd
*/
bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;
내가 작성한 ERC721.sol의 생성자함수를 보면 인터페이스ID를 keccak256('balanceOf(bytes4)')과// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC721Enumerable {
function totalSupply() external view returns (uint256);
function tokenByIndex(uint256 _index) external view returns (uint256);
function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}
토큰의 총 공급량 반환, 인덱스를 통한 토큰 반환, 오너의 토큰목록에서 인덱스에 해당하는 토큰 반환으로 이루어진 인터페이스다.
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import './ERC721.sol';
import './interfaces/IERC721Enumerable.sol';
contract ERC721Enumerable is ERC721, IERC721Enumerable {
uint256[] private _allTokens;
// mapping from tokenId to position in the allTokens array
mapping(uint256 => uint256) private _allTokensIndex;
// mapping of owner to list of all owner token ids
mapping(address => uint256[]) private _ownedTokens;
// mapping from token ID index of the owner token ids
mapping(uint256 => uint256) private _ownedTokensIndex;
constructor() {
_registerInterface(bytes4(keccak256('totalSupply(bytes4)')^
keccak256('tokenByIndex(bytes4)')));
}
function _mint(address to, uint256 tokenId) internal override(ERC721) {
super._mint(to, tokenId);
_addTokenAllTokenEnumeration(tokenId);
_addTokenToOwnerEnumeration(to, tokenId);
} // 민트된 토큰은 이뉴머레이션에 추가
function _addTokenAllTokenEnumeration(uint256 tokenId) private {
_allTokensIndex[tokenId] = _allTokens.length;
_allTokens.push(tokenId);
} // 토큰을 전체목록에 추가
function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
_ownedTokensIndex[tokenId] = _ownedTokens[to].length;
_ownedTokens[to].push(tokenId);
} // 토큰을 오너목록에 추가
function tokenByIndex(uint256 index) public view override returns (uint256) {
require(index < totalSupply(), "ERC721Enumerable: global index out of bounds");
return _allTokens[index];
} // 인덱스에 해당하는 토큰 반환(토큰 검색)
function tokenOfOwnerByIndex(address owner, uint256 index) public view override returns (uint256) {
require(index < balanceOf(owner), "ERC721Enumerable: owner index out of bounds");
return _ownedTokens[owner][index];
} // 오너의 토큰목록에서 인덱스에 해당하는 토큰 반환(토큰 검색)
function totalSupply() public view override returns (uint256) {
return _allTokens.length;
} // 토탈서플라이 길이 반환
}
ERC721Enumerable은 ERC-721 토큰 컨트랙트에서 NFT를 열거하고 관리하기 위한 기능을 제공한다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC721Metadata {
function name() external view returns (string memory _name);
function symbol() external view returns (string memory _symbol);
// function tokenURI(uint256 _tokenId) external view returns (string memory);
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import './interfaces/IERC721Metadata.sol';
import './ERC165.sol';
contract ERC721Metadata is IERC721Metadata, ERC165 {
string private _name;
string private _symbol;
constructor(string memory named, string memory symbolified) {
_registerInterface(bytes4(keccak256('name(bytes4)')^
keccak256('symbol(bytes4)')));
_name = named;
_symbol = symbolified;
}
function name() external view override returns (string memory) {
return _name;
}
function symbol() external view override returns (string memory) {
return _symbol;
}
}
/*
* bytes4(keccak256('name()')) == 0x06fdde03
* bytes4(keccak256('symbol()')) == 0x95d89b41
* bytes4(keccak256('tokenURI(uint256)')) == 0xc87b56dd
*
* => 0x06fdde03 ^ 0x95d89b41 ^ 0xc87b56dd == 0x5b5e139f
*/
내가 작성한 코드는 표준에 따르지 않는 거라고 보면 되겠다...// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "./ERC721Metadata.sol";
import "./ERC721Enumerable.sol";
contract ERC721Connector is ERC721Metadata, ERC721Enumerable {
// we want to carry the metadata info over
constructor (string memory named, string memory symbolified) ERC721Metadata(named, symbolified) {
}
}
ERC721Connector는 ERC721Metadata와 ERC721Enumaerable을 상속받아 ERC165, ERC721, ERCMetadata, ERC721Enumerable의 프로퍼티와 메소드를 모두 갖게 된다.
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import './ERC721Connector.sol';
contract Meoww is ERC721Connector {
// 배열 -> 민팅작업으로 생긴 NFT가 저장됨
string[] public meowwz;
mapping(string => bool) _meowwzExists;
// ERC721 -> tokenId 민트
// Meoww -> 사진 민트
function mint(address to, string memory _meoww) public {
require(!_meowwzExists[_meoww], 'Error - meoww already exists');
// this is deprecated - uint _id = meowwz.push(_meoww);
meowwz.push(_meoww);
uint _id = meowwz.length - 1;
// .push no longer returns the length but a ref to the added element
_mint(to, _id);
_meowwzExists[_meoww] = true;
}
// initialize this contract to inherit
// name and symbol from ERC721Metadata so that
// the name is Meoww and the symbol is MEW
constructor() ERC721Connector("Meoww", "MEW") {
}
}
msg.sender
개념이 잘 안잡혀 있어서, mint
함수에 address to
매개변수를 추가하고 _mint
에 msg.sender 대신 to를 집어넣었었다. mint
에 address to
매개변수를 빼고 _mint
에 to
대신 msg.sender
를 넣는 것이 더 알맞을 듯 하다.// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
contract Migrations {
address public owner;
uint256 public last_completed_migration;
constructor() {
owner = msg.sender; // 생성자의 주소를 owner에 저장
}
modifier restricted() {
require(msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}
function setCompleted(uint256 completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address); // 상속받은 Migrations의 인스턴스를 생성
upgraded.setCompleted(last_completed_migration);
}
}
Migrations.sol
은 주로 Truffle 프레임워크와 함께 사용되는 스마트 컨트랙트으로, 스마트 컨트랙트 배포 및 업그레이드 관리를 위한 용도로 만들어진 것이다. 이 스마트 컨트랙트은 일반적으로 다음과 같은 목적을 가지고 있다.
스마트 컨트랙트 업그레이드: 스마트 컨트랙트를 업그레이드하여 관리하기 위한 목적으로 사용된다. upgrade
함수를 통해 새로운 컨트랙트 주소로 업그레이드할 수 있다. 스마트 컨트랙트의 버전 업그레이드나 수정을 허용하며, 이전 데이터와 상태를 유지하면서 새로운 스마트 컨트랙트를 사용할 수 있게 한다.
스마트 컨트랙트 배포 및 소유권 관리: 컨트랙트를 배포할 때 owner
변수에 계약 생성자의 주소를 저장하여, 스마트 컨트랙트의 소유자를 추적하고 특정 함수를 소유자에게만 제한적으로 사용할 수 있게 한다. restricted
제한자를 사용하여 특정 함수가 스마트 컨트랙트 소유자에 의해서만 호출될 수 있도록 한다.
스마트 컨트랙트 버전 관리: last_completed_migration
변수를 사용하여 현재 스마트 컨트랙트의 버전을 추적한다. 이것은 업그레이드된 스마트 컨트랙트의 이전 버전을 추적하고 관리하는 데 사용된다.
Remix IDE에서 컨트랙트를 deploy 할 때마다 새로운 컨트랙트 Address가 생긴다.
Migration.sol은 truffle에서 위와 같은 것을 지원하는 컨트랙트인 것 같다.
// useMeowwzContract.js
import { useState, useEffect } from "react";
import Web3 from "web3";
import Meoww from "../truffle_abis/Meoww.json"; // Replace this with the actual contract JSON file.
const useMeowwzContract = (imageList, idx) => {
const [account, setAccount] = useState("");
const [contract, setContract] = useState(null);
const [totalSupply, setTotalSupply] = useState(0);
const [meowwz, setMeowwz] = useState([]);
const [isMinting, setIsMinting] = useState(false);
useEffect(() => {
const loadWeb3 = async () => {
if (window.ethereum) {
window.web3 = new Web3(window.ethereum);
await window.ethereum.enable();
console.log("Ethereum wallet is connected");
} else if (window.web3) {
window.web3 = new Web3(window.web3.currentProvider);
console.log("Legacy web3 browser detected");
} else {
window.alert(
"No ethereum browser detected! You can check out MetaMask!"
);
}
};
const loadBlockchainData = async () => {
const web3 = window.web3;
if (web3) {
try {
const accounts = await web3.eth.getAccounts();
setAccount(accounts[0]);
console.log(accounts[0]);
const networkId = await web3.eth.net.getId();
const networkData = Meoww.networks[networkId];
console.log(networkData);
if (networkData) {
const abi = Meoww.abi;
const address = networkData.address;
const contract = new web3.eth.Contract(abi, address);
setContract(contract);
console.log(contract);
const totalSupply = await contract.methods.totalSupply().call();
setTotalSupply(totalSupply);
const meowwzArray = [];
for (let i = 0; i < totalSupply; i++) {
const Meoww = await contract.methods.meowwz(i).call();
console.log(Meoww);
meowwzArray.push(Meoww);
}
setMeowwz(meowwzArray);
}
} catch (error) {
console.error("Error loading blockchain data:", error);
}
} else {
console.log(
"Web3 object is undefined. Make sure the provider is properly initialized."
);
}
};
loadWeb3();
loadBlockchainData();
}, []);
const mint = async () => {
try {
await contract.methods
.mint(account, imageList[idx])
.send({ from: account });
setMeowwz((prevState) => [...prevState, imageList[idx]]);
setIsMinting(true);
} catch (error) {
console.error("Error minting NFT:", error);
}
};
return {
account,
contract,
totalSupply,
meowwz,
mint,
isMinting,
};
};
export default useMeowwzContract;
사실 hooks로 만들지 않아도 됐는데 이 로직을 넣을 페이지에 코드가 너무 많아서 hooks로 따로 뺐다.
window.ethereum을 통해 web3 인스턴스를 생성한다.
당시에 왜인지 web3 undefined 에러가 떠서 여러 if 문으로 나누었었다.
그런데 이 방식 말고 const web3Instance = new Web3(window.ethereum)
로 해도 정상적으로 인스턴스가 생성된다.
imageList의 특정 인덱스에 있는 이미지를 account에 민팅한다.
const { assert } = require("chai");
const Meoww = artifacts.require("./Meoww");
// check for chai
require("chai").use(require("chai-as-promised")).should();
contract("Meoww", (accounts) => {
let contract;
// before tells our tests to run this first before anything else
before(async () => {
contract = await Meoww.deployed();
});
// testing container - describe
describe("deployment", () => {
// test samples with writing it
it("deploys successfuly", async () => {
const address = contract.address;
assert.notEqual(address, "");
assert.notEqual(address, null);
assert.notEqual(address, undefined);
assert.notEqual(address, 0x0);
});
it("has a name", async () => {
const name = await contract.name();
assert.equal(name, "Meoww");
});
it("has a symbol", async () => {
const symbol = await contract.symbol();
assert.equal(symbol, "MEW");
});
});
describe("minting", () => {
it("creates a new token", async () => {
const result = await contract.mint(
"0x86948078a2bC9A367DE4c1E24E9E8573f09cF20b",
"https...1"
);
const totalSupply = await contract.totalSupply();
//Success
assert.equal(totalSupply, 1);
const event = result.logs[0].args;
assert.equal(
event._from,
"0x0000000000000000000000000000000000000000",
"from the contract"
);
assert.equal(event._to, accounts[0], "to is msg.sender");
//Failure
await contract.mint(
"0x86948078a2bC9A367DE4c1E24E9E8573f09cF20b",
"https...1"
).should.be.rejected;
});
});
describe("indexing", () => {
it("lists Meowwz", async () => {
// Mint three new tokens
await contract.mint(
"0x86948078a2bC9A367DE4c1E24E9E8573f09cF20b",
"https...2"
);
await contract.mint(
"0x86948078a2bC9A367DE4c1E24E9E8573f09cF20b",
"https...3"
);
await contract.mint(
"0x86948078a2bC9A367DE4c1E24E9E8573f09cF20b",
"https...4"
);
const totalSupply = await contract.totalSupply();
// Loop through list and grab Meowwz from list
let result = [];
let Meowwz;
for (let i = 1; i <= totalSupply; i++) {
Meowwz = await contract.meowwz(i - 1);
result.push(Meowwz);
}
});
});
});
Mocha와 Chai를 이용한 테스트코드.
should는 의무조건, assert는 평가