‘이더리움 댑 개발’ 세미나 10. 솔리디티 공식 문서(버전 0.8.x) – Basics

이번 세미나는 솔리디티 공식 문서(버전 0.8.x)의 Basics 부분을 다룹니다.

 

솔리디티는

  • 스마트 컨트랙트를 구현하기 위한 객체지향 프로그램 언어입니다.
    • Smart contracts are programs which govern the behavior of accounts within the Ethereum state.
  • C++, 파이썬, 자바스크립트의 영향을 받았습니다.
  • 정적 타입 언어이고, 다중 상속을 지원하고, 라이브러리와 복잡한 사용자 정의 형을 지원합니다.
  • 빠르게 변경되는 언어입니다.
    • 가능한 최신 릴리즈 버전을 사용하도록 합니다.
    • 0.x 버전 번호를 사용하는데, x의 변경은 다른 버전 번호의 메이저 번호의 변경에 해당합니다.

 

Solidity is an object-oriented, high-level language for implementing smart contracts. Smart contracts are programs that govern the behavior of accounts within the Ethereum state.

A contract in the sense of Solidity is a collection of code (its functions) and data (its state) that resides at a specific address on the Ethereum blockchain.

솔리디티는 컨트랙트 개발 언어. 블록체인 중에서 이더리움에서. 이더리움에서 확장되는 블록체인들에서

Introduction to Smart Contracts

Storage Example을 작성해 봅니다. 리믹스를 사용합니다.

  • 컨트랙트 이름은 SimpleStorage입니다.
  • SimpleStorage는
    • 상태변수로 uint 타입의 storedData를 갖습니다.
    • storedData 값을 읽고 쓰는 get 함수와 set 함수를 갖습니다.

 

  • 컨트랙트는 기본적으로 오픈소스이므로 라이센스를 명시해 주는 것이 좋습니다.
    • //SPDX-License-Identifier: <라이센스>
  • 솔리디티는 변화가 매우 빠른 언어입니다. 언어 버전마다 컴파일러가 지원하는 것과 그렇지 않은 것이 다르기 때문에 컴파일러 버전에 신경을 써야 합니다.
    • pragma solidity <컴파일러 버전>
      • pragma solidity >=0.4.16 <0.9.0;
        • 부등호를 사용해서 특정 범위의 버전을 명시할 수 있습니다.
        • 해당 소스코드는 0.4.16 이상 0.9.0 미만 컴파일러로 컴파일해도 문제될 게 없다를 나타냅니다.
      • 등호를 사용해서 특정 버전을 명시할 수도 있습니다. 등호는 생략할 수 있습니다.
        • 등호를 생략하고 특정 버전 컴파일러를 명시하는 방법을 권합니다.
      • ^를 사용할 수도 있습니다.
  • 상태 변수나 함수 가시성은 명시적으로 작성하는 권합니다.
  • private 상태 변수 이름 앞에는 ‘_’를 붙여 작성하기를 권합니다.
  • 상태 변수 값을 설정하는 set 함수의 매개변수 이름은 일괄적으로 ‘value’로 작성하는 것을 권합니다.
  • 컨트랙트 내부에서 호출하는 함수가 아닌 경우는 public을 사용하지 않는 것을 권합니다.
    • 외부에서도 사용하고 내부에서도 사용하는 경우에만 public을 사용합니다.
  • 컨트랙트 이름, 함수 이름, 변수 이름과 같은 식별자들은 ASCII로만 작성할 수 있습니다. 물론 변수 값으로 저장되는 문자열은 유니코드(utf-8)를 지원합니다.

 

