크립토 좀비 Lesson 4,5,6 🧟

wook2·2021년 3월 18일
0

블록체인

목록 보기
1/1
post-thumbnail

🔥 Lesson4

Lesson4에서는 새로운 개념들 보다는 기존의 개념들을 복습하고 코드 리팩토링을 하였다. 핵심 개념들을 복습하여보자.

제어자


  • 함수 제어자

    함수에 어떤 제약을 준다거나 부가적인 기능을 가능하게 해주는 역할
modifier onlyOwner() {
  require(msg.sender == owner);
  _;
} 

위의 modifier는 msg.sender == owner여야 한다는 제약을 준다.
이 modifier에 의해 작성된 함수는 modifier를 거친 뒤, _;부터 실행한다.

솔리디티의 함수는 private, internal, public, exteral중 하나로 명시되어야 한다.
- private : 해당 컨트랙트에서만 호출될 수 있고, 상속되는 컨트랙트나 다른 외부 컨트랙트에서 호출될 수 없다. 또한 함수를 정의할때 이름 앞에 _ 를 붙이는 관례가 있다.
- internal : private와 비슷한 성격이다. 컨트랙트 내부와 상속된 컨트랙트에서 호출될 수 있다. private와 마찬가지로 함수이름 앞에 언더바를 붙여준다.
- public : 해당 컨트랙트 뿐만 아니라, 외부 컨트랙트에서도 호출될 수 있다.
- external : 컨트랙트 외부에서만 호출될 수 있다. 다른 컨트랙트와 트랜잭션으로만 호출될 수 있다.

함수의 상태를 제어하는 제어자에는 viewpure가 있다. 컨트랙트 외부에서 불렀을때 가스를 소모하지 않는다는 특징이 있다.
- view : view는 읽기 전용으로, 블록체인 네트워크 상의 데이터를 읽기만 하고 수정할 수 없습니다.

function getKittyName() view {
  return addressToKitty[msg.sender];
}

pure : pure는 순수함수로, 오로지 함수의 파라미터 값을 이용해 return을 하는 함수입니다. 블록체인 네트워크 상의 어떠한 데이터도 읽지 않는 역할을 합니다.

function Calc (uint a, uint b, uint c) pure {
  return (a * b + c) * (c - a) + b;
}
  • payable 제어자

    payable 제어자는 외부에서 이더를 송금받을수 있게 해주는 제어자이다. payable을 작성한 함수에서만 이더를 보낼 수 있기 때문에, 스마트 컨트랙트 내의 송금을 하는 함수는 반드시 payable키워드와 함께 작성되어야 한다.

컨트랙트에 예금하는 함수를 다음과 같이 payable 제어자를 이용해 작성할 수 있다.

function deposit() payable {
  deposits[msg.sender] += msg.value;
};

송금과 출금

  • 송금

    개인 뿐만 아니라 스마트 계약도 내부적으로 계정을 가진다. 계약을 통해
    A라는 사람이 B에게 1ETH를 보낸다고 할 때, 내부적으로는 다음과 같은 방식으로 처리를 한다.
    1. A가 본인의 계정에서 1ETH를 계약 계정으로 송금한다.
    2. 계약 계정에서 1ETH를 B계정으로 송금한다.

    다음과 같은 방식으로 송금할 수 있다.
contract OnlineStore {
  function buySomething() external payable {
    require(msg.value == 0.001 ether);
    transferThing(msg.sender);
  }
}
  • 출금

    컨트랙트로부터 이더를 인출하는 함수를 작성해야 출금할 수 있다.
    withdraw함수에서 transfer를 통해 컨트랙트에서 이더를 전송할 수 있다.
function withdraw() external onlyOwner {
    owner.transfer(this.balance);
  }

  • 솔리디티를 통한 난수 생성

    keccak256함수 사용
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;

now, msg.sender, randNonce를 통해서 해시값으로 만든 뒤, uint형으로 바꾸고 마지막 2자리만 받아와서 0부터 99까지의 숫자를 얻을 수 있다.

  • 취약점 : 트랜잭션을 알리지 않고 본인에게 유리한 경우만 트랜잭션을 블록에 추가할 수 있다.
  • oracle을 통해 안전한 난수를 만들 수 있다.

🔥 Lesson 5

ERC란?

  • ERC란 Ethereum Request for Comments의 약자로 이더리움 논평 요청서로 해석될 수 있다. 이더리움상의 프로그래밍 표준을 설명하는 기술 문서이다.
  • ERC의 목표는 어플리케이션과 컨트랙트가 보다 쉽게 상호작용하는 규약을 만드는 것이다.
  • 이더리움 블록체인 상의 디앱은 또 다른 다양한 분야에 적용될 수 있는 각각의 솔루션으로 그에 맞는 토큰을 발행하는 것이다. 이더리움 위에서 사용 가능한 토큰이다.

