ERC20으로 토큰 배포 및 transfer

이무헌·2023년 10월 10일
0

blcokchain

목록 보기
8/10
post-thumbnail

1.배포할 sol파일 작성(ERC20 형식)

1. ERC20형식의 interface(IERC20)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// 오픈 재플린은 솔리디 기반의 스마트 컨트랙트를 개발할 때 사용하는 표준 프레임 워크
// 컨트랙트를 개발할 때 표준토큰 규약을 지키면서 안정성있고 빠르게 개발할 수 있다.
// 오픈 재플린에서 제공하는 ERC20 인터페이스
interface IERC20 {
    // 토큰의 총 발행량을 조회할 수 있는 함수
    // external과 public의 다른 점은 외부에서 접근이 가능한 접근 제한자라는 것이다.
    // external은 외부의 EOA또는 CA에서 호출이 가능(즉, 외부에서 호출이 가능)
    // 내부에서는 접근 불가
    // public은 내부,외부 접근 다 가능

    // 그럼 왜 쓰냐? => public보다 가스비가 더 저렴
    // public은 함수의 변수를 메모리에 복사하고 사용을 하는데
    // external 복사를 하지 않아요 내부에서 복사를 해서 메모리에 값을 사용하느냐 CALLDATA를 직접 읽어서 사용하느냐의 차이
    function totalSupply() external view returns (uint);

    // 전달받은 매개변수(계정 주소) 토큰의 잔액을 조회하는 함수
    function balanceOf(address account) external view returns (uint);

    // 계정에서  토큰을 다른 계정으로 전달하는 함수
    function transfer(address to, uint amount) external returns (bool);

    // 소유자의 위임받은 토큰을 관리하는 공간
    function allowance(address owner, address spender) external returns (uint);

    // {0x1111111111.... : 100,0x1111112223123....: 200} 
    //  => {0x1111111111.... : 100,{0x1111112223123....: 50}} 

    // 소유권을 제 3자에게 위임하는 함수
    function approve(address spender, uint amount) external returns (bool);

    // 소유권을 관리하는 토큰을 보내는 함수, 위임받은 소유권이 있는지 확인을 하고 보내는 함수
    function transferFrom(address spender,address to,uint amount) external returns (bool);

    // event 함수를 작성해서 함수에서 실행을 시키면 이더스캔 같은 사이트에서 로그 확인을 할 수 있다.
    // 트랜잭션 내용에 로그를 만들어서 확인할 수 있다.
    // Transfer 함수를 실행했을 때 이벤트 함수를 실행시켜 발생하는 이벤트를 트랜잭션 로그에 확인 해볼수 있다.
    event Transfer (address from, address to, uint value);

    // approve함수가 실행 했을 때 이벤트 함수도 실행을 해서 로그를 확인해 볼 수 있다.
    event Approval(address owner, address spender, uint value);
}
  • 기본적으로 ERC20의 형식을 가지고 있는 인터페이스이다. 여기서 말하는 ERC20은 openzepplin을 말한다.

2. IERC20을 implements 받은 ERC20

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "./IERC20.sol";

// class에서 인터페이스 상속을 한 것 처럼
// implements 처럼