Subcurrency Example을 작성해 봅니다.

  • 컨트랙트 이름은 Coin입니다.
  • Coin은
    • 상태변수로
      • address 타입의 minter를 갖습니다.
        • 컨트랙트 생성 시 msg.sender로 설정합니다.
      • mapping 타입의 balances를 갖습니다.
        • balances는 주소와 uint를 매핑합니다.
    • storedData 값을 읽고 쓰는 get 함수와 set 함수를 갖습니다.
  • 함수로
    • mint를 갖습니다.
      • 매개변수로 address 타입의 receiver와 uint 타입의 amount를 갖습니다.
      • 리턴은 없습니다.
      • mint가 가능하려면 msg.sender가 minter와 같아야 합니다.
      • mint를 한다는 것은 receiver의 현재 잔고(balances[receiver])에 해당 수량(amount)을 더하는 것입니다.
      • receiver, amount로 Mint 이벤트를 발생시킵니다.
    • send를 갖습니다.
      • mint와 마찬가지로 receiver와 amount를 매개변수로 갖고, 리턴은 없습니다.
      • send가 가능하려면 msg.sender의 잔고가 amount 보다는 크거나 같아야 합니다.
      • send를 한다는 것은 msg.sender의 잔고에서 amount를 빼고, receiver의 잔고에 amount를 더하는 것입니다.
      • msg.sender, receiver, amount로 Sent 이벤트를 발생시킵니다.
    • Mint, Sent 이벤트 정의를 포함합니다.

 

  • 상태 변수를 public으로 선언하면 컴파일러는 자동적으로 get 함수를 생성해 줍니다.
    • external view 함수 입니다.
    • 배열이나 매핑이 아닌 타입의 상태 변수는 매개변수 없이 상태 변수 값을 리턴합니다.
      • function minter() external view returns (address) { return minter; }
    • 배열은 인덱스 타입을 매개변수로, 매핑은 키 타입을 매개변수로 작성해 줍니다. 배열은 배열 요소를 매핑은 값을 리턴해 줍니다.
      • function balances(address _account) external view returns (uint)  { return balances[_account]; }
      • 주소에 대한 잔고를 구하는 함수가 필요하다면 balanceOf 함수를 직접 작성합니다.
  • 상태 변수를 private으로 선언하고 직접 get 함수를 작성하는 것을 권합니다.
    • 배열이나 매핑 타입의 상태 변수에 접근하는 함수는 목적에 맞는 적당한 이름으로 작성합니다.
      • 계정 잔고를 구하고자 한다면 balanceOf로 작성합니다.
        • function balanceOf(address account) external view returns (uint)  { return balances[account]; }
  • 생성자의 visibility는 생략하는 것을 권합니다.
    • 컴파일러가 자동으로 처리해 줍니다.
      • abstract인 경우는 internal로, 그렇지 않은 경우 public으로 작성해 줍니다.
  • 상태 변경이 있는 외부 함수의 경우 상태 변경에 대한 이벤트 처리를 필수적으로 권합니다.
Storage, Memory and the Stack

EVM은 데이터가 저장될 수 있는 영역을 세 가지(storage, memory, stack)로 구분합니다.

EVM은 스마트 컨트랙트에 대한 실행환경입니다. EVM은 sandboxed되고 외부와 완전히 고립되어  실행됩니다. 네트워크나 파일 시스템이나 다른 프로세스에 접근할 수 없습니다.

  • 스토리지
    • 계정 별로 할당되는 데이터 영역입니다.
    • 영속성을 갖습니다.
    • 256비트 워드(words)와 256비트 워드를 매핑하는 key-value 저장소입니다. 다른 데이터 영역들에 비해 읽고 쓰는 비용이 큽니다. 이런 비용 때문에 스토리지를 사용해야 할 때는 주의를 기울여야 합니다.
  • 메모리
    • 메시지 호출 마다 초기화 됩니다.
    • 순차적(linear)이고, 바이트 단위로 다뤄질 수 있습니다. 8비트에서 256비트 단위로 쓸 수 있지만 읽을 때는 256비트 단위만 가능합니다.
    • 이전에 untouched된 메모리 워드에 접근할 때는 워드 단위로 확장됩니다. 확장을 위해 가스가 요구됩니다.  더 큰 확장이 요구될 때 비용이 지수적으로 커집니다.
  • EVM은 스택 머신입니다. 모든 연산이 스택 데이터를 사용해서 수행됩니다.
    • 최대 1024개 항목까지 다룰 수 있습니다. 한 항목은 256비트 words입니다.
    • 복사는 스택의 최상위 16개에 속하는 것 중 하나만 가능합니다.
    • 최상위 요소와 그 아래 16개의 요소 중 하나와 교환이 가능합니다.
    • 일반적으로 최상위 두 개의 요소를 사용해서 연산이 이루어지고, 연산 결과는 스택에 푸시됩니다. 연산에 따라 하나 이상을 사용하는 것들도 있습니다.
    • 좀 더  깊은 요소까지 접근하기 위해 스택 요소를 스토리지나 메모리로 이동할 수도 있습니다.

 

