[멋쟁이 사자처럼 블록체인 스쿨 3기] 23-05-26

임형석·2023년 5월 26일
0

Solidity


ERC-20 토큰 발행

어제 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

ERC-721은 NFT의 표준안이다. NFT는 대체불가토큰(Non Fungible Token)의 약자로 대체 불가능한 토큰이라는 의미이다. ERC-721로 발행되는 토큰은 대체 불가능하여 모두 제 각각의 가치(Value)를 갖고 있다.

openzepplin ERC-721

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);
   }

0개의 댓글