// is라는 구문을 사용하여 상속
contract ERC20 is IERC20 {
    // ERC20 토큰의 규약

    // 토큰의 이름 풀네임
    string public name;
    // 토큰의 심볼(토큰의 단위를 표현) ETH등
    string public symbol;

    // 토큰의 소수점 자리 기본 소수점 자리는 10단위
    uint8 public decimals = 18;

    // 토큰의 총 발행량
    // 이미 선언이 되어있는 함수를 interface 함수는 virtual 함수로 돼 있는데
    // 기본 선언하면 virtual 함수임
    // override 상속 받은 함수를 새로 정의해서 사용 다형성
    uint public override totalSupply;

    // 컨트랙트 배포자, 현재 버전엔 상속받아서 사용중 필요가 없기 때문에 없앤것
    address private owner;

    // 생성자 함수 컨트랙트 배포자가 실행을 할 함수
    // memory영역에서 사용을 하고 해제 시킨다는 구문
    // uint는 256 정해져있는 양을 사용함.
    // 동적으로 변할 수 있는 변수에는 memory를 다 붙인다.
    constructor(string memory _name, string memory _symbol, uint256 _amount) {
        // 어떤 이름의 토큰을 발행하고 있고
        // 어떤 단위 심볼을 사용할지
        //  처음에 총발행량을 얼마나 설정할지
        owner = msg.sender;
        name = _name;
        symbol = _symbol;
        // 토큰발행할 때 사용할 메서드 작성
        // _amount * (10 ** uint256(decimals))

        // 최초 토큰 발행량
        mint(_amount * (10 ** uint256(decimals)));
    }

    // balance 토큰을 누가 얼마만큼 가지고 있는지의 내용을 담을 객체
    mapping(address => uint) public balance;

    // 토큰의 권한을 위임 받은 내용이 들어있는 객체
    mapping(address => mapping(address => uint)) public override allowance;

    // 잔액을 조회하는 함수
    function balanceOf(address account) external view override returns (uint) {
        return balance[account];
    }

    // 다른 지갑으로 잔액을 전달하는 함수
    function transfer(
        address to,
        uint amount
    ) external override returns (bool) {
        // 전달하는 사람의 잔액이 감소해야하고
        balance[msg.sender] -= amount;
        // 전달받은 사람의 잔액이 증가
        balance[to] += amount;
        // transfer 이벤트를 실행시킨 로그를 트랜잭션에서 확인

        // emit 구문을 사용해서 이벤트 함수 실행
        emit Transfer(msg.sender, to, amount);

        // 성공은 true,실패는 false
        return true;
    }

    // 토큰의 소유권을 위임하는 함수
    function approve(
        address spender,
        uint amount
    ) external override returns (bool) {
        // 소유권을 추가
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);

        return true;
    }

    // 권한을 가지고 있는 제 3자가 토큰을 보낼 때 사용하는 함수
    function transferFrom(
        address spender,
        address to,
        uint amount
    ) external override returns (bool) {
        // 이 사람이 권한을 가지고 있는지 토큰의 양을 검사를 하고
        require(allowance[spender][msg.sender] >= amount);
        allowance[spender][msg.sender] -= amount;
        balance[spender] -= amount;
        balance[to] += amount;
        return true;
    }

    // 토큰을 발행하는 함수
    // internal 컨트랙트 내부에서만 실행시킬 함수
    function mint(uint amount) internal {
        balance[msg.sender] += amount;
        totalSupply += amount;
    }

    // 토큰 소각 시키는 함수
    // 토큰을 너무 많이 발행하면 가치가 떨어지기 때문에 소각을 시킨다.
    function burn(uint amount) external {
        balance[msg.sender] -= amount;
        totalSupply -= amount;
    }
}
  • override는 말 그대로 부모 (IERC20)의 메서드를 오버라이드 했다는 뜻이다.
  • balance는 사용자의 계좌잔액을 말하며, 여기서는 토큰의 양을 말한다.
  • decimal은 단위를 정해준다. 기본적으로 1eth= 10^18 wei이므로 내가 만든 토큰은 이더리움 단위와 일치한다.

    중요한 점은 이 토큰은 이더리움(코인)이 아니란 것이다. 위 주석에서 설명됐듯이 코인은 메인넷을 가지고 있지만 토큰은 그렇지 않다.
    때문에 토큰을 배포한다면 그 계좌는 이더리움,토큰 두개의 화폐를 가지고 있으며 이더리움을 가지고 오는 메서는 기본 메서드인 eth.getBalance를 사용하며 토큰은 우리가 만든 balanceOf를 사용하게 된다.

2. remix로 배포

1.remix기본 세팅

npm install -g @remix-project/remixd
remixd -s . --remix-ide https://remix.ethereum.org
# '.' 은 파일 경로를 의미한다. 여기서는 현재 경로에 있는 모든 폴더를 연동한다는 뜻이다.