Message Calls
  • 컨트랙트는 메시지 호출로 다른 컨트랙트를 호출하거나 EOA에 이더를 보낼 수 있습니다.
    • 메시지 호출은 트랜잭션과 유사하게 source, target, data payload, ether, gas와 리턴 데이터를 갖습니다.
      • 트랜잭션은 내부적으로 더 많은 메시지 호출을 만들 수 있는 최상위 메시지 호출입니다.
  • 컨트랙트는 내부 메시지 호출에 남아 있는 가스 중 얼마나 보낼 것인지 결정할 수 있습니다. 내부 호출 시 가스 부족 예외가 발생하면 에러 값은 stack에 push됩니다.
  • 메시지 호출 시 마다 초기화된 메모리가 할당 됩니다. 호출된 컨트랙트는 payload라 불리는 메시지로 전달된 데이터를 사용합니다. payload는 calldata라 불리는 분리된 영역에 저장됩니다. 실행을 마치면 호출자에 의해 미리 할당된 caller’s 메모리 위치에 저장된 데이터를 리턴할 수 있습니다.
  • 호출 깊이는 1024로 제한되기 때문에 재귀에 주의해야 합니다. 재귀 보다는 반복이 선호됩니다.

 

Delegatecall / Callcode and Libraries
  • delegatecall은 호출되는 컨트랙트가 호출한 컨트랙트의 컨텍스트에서 실행되는 특별한 메시지 호출입니다.
    • 호출한 컨트랙트의 저장소, 주소와 잔고를 사용합니다.
    • msg.sender와 msg.value 값이 바뀌지 않습니다.
    • 호출된 컨트랙트 주소로 부터 코드만 취하는 것입니다.
  • delegatecall은 재사용 가능한 라이브러리 구현을 가능하도록 합니다.

 

Logs
  • 이더리움은 로그 데이터를 저장할 수 있습니다. 솔리디티는 로그 기능을 위해 이벤트를 지원합니다.
    • 컨트랙트에서는 접근할 수 없습니다.
    • 블록체인 외부에서 효율적으로 액세스 할 수 있습니다.
    • 로그 데이터의 일부는 불룸 필터에 저장되기 때문에 효율적이고 암호학적으로 안전한 방법으로 데이터를 검색할 수 있도록 합니다.

 

Create
  • 컨트랙트는 심지어 특별한 연산코드를 사용해 다른 컨트랙트를 생성할 수도 있습니다.
    • 일반적인 메시지 호출과 유일한 차이는
      • payload 데이터가 실행되고 그 결과가 코드로 저장되고
      • 스택에 새로운 컨트랙트 주소가 push되어서 호출자에게 리턴된다는 것입니다.

 

솔리디티 컴파일러 버전
  • 4 부분으로 구성
    • 버전
    • 프리-릴리즈(pre-release) 태그
      • develop.YYYY.MM.DD 또는 nightly.YYYY.MM.DD
    • 커밋 해시
      • commit.GITHASH
    • 플랫폼
  • 예)
    • 릴리즈 예)
      • 0.4.8+commit.60cc1668.Emscripten.clang.
    • 프리-릴리즈 예)
      • 0.4.9-nightly.2017.1.17+commit.6ecb4aa3.Emscripten.clang

Solidity by Example

