ERC20으로 구현한 포켓몬 트레이드 시스템

이무헌·2023년 10월 11일
1

blcokchain

목록 보기
9/10
post-thumbnail

1. ERC20 의 기본이 되는 메서드를 정의한 인터페이스 IERC20

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

interface IERC20 {
    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);

    function approve(address spender, uint amount) external returns (bool);

    function transferFrom(
        address spender,
        address to,
        uint amount
    ) external returns (bool);

}

2.interface를 상속받아 구체화한 ERC20.sol

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

import "./IPERC20.sol";
// 0x37d4fD7Bfc602e59FE9f96D6a0649D13c95294ad
contract PERC20 is IERC20 {
    // 토큰의 이름
    string public name;
    // 토큰의 단위
    string public symbol;

    // 토큰의 소수점 자리 18 자리로 구성
    uint8 public decimals = 18;

    // 토큰의 현재 총 발행량
    uint public override totalSupply;

    address private owner;

    // CA 주소로 이더가 전송이 되었을 때 실행시키고 싶은 동작이 있어
    // 익명함수
    // receive (익명함수) 특별한 함수
    // CA에 이더를 받으면 자동으로 실해되는 메서드
    // 이더를 CA에 전송 받았을 때 동작을 추가 할 수 있다.

    receive() external payable {
        // 이더를 CA가 받았을 때 실행되는 동자
        // 배포자가 토큰의 발행량을 관리하고
        // 다른 이용자들이 토큰을 가지고 싶으면
        // 컨트랙트 배포자가 정한 비율에 따라 토큰을 가져갈 수 있게

        // 소유권을 줄 토큰의 양
        // 받은 이더 비율로 1이더*200개의 토큰
        uint amount = msg.value * 200;

        require(balance[owner] >= amount);
        balance[owner] -= amount;
        balance[msg.sender] += amount;
        // 만약에 토큰을 다 소유권을 넘겨서 배포자가 들오있는 토큰이 없다.
        // 만약 배포가 이더를 보냈다면 토큰을 발행 할 수 있게

        if (msg.sender == owner) {
            mint(amount);
        }
    }

    // 컨트랙트 생성자
    constructor(string memory _name, string memory _symbol, uint256 _amount) {
        owner = msg.sender;
        name = _name;
        _symbol = symbol;
        mint(_amount * (10 ** uint256(decimals)));
    }

    mapping(address => uint) public balance;

    mapping(address => mapping(address => uint)) public override allowance;

    function mint(uint amount) internal {
        balance[msg.sender] += amount;
        totalSupply += amount;
    }

    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;
        return true;
    }

    function approve(
        address spender,
        uint amount
    ) external override returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transferFrom(
        address sender,
        address to,
        uint amount
    ) external override returns (bool) {
        require(allowance[sender][msg.sender] >= amount);
        allowance[sender][msg.sender] -= amount;
        balance[sender] -= amount;
        balance[to] += amount;
        return true;
    }

    function burn(uint amount) external {
        balance[msg.sender] -= amount;
        totalSupply -= amount;
    }
}
  • 여기서 override를 한 변수가 interface에선 function(함수)로 정의돼 있는 걸 볼 수있다. 즉 ,함수->객체,uint 로 오버라이딩 된건데 구체화하는 형식이라 solidity에서는 가능하다.

receive() external payable

  • 같은 경우에는 해당 컨트랙트에 누가 이더리움을 송금했을 때 자동으로 발생하는 함수로서 여기에선 자동으로 토큰으로 변환해주는 로직을 적었다. 보통 sendTransaction 으로 이더리움을 전송한다.(당연한 말이지만 payable 속성이 있어야 한다.)

3.포켓몬 트레이드 시스템이 있는 Poketmon.sol


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "./PERC20.sol";