리믹스에 대한 의존 파일을 설치하고 서버를 연다.

  • 위 이미지에서 localhost를 누르면 vsc와 로컬 연결을 한다. 그 후 원하는 contract파일을 선택후 컴파일을 진행하다
  • 왼쪽 3번째 v표시를 누르면 다음과 같은 화면이 나온다.

  • compiler는 자신이 설정한(pragma)파일의 버전으로 맞추고 파란색 컴파일을 실행시키면 컴파일이 완료되며 배포할 준비를 마쳤다.

  1. environment는 메타마스크로 변경하자 우린 테스트넷(ganache)를 쓸것이다.
  2. account에 현재 ganache에 있는 계좌가 나오는지 확인하자 나오면 잘 진행된 것이다.
  3. contract에는 컴파일한 파일의 이름이 있다.
  4. deploy에 본인이 배포한 생성자 함수의 매개변수를 입력하고 transact 버튼을 누르면 배포 성공이다.

  • 위와 같이 배포한 스마트 컨트랙트의 메서드를 실행시킬 수 있고 CA또한 확인이 가능하다. 이 CA를 이용해 메타마스크 토큰 가져오기로 토큰도 가져오자

3. react로 배포한 smart contract 실행

1.web3.hook.js

import React, { useEffect, useState } from "react";
import Web3 from "web3";
function useWeb3() {
  const [user, setUser] = useState({
    account: "",
    balance: "",
  });

  const [web3, setWeb3] = useState(null);

  useEffect(() => {
    if (window.ethereum) {
      window.ethereum
        .request({ method: "eth_requestAccounts" })
        .then(async ([data]) => {
          const webProvider = new Web3(window.ethereum);
          setWeb3(webProvider);

          setUser({
            account: data,
            balance: webProvider.utils.toWei(
              await webProvider.eth.getBalance(data),
              "ether"
            ),
          });

          //   웹 메타마스크 지갑 다 뜰거고
          //  그 지갑에 있는 토큰 양을 다 보여줄거고
          // 컨트랙트를 배포한 네트워크가 맞는지 아니면 네트워크 변경할수 있게 함수 실행
          // 지갑을 바꾸면 바꾼 지갑내용으로 브라우저에 보일 수 있게
        });
    } else {
      alert("메타 마스크 설치해주세요");
    }
  }, []);

  return { user, web3 };
}

export default useWeb3;

2. App.js