Voting 컨트랙트 작성해 보기
  • 컨트랙트 이름은 Ballot입니다.
  • 컨트랙트가 생성되고 나면 다수의 제안이 등록되어 있어야 합니다.
    • 생성자를 작성합니다.
      • 컨트랙트 생성 주소를 chairman으로 설정합니다.
        • 상태변수로 address 타입의 chairman을 선언합니다.
      • chairman도 유권자가 됩니다.
        • 등록되는 유권자는 어떤 제안에 대해 투표합니다.
          • Voter 구조체를 작성합니다.
            • 투표 여부를 나타내는 bool 타입의 voted를 속성으로 갖습니다.
            • 어떤 제안에 투표했는지를 index로 관리하는 uint 타입의 vote 속성을 갖습니다.
          • 주소에 Voter 구조체가 매핑된 voters 상태변수를 추가합니다.
        • 위임도 가능합니다.
          • 어떤 주소에 위임 했는지를 나타내는 address 타입의 delegate 속성을 추가합니다.
          • 위임이 가능하기 때문에 한 투표지만 다수의 투표수가 될 수 있습니다. uint weight 속성을 추가합니다.
      • 제안은 byte32[] 타입의 proposalNames 인자로 전달됩니다.
        • 제안은 이름과 투표수를 관리해야 합니다.
          • Proposal 구조체를 작성합니다.
            • byte32 타입의 name 속성과, uint 타입의 voteCount 속성을 갖습니다.
        • proposalNames에 따라 제안목록을 추가하고 관리해야 합니다.
          • Proposal[] 타입의 proposals 상태 변수를 가져야 합니다. 
    • 유권자를 등록합니다.
      • giveRightToVote 함수를 작성합니다.  주소를 인자 voter로 받습니다. 
        • chairman 권한이 있어야 합니다.
        • 등록된 유권자가 아니어야 합니다.
          • 등록되지 않은 유권자라면 weight가 0입니다.
          • 등록된 유권자가 위임했을 수도 있기 때문에 voted 여부도 확인해야 합니다.
        • weight를 1로 설정합니다.
    • 위임 합니다.
      • delegate 함수를 작성합니다. 
        • 위임을 받는 주소를 인자 to로 받습니다.
        • msg.sender가 to가 아니어야 합니다.
        • msg.sender가 유권자로 등록되어 있어야 하고, 아직 투표하지 않은 상태여야 합니다.
          • 로컬 변수의 데이터 위치를 storage로 선언(참조 타입이라는 것)하고 상태 변수 값을 할당하면 참조가 할당 됩니다. storage로 선언된 로컬 변수를 로컬 storage 변수라고 합니다. 이름은 storage이지만 상태 변수 값을 저장하는 스토리지는 아닙니다.
        • msg.sender가 to로 부터 위임 받은 주소가 아니어야 합니다. 위임 고리를 추적해 봐야 합니다.
        • 위임을 했다는 것은 투표를 했다는 것으로 여겨집니다.
        • 위임 처리를 합니다.
        • to가 이미 투표를 했다면 위임한 weight를 투표 결과로 반영해야 합니다.
    • 투표 합니다.
      •  vote 함수를 작성합니다. 
        • 투표 대상이 되는 제안의 인덱스를 인자 proposal로 받습니다.
        • msg.sender가 유권자로 등록되어 있어야 하고, 아직 투표하지 않은 상태여야 합니다.
        • 투표 처리를 합니다.
    • winningProposal을 구합니다.
      • winningProposal 함수를 작성합니다.
        • 제안 중에서 voteCount가 가장 큰 제안을 리턴합니다.
        • getWinnerName에서도 호출해야 하니까 public으로 작성합니다.
        • 상태 변수 값을 읽기 만 하니까 view로 작성합니다.
    • 채택된 제안의 이름을 리턴합니다.
      • getWinnerName 함수를 작성합니다.
        • winningProposal로 부터 winner를 구하고, winner에 해당하는 proposal을 구하고 이것의 name 속성 값을 리턴합니다.
        • 상태 변수 값을 읽기 만 하니까 view로 작성합니다.
Simple Open Auction 컨트랙트 작성해 보기
  • 컨트랙트 이름은 SimpleAuction입니다.