[출처] : (https://hackernoon.com/ethereums-erc-20-tokens-explained-simply-88f5f8a7ae90)

ERC20

이더리움 네트워크 상에서 유통할 수 있는 토큰의 호환성을 보장하기 위한 표준 사양중 하나이다.
이더리움을 안드로이드나 ios같은 플랫폼으로 생각하면, ERC20은 그 위에서 구동되는 앱이라고 비유할 수 있다.


ERC 721

ERC 721에서 제공하는 인터페이스이다. 메서드를 구현하여 ERC721 토큰을 생성할 수 있다.
[출처]: (https://docs.openzeppelin.com/contracts/3.x/api/token/erc721)

메서드를 작성하기 위하여, ERC721.sol 파일을 통해 ERC721 컨트랙트를 상속하고 메서드를 구현하여 준다.

contract ZombieOwnership is ZombieAttack, ERC721 {
 }

위의 코드와 같이 다중 상속을 지원하기 때문에, 두 컨트랙트를 상속하여 온다.가장 오른쪽의 컨트랙트의 메서드부터 오버라이딩을 받기 때문에 ERC721을 오른쪽에 적어준다.

  • balanceOf

    address를 받아서 토큰을 얼마나 가지고 있는지를 반환하는 메서드를 만들어 준다. 이전 Lesson에서 만들었던 mapping을 이용하여 구할 수 있다.
function balanceOf(address _owner) public view returns (uint256 _balance) {
    return ownerZombieCount[_owner];
  }
  • ownerOf

    토큰ID를 받아서 주인(addresss)를 반환하는 메서드를 만들어 준다.
function ownerOf(uint256 _tokenId) public view returns (address _owner) {
    return zombieToOwner[_tokenId];
  }

  • ownerOf 제어자 수정하기

    이전에 만들었던 ownerOf라는 좀비의 주인이 이 함수를 실행하는 사람인지 확인하는 제어자가 있었다.
    하지만 ERC721 인터페이스에서도 ownerOf라는 메서드가 존재해 메서드 이름에 충돌이 발생한다.
    ERC721인터페이스를 상속하는 owenrOf의 이름을 바꾸는 방법은 무리가 있기때문에, 기존의 modifier ownerOf의 명칭을 onlyOwnerOf로 바꾸어 주었다.
modifier onlyOwnerOf(uint _zombieId) {
    require(msg.sender == zombieToOwner[_zombieId]);
    _;
  }

ERC721 전송 로직

1. transfer
전송상대의 addess와 보내고자 하는 token의 Id와 함께 함수를 실행한다. 토큰의 소유자가 전송하는 방식이다.

2. approve, takeOwnership
토큰의 소유자가 자신의 토큰을 가져갈 수 허가를 받은 주소를 매핑한다. 이후 허가를 받은 사람이 takeOwnership을 통해 토큰을 가져갈 수 있다.

_transfer함수를 만들어 1번과 2번 공통의 전송로직을 작성한다.

function _transfer(address _from, address _to, uint256 _tokenId) private {
   ownerZombieCount[_to]++; // 토큰을 받는 사람의 count++
   ownerZombieCount[_from]--; // 토큰을 주는 사람의 count--
   zombieToOwner[_tokenId] = _to; // 토큰의 주인 바꾸기
   Transfer(_from, _to, _tokenId); // 전송 이벤트 실행
 }

transfer함수 작성하기

토큰 전송함수에 전송로직을 작성하였다. onlyOwnerOf 제어자를 통해 토큰의 주인만 보낼수 있다.

function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
   _transfer(msg.sender, _to, _tokenId);
 }

approval함수 작성하기

zombieApprovals매핑을 통해 토큰의 아이디를 통해 허가받은 주소를 확인한다. approval함수를 실행하고 마지막에는 ERC721의 Approval이벤트를 실행한다.

mapping (uint => address) zombieApprovals;
function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
    zombieApprovals[_tokenId] = _to;
    Approval(msg.sender, _to, _tokenId);
  }

takeOwnership함수 작성하기

토큰을 받는사람이 토큰을 가져가도록 하는 함수이다. require를 통해 받는 사람이 가져갈 자격이 있는지 확인한다.

function takeOwnership(uint256 _tokenId) public {
    require(zombieApprovals[_tokenId] == msg.sender);
    address owner = ownerOf(_tokenId);
    _transfer(owner, msg.sender, _tokenId);
  }