import React, { useEffect, useState } from "react";
import useWeb3 from "./hooks/web3.hook";
import abi from "./abi/ERC20.json";
import Web3 from "web3";
function App() {
  const { user, web3 } = useWeb3();
  const [ERC20Contract, setERC20Contract] = useState(null);
  const [network, setNetwork] = useState(null);
  const [accounts, setAccounts] = useState([]);
  const [token, setToken] = useState("0");
  const [value, setValue] = useState("");
  const [value2, setValue2] = useState("");
  useEffect(() => {
    if (web3 !== null) {
      if (ERC20Contract) return;
      const ERC20 = new web3.eth.Contract(
        abi,
        "0xAC7da4aF2E5C3A26B049B265BFe89d0B601b8a34",
        { data: "" }
      );
      console.log(ERC20);
      setERC20Contract(ERC20);
    }
  }, [web3]);
  useEffect(() => {
    // 이벤트 등록 네트워크가 변경되면 발생하는 이벤트 등록
    window.ethereum.on("chainChanged", (chainId) => {
      console.log("네트워크 변경", chainId);
      console.log(network);
      if (chainId === "0x539") {
        getAccounts();
      }
    });

    // 지갑이 변경되면 실행할 이벤트 등록
    window.ethereum.on("accountsChanged", (account) => {
      console.log("지갑 변경됨", account);
      getAccounts();
    });
    if (!ERC20Contract) {
      return;
    }

    // 컨트랙트 인스턴스가 있으면 실행시키지 않고
    // 네트워크가 정상적일 때 실행시켜도 되겠다.
  }, [network]);
  const switchNet = async () => {
    // 해당 네트워크가 맞는지 요청
    // 메타마스크로 요청
    // wallet_switchEthereumChain == chainId가 맞는지 확인 매개변수로 전달한 chainId가 맞는지
    // 0x539우리가 설정한 chainId 1337
    const net = await window.ethereum.request({
      jsonrpc: "2.0",
      method: "wallet_switchEthereumChain",
      params: [{ chainId: "0x539" }],
    });
    console.log(net);
    // net값이 null이 반환되면 해당 네트워크에 있다는 뜻.
    setNetwork(net || true);
  };

  // 전달받은 매개변수 (지갑주소) 의 잔액을 보여주는 함수
  const getToken = async (account) => {
    if (!ERC20Contract) {
      console.log(web3);
      return;
    }
    let result = web3.utils
      .toBigInt(await ERC20Contract.methods.balanceOf(account).call())
      .toString(10);
    result = await web3.utils.fromWei(result, "ether");
    return result;
  };

  // 해당 계정의 이더리움을 가져오는 함수
  const getETH = async (account) => {
    if (!ERC20Contract) {
      return;
    }
    let ETHResulut = await web3.eth.getBalance(account);
    console.log(ETHResulut, "가져온 이더리움 계좌 잔액");
    ETHResulut = web3.utils.fromWei(ETHResulut, "ether");
    console.log(ETHResulut, "가져온 이더리움 계좌 이더변환");
    return ETHResulut;
  };

  // 메타마스크의 모든 지갑을 보여줄 함수
  const getAccounts = async () => {
    const accounts = await window.ethereum.request({
      method: "eth_requestAccounts",
    });
    // 배열을 돌릴 때 map에서 일어나는 promise반환값을 다 처리하고 넘어가기
    // promise.all  요청이 다 끝나면 진행

    const accountsCom = await Promise.all(
      accounts.map(async (account) => {
        const token = await getToken(account);
        const eth = await getETH(account);
        return { account, token, eth };
      })
    );
    // console.log(await getToken(accounts[0]),'getAccounts');
    setToken(await getToken(accounts[0]));

    setAccounts(accountsCom);
  };

  // 지갑에서 다른 지갑으로 토큰 전송할 함수
  const transfer = async () => {
    await ERC20Contract.methods
      .transfer(
        value.replaceAll(" ", ""),
        await web3.utils.toWei(value2, "ether")
      )
      .send({
        from: user.account,
      });
    getAccounts();
  };
  // 0xAC7da4aF2E5C3A26B049B265BFe89d0B601b8a34

  // 0xD1b0b915Ee4c470C02B445dE2cfEA715848bfa06
  if (user.account === null) {
    return "지갑 연결하셈";
  }
  return (
    <div>
      <button onClick={switchNet}>토큰 컨트랙트 연결</button>
      <div> 지갑 주소:{user.account}</div>
      <h2> 토큰 보유량:{token}</h2>
      {accounts.map((item, index) => {
        return (
          <div key={index}>
            계정:{item.account},
            <br />
            토큰량:{item.token}
            <br />
            이더리움 양:{item.eth}
          </div>
        );
      })}
      <div>
        <label>누구한테 보낼거임? </label>
        <input
          onChange={(e) => {
            setValue(e.target.value);
          }}
        ></input>
        <label>토큰수 </label>
        <input
          onChange={(e) => {
            setValue2(e.target.value);
          }}
        ></input>
      </div>
      <button onClick={transfer}>보내기</button>
      계정들의 이더리움 잔액 보여주는 함수 만들어서 보여주자
    </div>
  );
}

export default App;
  • 이더리움에 계정 있는 토큰과 이더리움을 조회 할 수 있다.
  • switchNet에 있는 net메서드는 현재 네트워크가 1337(ganache)테스트 네트워크일 경우 null을 반환한다.
  • accountsCom으로 모든 계정의 이더리움 잔액,토큰 수를 모은다.
    -transfer 앞서 말했듯이 내가 만든 토큰은 이더리움 단위와 일치하므로 html에 입력한 input값 그대로 토큰이 전달된다.

3. 결과

  • 보내기전 현재 계좌(2개만 등록함)의 토큰과 이더리움 양이 나온다.
  • 5 토큰을 보내자
  • 성공적으로 토큰 transfer가 완료됐다.

4.느낀점

리믹스를 써서 확실히 편했지만 자유도가 떨어지는 느낌이다. 코어 개발을 할 만큼 아직 실력이 있진않지만, 실무에서는 코어개발이 가능할 정도로 공식문서, 라이브러리 형식을 꼼꼼히 익혀 토큰,nft발행과 같은 스마트 컨트랙트 로직을 독자적으로 설계하고 싶다.

profile
개발당시에 직면한 이슈를 정리하는 곳

0개의 댓글