경매 시간이 제한되고 경매가 종료되면 낙찰 금액이 beneficiary에게 전송됩니다. 

  • 생성자를 작성합니다.
    • 입찰 시간이 uint biddingTime 인자로 전달됩니다.
      • 경매 종료 시간은 현재 block.timestamp에 biddingTime을 더한 것입니다. 
        • auctionEndTime 상태 변수가 관리되어야 합니다.
    • address beneficiary 매개변수가 요구됩니다. 
      • beneficiary 상태 변수가 관리되어야 합니다. 
  • 입찰 합니다.
    • bid 함수를 작성합니다.
      • 입찰은 이더로 합니다. payable이 되어야 합니다.
      • auctionEndTime이 block.timestamp보다 크거나 같아야 합니다.
      • 입찰 금액이 현재 가장 높은 입찰 금액보다 커야 합니다.
        • 현재 가장 높은 입찰 금액(highestBid)과 입찰자(highestBidder)가 관리되어야 합니다.
      • 입찰 처리합니다.
        • 이전 최고 입찰자가 입찰 금액을 돌려 받을 수 있도록 해야 합니다.
          • 인출을 호출해서 돌려 받아야 하기 때문에 아직 돌려 받지 않은 입찰자들이 관리되어야 합니다.
            • 입찰자와 입찰금액에 매핑으로 pendingReturns 상태 변수를 선언합니다. 
        • 가장 높은 입찰 금액과 입찰자를 갱신합니다.
        • 가장 높은 입찰 금액과 입찰자가 갱신되었음을 알립니다.
          • HighestBidIncreased 이벤트가 필요합니다.
  • 인출 합니다.
    • withdraw 함수를 작성합니다. 
    • 인출할 금액이 남아 있어야 합니다.
    • msg.sender에게 이더를 전송해야 하는데 msg.sender는 address payable 타입이 아니기 때문에 형 변환이 필요 합니다.
      • payable(msg.sender)
    • 인출 처리 합니다.
      • pending 금액을 0으로 설정하고, msg.sender에게 이더를 전송합니다.
  • 경매를 종료 합니다.
    • block.timestamp이 auctionEndTime 보다 크거나 같아야 합니다. 
    • 경매가 종료되지 않았어야 합니다. 경매 종료에 대한 상태 변수가 관리되어야 합니다.
    • 경매 종료를 알립니다.
      • AuctionEnded 이벤트가 필요 합니다.
Blind Auction 컨트랙트 작성해 보기

Simple Open Auction으로 할 경우 어떤 문제점이 있는지 의견을 나눠 봅니다.

 

Blind 방식으로 경매를 진행합니다.

  • 입찰 기간 중에 입찰자는 실제 입찰 내용을 제출하는 것이 아니라 입찰 내용에 대한 해시를 제출합니다(commit).
  • 입찰자가 낙찰되고도 입찰 금액을 제출하지 않을 수 있음으로 입찰 시 입찰 금액도 함께 제출하도록 해야 합니다. 문제는 이렇게 할 경우 입찰 내용을 숨기더라도 입찰 금액이 노출된다는 것입니다.
    • 입찰자가 입찰 금액 이상을 제출하도록 허용합니다. 입찰 금액 한도를 정하고 한도 만큼 보내게 하는 방법을 시도할 수도 있습니다.
  • 입찰 기간이 종료되면 입찰자는 실제 입찰 내용을 공개합니다(reveal).
    • commit만 하고 reveal을 하지 않을 수도 있음으로 reveal한 입찰에 대해서만 경매 종료 후 제출한 입찰 금액을 인출할 수 있도록 합니다.

 