contract Poketmon is PERC20 {
    constructor() PERC20("Poketmon", "PKT", 10000) {}

    // 포켓몬 객체를 만들 것
    // 이 객체 하나가 포켓몬의 데이터

    struct Pokets {
        string url;
        string name;
    }

    // 포켓몬 빵 구매한 사람들의 주소를 담아놓을 것
    struct Users {
        address account;
    }

    // ERC20 토큰을 지불해서 포켓몬 빵을 하나 사는것
    // 빵하나에 얼마?

    // 단위 하나를 이더로 지정 10**18 소수점 단위
    // 가격이 1000토큰
    uint256 private tokenPrice = 1000 ether;

    // 우리가 포켓몬 빵을 사면 랜덤란 스티커가 들어있는데
    // 배열에 나올 수있는 포켓몬의 이름을 선언 해두자
    // 한글을 사용하려면 유니코드 작성해야함... 영어로 쓰자

    string[] poketmonName = ["pikachu", "kobuk", "Charmander"];

    // 포켓몬 이쁜 이미지를 담아놓을 배열
    string[] poketmonUrl = [
        "https://archivenew.vop.co.kr/images/4869584989ba805ed7e364eefc3a4664/2011-02/11053927_Untitled-16.jpg",
        "https://media.bunjang.co.kr/product/208341180_1_1691548729_w180.jpg",
        "https://i1.ruliweb.com/img/19/11/08/16e49e9f1ff509386.jpg"
    ];
    // 구매하면 한개를 얻는데
    // 또 사면 두개
    mapping(address => Pokets[]) public poketmons;

    // 한 번이라도 포켓몬 빵을 구매한 사람들의 주소를 가지고 있는 Users 객체

    Users[] public users;

    // 지갑 주소가 가지고있는 포켓몬 조회
    function getPoketmon() public view returns (Pokets[] memory) {
        return poketmons[msg.sender];
    }

    function getPoketmonUsers() public view returns (Users[] memory) {
        return users;
    }

    // 포켓몬 거래 함수
    function tradePoketmon(
        address to,
        string memory tradePoketmonName
    ) public returns (bool) {
    
        Pokets[] memory tempPoketArr = poketmons[msg.sender];
        string memory tradePoketmon;
        for (uint256 index = 0; index < tempPoketArr.length; index++) {
            if (
                keccak256(abi.encodePacked(tempPoketArr[index].name)) ==
                keccak256(abi.encodePacked(tradePoketmonName))
            ) {
                string memory name = poketmons[msg.sender][index].name;
                string memory url = poketmons[msg.sender][index].url;

                poketmons[to].push(Pokets(url, name));
                if (index < poketmons[msg.sender].length - 1) {
                    poketmons[msg.sender][index] = poketmons[msg.sender][
                        tempPoketArr.length - 1
                    ];
                }
                poketmons[msg.sender].pop();
                // 로직
            }
        }
        return true;
    }

    // function getUsersWhoHaveDrawn() public view returns(Users[] memory){
    //      Users[] memory tempUser;
    // for(uint i=0;i<users.length;i++){
    //     if(users[i].length !=0){
    //         tempUser.push(users[i]);
    //     }
    // }
    // return tempUser;

    // }

    function buyPoketmon() public {
        require(balance[msg.sender] >= tokenPrice);
        balance[msg.sender] -= tokenPrice;
        totalSupply -= tokenPrice;

        uint random = uint(
            keccak256(
                abi.encodePacked(block.timestamp, block.coinbase, block.number)
            )
        );
        random = uint(random % 3); //0~3까지의 랜덤값

        // Pokets구조체 형태로 객체를 만들어서 배열에 푸쉬
        poketmons[msg.sender].push(
            Pokets(poketmonUrl[random], poketmonName[random])
        );
        // 유저가 포켓몬 빵을 한 번 산적이 있는지
        bool isUser = false;
        for (uint256 i = 0; i < users.length; i++) {
            if (users[i].account == msg.sender) {
                isUser = true;
                break;
            }
        }

        if (!isUser) {
            users.push(Users(msg.sender));
        }
    }
}

  • Pokets은 포켓몬의 정보가 들어있는 구조체이고, Users는 포켓몬을 한 번이라도 구매한 유저가 있는 구조체 이다.

  • tokenPrice 는 토큰의 단위이다. 10000개를 발행하면 mint함수에서 10^18을 곱한 값 (wei)로 저장되는데, 이는 ether와 단위가 같아 1000 ether이면 1000 * 10^18이 된다. 즉, 1000ether는 1000토큰이 되는 것이다.

  • tradePoketmon에서는 해당 계정의 포켓몬을 순회 탐색하여 사용자가 원하는 포켓몬이 나올 경우, 이를 to 계정에 보내고 기존 계정에 있던 포켓몬(poketmons[msg.sender])은 삭제된다.

  • 이 때 구조체에 있는 string은 일반적인 방법으로 비교가 안된다.

    keccak256(abi.encodePacked(tempPoketArr[index].name)) ==
    keccak256(abi.encodePacked(tradePoketmonName))

  • 때문에keccak256 해시를 통해 비교해야 한다. 자바스크립트에 감사하자