컨트랙트 보안 강화 safeMath

uint256를 사용한다고 하더라도, 2^256가지나 되지만 혹시나 만일의 overflow라는 상황에 대비해 보호장치를 해주어야 한다. 이를 막기 위해 openZeppelin의 safeMath 라이브러리를 사용한다.

using SafeMath for uint256;

uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10

safeMath를 이용하여 ++,--를 add와 sub함수로 바꾸었다.

ownerZombieCount[_to] = ownerZombieCount[_to].add(1);
    ownerZombieCount[msg.sender] = ownerZombieCount[msg.sender].sub(1);
  • safeMath16, safeMath32

    uint16또는 uint32로 선언된 변수를 safeMath를 통해 계산을 하면 오버플로우나 언더플로우 문제를 해결할 수 없다. 이 변수들을 uint256으로 바꾸고 계산하기 때문에 오버플로우를 막지 못한다.
    그렇기 때문에 safeMath16, safeMath32를 이용하여 따로 처리를 해주어야 한다.
using SafeMath32 for uint32;
using SafeMath16 for uint16;

주석

솔리디티에서 표준으로 쓰이는 주석 형식은 natspec 이다. @title, @author @param @return등 주석으로 해당라인이 어떤의미인지 알려줄 수 있다.

/// @title 좀비 소유권 전송을 관리하는 컨트랙트
/// @author wook
/// @dev OpenZeppelin ERC721

🔥 Lesson 6

web3.js

이더리움 네트워크에서 사용자가 스마트 컨트랙트에게 질의할 수 있도록 해주는 라이브러리이다. JSON-RPC를 통해 소통한다.

  • JSON-RPC
    REST방식이 HTTP위에서 동작하는 방식이었다면, JSON-RPC는 TCP위에서 동작한다.
    • REST는 action을 처리하기 어렵다.
      REST는 객체에 대해 CRUD 방식을 사용한다. GET/zombie/validate 라는 API가 있다면, 명사가 아니기 때문에 REST 방식이라고 보기 어렵다.
    • JSON-RPC는 다양한 action을 표현할 수 있다.
      zombie.calculate.LevelupFee // 좀비가 레벨업 할때까지 필요한 fee계산
      zombie.validate // 좀비가 유효한지 확인
  • Web3 Privider

    • provider는 app이 이더리움 네트워크의 노드들과 통신하게 해준다.
    • provider에서는 내부적으로 sendAsync 메서드를 사용하는데, JSON-RPC payload request를 받아서 처리해준다.
    • provider를 사용하면, dapp에서 이더리움 노드에게 서명, 가스비, 트랜잭션 제출등의 요청을 보낼 수 있다.
  • infura
    infura를 provider로 사용하면 이더리움 블록체인과 메시지를 쉽고 안전하게 주고받을 수 있다.
    [출처] : (https://infura.io/)

  • metamask
    이더리움 계정과 개인 키를 안전하게 관리할 수 있게 해주는 크롬과 파이어폭스의 브라우저 확장 프로그램이다.
    metamask는 web3라는 전역 자바스크립트 객체를 통해 web3 provider를 주입한다. 그렇기 때문에 앱에서 web3의 존재여부를 확인하면 해당 provider를 사용하면 된다.

window.addEventListener('load', function() {

        // Web3가 브라우저에 주입되었는지 확인(Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
          // Mist/MetaMask의 프로바이더 사용
          web3js = new Web3(web3.currentProvider);
        } else {
          // 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
          // 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
        }

        //앱을 시작
        startApp()
      })

컨트랙트와 통신하기

  • 컨트랙트 ABI
    ABI는 Application Binary Interface의 줄임말이다. 기본적으로 JSON 형태로 컨트랙트의 메소드를 표현한다.
    컨트랙트와 web3가 어떤 형식으로 통신해야 하는지를 알려주는 것이다.

    컨트랙트 주소와 ABI를 통해 컨트랙트를 인스턴스화 하였다.
var cryptoZombies;
      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
      }
  • call
    컨트랙트 함수를 호출하기 위해 web3에서 사용한다. call은 view와 pure전용으로, 트랜잭션을 만들지 않는다.