Blind Auction 컨트랙트를 작성합니다.

  • 컨트랙트 이름은 BlindAuction입니다. 
  • 경매를 진행하려면 입찰 마감 시간과 reveal 마감 시간과 beneficiary가 정해져야 합니다.
    • 생성자를 작성합니다.
      • 입찰 기간, reveal 기간, beneficiary를 인자로 받아, 입찰 마감 시간, reveal 마감 시간을 구하고 beneficiary를 설정합니다.
        • biddingTime, revealTime, beneficiary
    • 입찰 마감 시간, reveal 마감 시간을 구하고 beneficiary가 관리되어야 합니다.
      • biddingEnd, revealEnd , beneficiary
  • 입찰 내용에 대한 해시로 입찰 합니다.
    • bid 함수를 작성합니다.
      • 입찰 내용 해시는 bytes32 타입의 blindedBid로 전달됩니다.
      • 입찰 금액에 해당하는 이더를 전송 받을 수 있어야 합니다.
        • payable
      • 입찰 마감 시간 이전에만 가능합니다.
        • onlyBefore modifier를 작성합니다. uint time을 매개변수로 작성합니다.
          • block.timestamp가 time보다 작아야 합니다.
        •  biddingEnd를 onlyBefore의 인자로 전달합니다.
      • 하나의 계정에 대해 여러 번 입찰이 가능합니다. 입찰 내용에 대한 해시와 전송된 이더 수량이 입찰로 관리되어야 합니다.
        • Bid 구조체를 작성합니다.
          • blindedBid와 deposit 속성을 갖습니다.
        • 계정에 매핑된 입찰들을 관리해야 합니다.
          • mapping(address => Bid[]) 타입의 bids 상태 변수를 선언합니다.
    • 해시는 keccak256(abi.encodePacked(value, fake, secret)와 같이 작성합니다.
      • 다른 입찰자들을 속이기 위해 fake 입찰(fake가 true)을 제출할 수 있습니다.
  • 블라인드 입찰을 공개합니다.
    • reveal 함수를 작성합니다.
    • 다수의 입찰이 가능하고 일부는 다른 입찰자들을 속이기 위한 것입니다.
      • 입찰 금액과 fake 여부와 해시는 모두 같은 길이의 배열로 전달됩니다.
        • uint[] memory values, bool[] memory fake, bytes32[] memory secret
    • 입찰 마감이 되고 입찰 공개 시간이 되지 않아야 가능합니다.
      • onlyBefore(revealEnd)
      • onlyBefore를 참조해서 onlyAfter modifier를 작성합니다. biddingEnd로 onlyAfter를 적용합니다.
    • 올바로 블라인드 되었고, fake가 아니고, 최고가 입찰이 아닌 경우 환불 받아야 합니다.
      • 환불 금액을 구해서 전송합니다.
      • 실제적인 입찰 처리를 합니다.
        • place bid 함수를 작성합니다.
          • 최고가 입찰을 갱신합니다.
            • 최고가 입찰자와 최고가 입찰 금액이 관리되어야 합니다.
              • highestBidder, uint highestBid;
          • 최고가 입찰이 아닌 경우 환불 대상이 됩니다. 이 금액은 직접 사용자가 인출을 요청할 때 전송됩니다.
            • mapping(address => uint) pendingReturns;
  • 경매를 마감합니다.
    • revealEnd가 지나야 가능합니다.
    • 마감되지 않은 경매여야 합니다.
      • 마감여부가 관리되어야 합니다.
        • ended
    • AuctionEnded 이벤트를 발생시킵니다.
      • highestBidder, highestBid 속성으로 이벤트를 작성합니다.
    • beneficiary에게 highestBid를 전송합니다.
  • withdraw 함수를 작성합니다.
    • msg.sender에게 pendingReturns를 전송합니다.
Safe Remote Purchase 컨트랙트 작성해 보기

좀 더 안전한 구매를 위해 에스크로를 사용하기도 합니다. 구매자와 판매자는 물품의 두 배 금액을 컨트랙트에 전달합니다. 이 금액은 구매자가 물품을 받았다고 확정할 때까지 컨트랙트가 잠가둡니다. 구매자는 물품을 받았다고 확정하고 예치 금액의 절반을 돌려 받습니다. 판매자는 예치 금액과 물품 금액을 받습니다.

  • 컨트랙트 이름은 Purchase입니다.
  • Purchase는 판매자가 물품 가격의 2배 금액을 제공하면서 시작합니다.
    • 생성자를 작성합니다.
      • msg.sender를 판매자로 설정합니다.
        • 판매자(seller)가 관리되어야 합니다.
      • 물품 가격은 msg.value의 절반에 해당합니다.
        • 2로 나누어야 하므로 msg.value는 짝수로 보내야 합니다.
        • 물품 가격을 설정합니다.
          • 물품 가격(value)이 관리되어야 합니다.
  • 구매자가 구매를 확정합니다.
    • confirmPurchase 함수를 작성합니다.
      • Purchase의 상태가 Created인 경우에만 가능합니다.
        • 상태머신을 모델링합니다.
          • 열거형으로 State를 작성합니다.
        • 상태 체크를 위해 inState modifier를 작성합니다.
      • 구매자는 물품 금액의 2배 만큼의 금액을 전달합니다.
        • 조건 체크를 위해 condition modifier를 작성합니다.
        • 이더 전송이 필요함으로 payable이어야 합니다.
      • 구매 확정 이벤트를 발생시킵니다.
        • 매개변수 없는 PurchaseConfirmed 이벤트를 작성합니다.
      • msg.sender로 구매자를 설정하고, 현재 상태를 State.Locked로 작성합니다.
        • buyer, state가 관리되어야 합니다.
  • Purchase를 취소합니다.
    • abort 함수를 작성합니다.
    • 판매자만 가능합니다.
      • onlySeller modifier를 작성합니다.
    • Created 상태에서만 가능합니다.
    • 취소 이벤트를 발생시킵니다.
      • 매개변수 없는 Aborted 이벤트를 작성합니다.
      • 상태를 Inactive로 설정합니다.
      • 판매자에게 예치된 금액을 전송합니다.
  • 물품 받았음을 확정합니다.
    • confirmReceived 함수를 작성합니다.
    • 구매자만 가능합니다.
      • onlyBuyer modifier를 작성합니다.
    • Locked 상태에서만 가능합니다.
    • 물품을 받았다는 이벤트를 발생시킵니다.
      • 매개변수 없는 ItemReceived 이벤트를 작성합니다.
    • Release 상태로 변경합니다.
    • 구매자에게 물품 금액에 해당하는 금액을 전송합니다.
  • refundSeller 함수를 작성합니다.
    • 판매자만 가능합니다.
    • Release 상태여야 합니다.
    • SellerRefunded 이벤트를 발생시킵니다.
    • 상태를 Inactive로 설정합니다.
    • 판매 금액에 판매자 에스크로까지 포함해서 판매자에게 전송합니다.
Micropayment Channel 컨트랙트 작성해 보기
  • ReceiverPays 컨트랙트를 작성합니다.
    • 앨리스는 직접 이더 전송 트랜잭션을 사용하지 않고, 이메일과 같은 오프체인 방식으로 서명된 메시지를 밥에게 보내는 방식을 사용하고 싶어 합니다.
      • 앨리스는 ReceiverPays 컨트랙트를 작성해서 배포합니다.
      • 밥은 앨리스로 부터 전달받은 서명을 사용해서 ReceiverPays의 지불 요청(claimPayment) 함수를 호출합니다.
    • 이를 위해서는
      • 오프체인에서 메시지에 서명하고, 컨트랙트에서 서명을 검증할 수 있어야 합니다.
        • EIP 762에서 제안된 방식을 사용합니다.
        • 서명 메시지에는 수신자 주소, 수량, 리플레이(replay) 공격을 막기 위한 논스와 컨트랙트 주소를 포함합니다.
          • ethereumjs-abi 라이브러리는 soliditySHA3 함수를 제공합니다.
            • 솔리디티에서 abi.encodePacked를 사용해서 인코딩된 결과에 keccak256 함수를 적용한 것과 같은 결과를 제공합니다.
        • 솔리디티는 메시지와 서명을 가지고 메시지 서명에 사용된 주소를 구할 수 있는 ecrecover 함수를 제공합니다.
  • claimPayment 함수를 작성합니다.
    • 오프체인으로 받은 서명 메시지가 올바른지 검증할 수 있어야 합니다.
      • 오프체인에서 서명한 메시지를 재현할 수 있어야 합니다.
        • 수량, 논스, 서명, 컨트랙트 주소가 있어야 합니다.
          • 컨트랙트 주소는 this로 구할 수 있습니다.
          • 인자로 uint256 amount, uint256 nonce, bytes memory signature을 받습니다.
      • 논스는 한 번만 사용되어야 합니다.
        • 논스 별로 사용 여부가 관리되어야 합니다.
          • mapping(uint256 => bool) usedNonces;
      • abi.encodePacked와 keccak256 함수를 사용해서 메시지를 재현합니다.
        • eth_sign 행위를 모의하기 위해 prefixed hash를 작성해야 합니다.
        • prefixed 함수를 작성합니다.
          • “\x19Ethereum Signed Message:\n32″와 hash를 abi.encodePacked로 결합하고 keccak256 함수를 적용한 결과를 리턴합니다.
    • 컨트랙트 소유자 만이 지불할 수 있습니다.
      • 재현한 메시지와 서명으로 복구한 주소가 owner여야 합니다.
        • recoverSigner 함수를 작성합니다.
          • ecrecover 함수는 인자로 메시지와 서명의 분리된 형태인 v, r, s를 요구합니다.
            •  splitSignature 함수를 작성합니다.
              • sig.length가 65여야 합니다.
              • assembly를 사용합니다.

       

    • msg.sender에게 amount를 전송합니다.
  • shutdown() 함수를 작성합니다.
    • owner만 가능합니다.
    • msg.sender를 인자로 selfdestruct를 호출합니다.

 

Simple Payment Channel 컨트랙트 작성해 보기

지불 채널은 트랜잭션을 사용하지 않고 참여자들이 반복적으로 이더를 보낼 수 있게 합니다.

앨리스는 이더를 컨트랙트에 제공하는데 이것을 지불 채널을 연다라고 합니다. 앨리스는 반복적으로 밥에게 지불합니다. 밥은 채널을 닫고 자신에게 지불된 이더를 인출합니다. 남은 이더는 앨리스에게 전송됩니다.

밥이 채널을 영원히 닫지 않을 수도 있기 때문에 컨트랙트에는 타임아웃이 있어야 합니다.

지불 채널을 닫는 것은 한 번만 가능합니다. 따라서 밥에게 전달된 서명된 메시지들은 누적된 금액이 포함되어야 합니다. 밥은 당연히 가장 큰 금액의 메시지를 사용해서 지불 채널을 닫습니다. 메시지를 작성할 때 논스는 필요 없습니다.

  • SimplePaymentChannel 컨트랙트를 작성합니다. 
  • 컨트랙트 생성 시 수신자와 타임아웃을 설정합니다.
    • address 타입의 recipient와 uint256 duration을 매개변수를 갖는 생성자를 작성합니다.
    • msg.sender를 sender로 할당합니다.
    • block.timestamp에 duration을 더해 expiration으로 할당합니다.
  • sender는 타임아웃을 연장할 수 있습니다.
    • extend 함수를 작성합니다. 
  • recipient는 채널을 닫고 지불을 요청합니다.
    • close 함수를 작성합니다.
      • uint256 amount, bytes memory signature를 매개변수로 작성합니다.
      • 서명이 유효해야 합니다.
        • isValidSignature 함수를 작성합니다.
      • recipient에게 amount를 전송합니다.
      • sender를 인자로 selfdestruct를 호출합니다.
  • 타임아웃이 되면 채널을 닫을 수 있습니다.
    • claimTimeout 함수를 작성합니다.
    • 현재 블록 타임스탬프가 expiration보다 크거나 같아야 합니다.
    • sender를 인자로 selfdestruct를 호출합니다.

 

컨트랙트를 사용해서 수신자는 최종 단계의 메시지 만을 검증합니다. 중간 단계에서도 메시지를 검증할 수 있어야 합니다.

  • ethereumjs-util 라이브러리를 사용해서 검증 코드를 작성합니다.
Modular Contracts 작성해 보기

라이브러리를 사용해서 공통적인 처리를 모듈화할 수 있습니다.

  • Token 컨트랙트를 작성합니다. 
  • transfer, transferFrom 함수를 작성합니다.
  • transfer, transferFrom에서 공통 부분을 Balances 라이브러리의 move 함수로 분리합니다.
  • Token 컨트랙트에서 using for를 사용해서 라이브러리 함수를 attach 합니다.
About the Author
(주)뉴테크프라임 대표 김현남입니다. 저에 대해 좀 더 알기를 원하시는 분은 아래 링크를 참조하세요. http://www.umlcert.com/kimhn/

Leave a Reply

*