if (index < poketmons[msg.sender].length - 1) {
poketmons[msg.sender][index] = poketmons[msg.sender][ tempPoketArr.length - 1 ];
}

  • 일반적인 방법으로 해당 index요소를 solidity에선 삭제할 수 없다. 따라서 끝에 있는 요소를 해당 index요소에 삽입후, 마지막 요소를 제거하여 (pop) 가스비를 절약할 수 있고, 삭제도 가능하다.

4. App.js


import React, { useEffect, useState } from "react";
import useWeb3 from "./hooks/web3.hook";
import abi from "./abi/Poketmon.json";
function App() {
  const { user, web3 } = useWeb3();
  const [contract, setContract] = useState(null);
  const { token, setToken } = useState(0);
  const [account, setAccount] = useState([]);
  const [trainer, setTrainer] = useState([]);
  const [selectedPoketmon, setSelectedPoketmon] = useState();
  const [toAccount, setToAccount] = useState("");
  const img = {
    width: "200px",
    height: "200px",
  };
  useEffect(() => {
    if (web3 !== null) {
      if (contract) {
        return;
      }
      const poketmon = new web3.eth.Contract(
        abi,
        "0x6c4496dC9196A8458875a7eD26229FfF41EF6FAb",
        { data: "" }
      );
      setContract(poketmon);
    }
  }, [web3]);

  //   해당 지갑의 포켓몬 조회
  const getPoketmon = async (account) => {
    const result = await contract.methods.getPoketmon().call({ from: account });
    console.log(result);
    return result;
  };

  // 지갑의 토큰 양 조회
  const getToken = async (account) => {
    if (!contract) {
      return;
    }
    let result = web3.utils
      .toBigInt(await contract.methods.balanceOf(account).call())
      .toString(10);

    result = await web3.utils.fromWei(result, "ether");
    return result;
  };

  //   메타 마스크 계정들 조회
  const getAccount = async () => {
    const accounts = await window.ethereum.request({
      method: "eth_requestAccounts",
    });
    const _accounts = await Promise.all(
      accounts.map(async (account) => {
        const token = await getToken(account);
        const poketmon = await getPoketmon(account);
        // 추가로 어떤 포켓몬도 있는지 추가할 부분
        return { account, token, poketmon };
      })
    );
    setAccount(_accounts);
  };
  const getNewPoketmon = async () => {
    await contract.methods.buyPoketmon().send({
      from: user.account,
    });
  };

  const getUsersWhoHaveDrawn = async () => {
    const result = await contract.methods.getPoketmonUsers().call({
      from: user.account,
    });
    console.log(result);
    setTrainer(result);
  };

  // 포켓몬 트레이드 시스템
  const tradePoketmon = async () => {
    await contract.methods.tradePoketmon(toAccount, selectedPoketmon).send({
      from: user.account,
    });
  };
  // 1.포켓몬 랜덤으로 뽑을 수 있는 버튼
  // 2. 한 번이라도 뽑은 계정만 모으고 어떤 포켓몬을 가지고 있는지 보여주기
  // 3.포켓몬 거래(소유권) 함수 작성
  // 4.포켓몬 대전 판돈(선택)
  useEffect(() => {
    if (!contract) {
      return;
    }
    getAccount();
  }, [contract]);

  useEffect(() => {
    console.log(selectedPoketmon);
  }, [selectedPoketmon]);

  if (user.account === null) {
    return "지갑 연결하세요";
  }

  return (
    <div>
      <div>토큰 보유량:{token}</div>
      {account.map((el, index) => {
        return (
          <div key={index}>
            계정:{el.account} <br />
            토큰 값:{el.token}
            <div> 포켓몬들</div>
            <div style={{ display: "flex" }}>
              {el.poketmon.map((el2, index2) => {
                return (
                  <div key={index2}>
                    {el2.name}: <img style={img} src={el2.url} alt="포켓몬" />
                  </div>
                );
              })}
            </div>
          </div>
        );
      })}
      <div style={{ width: "100%", height: "500px", margin: "auto" }}>
        <img
          src="https://mblogthumb-phinf.pstatic.net/20160326_233/poi8969_1458975983221ymkBv_JPEG/cafe_naver_com_20160326_152644.jpg?type=w2"
          alt="오박사"
        />{" "}
        <br />
        <button onClick={getNewPoketmon}>너의 포켓몬을 골라보려어어엄!</button>
      </div>

      <div>
        <h2>포켓몬 트레이너들 보기</h2>
        <button onClick={getUsersWhoHaveDrawn}>
          스바라시한 트레이너들 보기
        </button>
        {trainer &&
          trainer.map((el, index) => {
            return (
              <ol key={index}>
                <li>{el.account}</li>
              </ol>
            );
          })}
      </div>

      <div style={{ width: "100%", height: "400px", margin: "auto" }}>
        <h2>포켓몬 입양 보내기</h2>
        <div>
          당신이 세상에서 전부인 불쌍한 포켓몬을 입양보내는 시스템 입니다.
        </div>
        <div>입양 보낼(ㅠㅠ) 포켓몬 선택하기</div>
        <label>
          피카츄
          <input
            name="radio"
            type="radio"
            value={"pikachu"}
            onChange={(e) => {
              setSelectedPoketmon(e.target.value);
            }}
          />
        </label>
        <label>
          파이리
          <input
            name="radio"
            type="radio"
            value={"Charmander"}
            onChange={(e) => {
              setSelectedPoketmon(e.target.value);
            }}
          />
        </label>
        <label>
          꼬북칩
          <input
            name="radio"
            type="radio"
            value={"kobuk"}
            onChange={(e) => {
              setSelectedPoketmon(e.target.value);
            }}
          />
        </label>
        <br />
        <br />
        <br />
        <label>당신보다 훌륭한 주인의 wallet입력하기</label>
        <input
          onChange={(e) => {
            setToAccount(e.target.value);
          }}
        />
        <button onClick={tradePoketmon}>입양 ㄱㄱ</button>
      </div>
    </div>
  );
}

export default App;

5.결과

1.처음 화면

  • 여기서 현재 계정으로 포켓몬을 추가해 보겠다.

2. 포켓몬 추가



  • 보류가 된지도 모르고 두번 눌렀다
  • 랜덤으로 지정된 피카츄,꼬부기가 생성되었다.
    이제 바로 밑 계정으로 포켓몬을 보내자

3.포켓몬 트레이드



  • 성공적으로 꼬부기가 0xf14710178e50126f09faefa0ef374788f1472f10 계정으로 전달되었다.

  • 성공적으로 전송을 요청한 계정에서 꼬부기가 삭제되었다.

6.느낀점

실제 토큰으로 nft 비스무리한 것을 교환하니 굉장히 재미가 있었다.
기본적으로 수수료 없이 원하는 토큰을 교환하고 이를 실시간으로 확인 한다는점이 의의가 있었고, 코어쪽 (IERC20)등등을 더 공부하여 베이스를 더 이해 할 수 있도록 해야겠다.

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

0개의 댓글