function zombieToOwner(id) {
        return cryptoZombies.methods.zombieToOwner(id).call()
      }
  • send
    send는 트랜잭션을 만들고 블록체인 상의 데이터를 변경한다. 메타마스크를 사용한다면 트랜잭션 발생시 서명을 위한 창이 나온다.
      function getZombiesByOwner(owner) {
        return cryptoZombies.methods.getZombiesByOwner(owner).call()
      }

  • 메타마스크 계정(address) 이용하기
    메타마스크를 통해 주입된 web3 변수에 활성화된 계정을 확인할 수 있다.
    100ms마다 사용자 계정이 바뀌었는지 확인하는 setInterval을 만들어 주었다.
var accountInterval = setInterval(function() {
          // 계정이 바뀌었는지 확인
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
            // 새 계정에 대한 UI로 업데이트하기 위한 함수 호출
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);

  • 페이지에 좀비 보여주기
    메타마스크 계정이 바뀌었을때 그 계정이 가지고 있는 군대로 보여주어야 한다. then을 통해 받은 배열을 displayZombies로 실행시킨다.
    displayZombies에는 이전의 div 태그 내용을 지우고, 좀비를 하나씩 붙여준다.
function displayZombies(ids) {
        $("#zombies").empty();
        for (id of ids) {
          // 우리 컨트랙트에서 좀비 상세 정보를 찾아, `zombie` 객체 반환
          getZombieDetails(id)
          .then(function(zombie) {
            // 각각을 #zombies div에 붙여넣기
            $("#zombies").append(`<div class="zombie">
              <ul>
                <li>Name: ${zombie.name}</li>
                <li>DNA: ${zombie.dna}</li>
                <li>Level: ${zombie.level}</li>
                <li>Wins: ${zombie.winCount}</li>
                <li>Losses: ${zombie.lossCount}</li>
                <li>Ready Time: ${zombie.readyTime}</li>
              </ul>
            </div>`);
          });
        }
      }

  • 컨트랙트 함수 실행하기
    CryptoZombies.methods.createRandomZombie(name)를 통해서 컨트랙트의 함수를 실행한다.
    .send를 통해 사용자를 보내고, on을 통해 성공시와 실패시를 처리해준다.
    성공했다면 UI를 다시 그린다.
    실패했다면 사용자에게 실패 메시지를 전송한다,
function createRandomZombie(name) {
  // 시간이 꽤 걸릴 수 있으니, 트랜잭션이 보내졌다는 것을
  // 유저가 알 수 있도록 UI를 업데이트해야 함
  $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
  // 우리 컨트랙트에 전송하기:
  return CryptoZombies.methods.createRandomZombie(name)
  .send({ from: userAccount })
  .on("receipt", function(receipt) {
    $("#txStatus").text("Successfully created " + name + "!");
    // 블록체인에 트랜잭션이 반영되었으며, UI를 다시 그려야 함
    getZombiesByOwner(userAccount).then(displayZombies);
  })
  .on("error", function(error) {
    // 사용자들에게 트랜잭션이 실패했음을 알려주기 위한 처리
    $("#txStatus").text(error);
  });
}

  • 이더 지불하기
    레벨업 하는데 이더를 지불한다. 이더를 지불하기 전에 이더의 가장 작은 단위인 wei로 바꾸어서 지불해야 한다.
function levelUp(zombieId) {
        $("#txStatus").text("좀비를 레벨업하는 중...");
        return CryptoZombies.methods.levelUp(zombieId)
        .send({ from: userAccount, value: web3.utils.toWei("0.001") })
        .on("receipt", function(receipt) {
          $("#txStatus").text("압도적인 힘! 좀비가 성공적으로 레벨업했습니다.");
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }

  • Event 구독하기
    web3에서 컨트랙트 이벤트를 구독하기 위해서는 다음과 같은 코드를 작성하면 된다.
    cryptoZombies.events.NewZombie().on()
    ...
    하지만 이것은 새로운 좀비가 나타날 때 모든 이벤트에 대해서 구독하기 때문에 특정 사용자에 대한 이벤트만 받도록 제한해야 한다.

  • indexed
    이벤트를 필터링하기 위해 사용된다.

event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
// filter를 통해서 사용자에 대한 Transfer만 받을 수 있다.
  • 지난 이벤트 받기
    getPastEvents를 이용해 지난 이벤트를 받을 수 있다. fromBlock과 toBlock을 통해 이벤트에 대한 범위를 설정할 수 있다.
    이 방식을 통해 지난 이벤트에 대해 받는 것은 값싼 storage 역할을 할 수 있다.
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })

사용자가 송금을 받았을때 UI를 교체하는 함수이다.

cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
        .on("data", function(event) {
          let data = event.returnValues;
          getZombiesByOwner(userAccount).then(displayZombies);
        }).on("error", console.error);
      }

profile
꾸준히 공부하자

0개의 댓글