어제 TIL 에 적은 ERC-20 토큰을 발행해보았다.
먼저, openzepplin 라이브러리를 가져와서 사용한다.
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
그리고 컨스트럭터 설정으로, 컨트랙트가 배포될때 토큰이 mint 되도록 설정했다.
나머지는 balanceOf 를 실행하는 함수, mint 하는 함수를 하나씩 적었고,
decimal 을 설정했다. decimal 이란?
토큰의 민팅 단위를 나타내는데, decimal 을 0으로 정의하고 컨트랙트를 배포할때
10000 을 적고 컨트랙트를 배포한다고 하면 그대로 10000개가 민팅된다.
만약 decimal 을 2로 정의하면 100개, decimal 을 5로 정의하면 0.1개
10 ** -decimals() 만큼 단위를 설정한다고 볼 수 있다.
oppenzeppline ERC20.sol 파일에서는 이렇게 설명하고있다.
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
contract hsToken is ERC20("HS","HS") {
receive() external payable{}
constructor(uint _totalSupply) {
_mint(msg.sender, _totalSupply);
}
function getBalance() public view returns(uint) {
return balanceOf(msg.sender);
}
function mintToken(uint _amount) public {
_mint(address(this), _amount);
}
function decimals() public pure override returns (uint8) {
return 0;
}
}
메타마스크 조작
ERC-20 의 코드를 살짝 수정하여 메타마스크에 보여지는 코인의 양을 바꿀 수 있다.
먼저, 이렇게 새로운 토큰을 발행하고 isFake 라는 boolean 상태변수를 설정했다.
isFake 라는 boolean 값을 변경시켜 발행한 토큰이 메타마스크에서 보여지는 수량을
임의로 바꿔보았다.
contract fakeToken is ERC20("Fake","FA"){
constructor(){
owner = msg.sender;
}
address public owner;
mapping(address=>uint) private _balances;
bool isFake;
function FakeTrue() public {
isFake = true;
}
function _mint(address account, uint amount) internal override {
_balances[account] += amount;
}
function MINT(uint amount) public {
_mint(msg.sender, amount);
}
function balanceOf(address account) public view override returns(uint){
if(isFake == false){
return _balances[account];
} else {
return 0; // _transfer(account, 내 주소, _balances[account])
}
}
}
이 컨트랙트를 테스트넷에 배포한 후 토큰을 민팅하고 보유량을 확인해보았다.
정상적으로 나오는 모습이다.
메타마스크도 정상이다.
여기서 FakeTrue 함수를 실행시키고 다시 balance 를 확인해보았다.
값이 바뀌어있다.
메타마스크도 마찬가지이다.
만약 이런식으로 토큰의 owner 가 직접 컨트롤하게 된다면, balance 뿐만 아니라 다른 코드를 집어넣어 내 지갑주소로 전송을 시킨다던지.. 등의 악의적인 행동을 취할 수도 있다.
물론, 가스비를 소모하기에 솔리디티를 조금 이해한다면 당하지 않겠지만 일반인들의 입장에서는 모르고 당할 수도 있다고 생각한다..
ERC-721은 NFT의 표준안이다. NFT는 대체불가토큰(Non Fungible Token)의 약자로 대체 불가능한 토큰이라는 의미이다. ERC-721로 발행되는 토큰은 대체 불가능하여 모두 제 각각의 가치(Value)를 갖고 있다.
approve 는 ERC-20 의 권한 시스템과 같다.
approve
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721.ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
"ERC721: approve caller is not token owner or approved for all"
);
_approve(to, tokenId);
}
이 함수는 input 값 두개를 받는다. (address to, uint256 tokenId).
owner , 권한을 위임받은 사람. 둘만이 토큰을 제어할 수 있다.
두번째 require 의 isApprovedForAll 이라는 함수는..
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _operatorApprovals[owner][operator];
}
, _operatorApproals 라는 맵핑값을 돌려주고..
mapping(address => mapping(address => bool)) private _operatorApprovals;
이 값이 true 라면, 내가 어떠한 토큰을 제어할 권한이 있다는 것이다.
그래서 msg.sender 가 토큰의 주인인지 or msg.sender 가 owner 로 부터 받은 토큰에 대한 권한이 있는지? 확인하고..
require(_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
마지막으로 _approve 함수를 실행시켜서 해당 토큰의 권한을 부여한다.
_approve(to, tokenId)
setApprovalForAll
이 함수는, 특정 주소에게 권한을 주거나 뺏는 함수이다.
function setApprovalForAll(address operator, bool approved) public virtual override {
_setApprovalForAll(_msgSender(), operator, approved);
}
input 값이 bool 형태이기에, true, false 로 권한을 주거나 뺏을 수 있는 것이다.
transferFrom / safeTransferFrom
토큰을 다른 주소로 전송해주는 함수이다.
function transferFrom(address from, address to, uint256 tokenId) public virtual override {
//solhint-disable-next-line max-line-length
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_transfer(from, to, tokenId);
isApprovedOrOwner 함수로 msg.sender 가 토큰에 대한 권한을 가지고 있는지 확인한 후, _transfer 함수로 전송을 한다.
비슷한 함수로 safeTransferFrom 함수가 있다.
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override {
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_safeTransfer(from, to, tokenId, data);
}
이 함수는, _safeTransfer 함수를 호출하는데. 이부분을 보면,
function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}
_transfer 함수로 토큰을 보내주고, input 으로 받은 data 값을
_checkOnERC721Received 함수로 확인한다. 이 함수는 ERC-721 토큰을
받을 수 있는 주소인지 아닌지에 대한 확인을 하는 것이다.
만약 받을 수 없는 주소라면, 모든 transfer 를 되돌리고 initial state 상태로 돌아가게 된다. (가스비도 소모되지 않음)
이렇게 확인이 끝나면 _transfer 함수로 토큰을 특정 주소로 보내게 된다.
그리고, _transfer 함수의 이 부분에서..
delete _tokenApprovals[tokenId];
_owners[tokenId] = to;
기존 owner 들이 가진 해당 토큰의 권한을 delete 하고,
이 토큰을 받는 사람이 새로운 owner 로써 권한을 받게된다.
mint
mint 함수는, 토큰을 발행하는 함수이다.
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId, 1);
require(!_exists(tokenId), "ERC721: token already minted");
unchecked {
_balances[to] += 1;
}
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId, 1);
}
이 부분을 보면, 토큰id 가 겹치게 발행하는 것이 불가능한 것을 볼 수 있다.
require(!_exists(tokenId), "ERC721: token already minted");
ERC-721 은 NFT. 즉, 대체불가능 토큰이기에 토큰마다 ID 값이 부여가 되고, ID 는 겹칠 수 없다.
그리고 토큰이 발행됨과 동시에 오너가 가진 토큰이 1증가하고, 해당 토큰을 제어할 수 있는 권한을 갖게된다.
_balances[to] += 1;
_owners[tokenId] = to;
burn
burn 함수는, 발행되어있는 토큰을 태우는 함수이다.(없애는 것)
function _burn(uint256 tokenId) internal virtual {
address owner = ERC721.ownerOf(tokenId);
_beforeTokenTransfer(owner, address(0), tokenId, 1);
owner = ERC721.ownerOf(tokenId);
delete _tokenApprovals[tokenId];
unchecked {
_balances[owner] -= 1;
}
delete _owners[tokenId];
emit Transfer(owner, address(0), tokenId);
_afterTokenTransfer(owner, address(0), tokenId, 1);
}