‘이더리움 댑 개발’ 세미나 9. 7장. FundraiserFactory

이번 세미나는 7장. FundraiserFactory를 다룹니다.

이번 장을 통해 우리는 개별 기금 모금에 해당하는 Fundraiser를 생성하는 방법에 대해서 배우게 됩니다.

 

Migrating Our FundraiserFactory

Fundraiser 컨트랙트는 FundraiserFactory를 통해 생성될 것이기 때문에 배포될 필요 없고, FundraiserFactory만 배포하면 됩니다.

 

FundraiserFactory 컨트랙트를 배포합니다.

  • 테스트를 작성합니다.
    • test 디렉토리에 fundraiser_factory_test.js 파일을 생성합니다.
    • “FundraiserFactory: deployment”로 최상위 테스트 스위트를 작성합니다.
      • contract 함수를 작성합니다.
    • “it has been deployed” 테스트 케이스를 작성합니다.
  • 테스트를 통과시키기 위해 FundraiserFactory  컨트랙트를 작성합니다.
  • 배포 스크립트를 작성합니다.
    • 2_fundraiser_factory_migration.js

Creating Fundraisers

Fundraiser 컨트랙트를 생성합니다.

  • 테스트를 작성합니다.
    • “FundraiserFactory”로 최상위 테스트 스위트를 작성합니다.
    • “createFundraiser()”로 테스트 스위트를 작성합니다.
    • “it increments the fundraisersCount”로 테스트 케이스를 작성합니다.
      • 기금 모금 갯수(fundraisersCount)가 1증가 해야 합니다.
  • 테스트를 통과시킬 수 있도록 FundraiserFactory 컨트랙트를 작성합니다.
    • Fundraiser 컨트랙트를 임포트합니다.
    • 다수의 생성된 Fundraiser를 저장해야 하므로 Fundraiser 컨트랙트의 동적 배열로 상태변수를 선언합니다.
      • Fundraiser[] private _fundraisers;
    • 기금 모금 갯수를 리턴하는 함수를 작성합니다.
      • _fundraisers.length로 생성된 Fundraiser 갯수를 구해서 리턴합니다. uint256으로 갯수를 리턴합니다.
      • 상태를 쓰지는 않고 읽기만 합니다. view로 작성합니다.
    • createFundraiser 함수를 작성합니다.
        • new로 Fundraiser 컨트랙트를 생성합니다.
          • Fundraiser 컨트랙트 인스턴스가 생성되고  주소를 리턴합니다.
  • 테스트를 실행하고 테스트가 통과되는 것을 확인합니다.
  • FundraiserCreated 이벤트를 발생 시킵니다.
    • “it emits the FundraiserCreated event”로 테스트 케이스를 작성합니다. createFundraiser() 함수 인자로 사용할 정보들은 “it increments the fundraisersCount”에서 테스트 스위트로 이동해서 같이 사용합니다.
  • 테스트가 통과할 수 있도록 FundraiserFactory 컨트랙트를 변경합니다.
    • FundraiserCreated 이벤트를 추가합니다.
      • event FundraiserCreated(Fundraiser indexed fundraiser, address indexed owner);
    • createFundraiser() 함수에서 이벤트를 발생시킵니다.
      • emit FundraiserCreated(fundraiser, msg.sender);
  • 테스트를 실행하고 테스트가 통과됨을 확인합니다.

Viewing Available Fundraisers

페이지를 구분이 가능하도록 기금 모금 목록을 가져올 수 있어야 합니다. 페이지 구분을 위해 가져올 목록 갯수 제한(limit)과 가져오기 시작 위치(offset)를 사용합니다.

  • “fundraisers()”로 테스트 스위트를 작성합니다.
  • “it returns an empty collection”으로 테스트 케이스를 작성합니다.
  • 테스트가 통과할 수 있도록 FundraiserFactory 컨트랙트에 fundraisers() 함수를 추가합니다.
    • limit과 offset을 매개변수로 갖고, 상태 변수를 읽기해야 함으로 view로 작성합니다.
  • 테스트를 실행하고 테스트가 통과됨을 확인합니다.
  • 가져올 목록 제한 갯수 설정에 따라 리턴되는 기금 목록 갯수를 테스트 합니다.
    • 테스트 케이스를 실행하려면 먼저 기금 목록을 생성해 두어야 합니다.
      • createFundraiserFactory() 함수를 추가합니다.
        • addFundraisers() 함수를 추가합니다.
      • 테스트 케이스 실행 전에 createFundraiserFactory() 함수를 호출합니다.
      • 한 번에 최대로 리턴할 수 있는 목록 갯수는 20개 입니다.
        • 가져올 목록 갯수를 10개로 요청할 경우 10개를 리턴합니다.
          • “it returns 10 results when limit requested is 10″으로 테스트 케이스를 작성합니다.
        • 테스트를 통과시키기 위해서 FundraiserFactory 컨트랙트의 fundraisers() 함수를 변경합니다.
          • fundraisersCount()가 limit보다 작으면 fundraisersCount()를, 그렇지 않으면 limit를 size로 설정해서 Fundraiser 배열을 생성해서 리턴한다.
            •  uint256 size = fundraisersCount() < limit ? fundraisersCount() : limit;
               return coll = new Fundraiser[](size);
        • 가져올 목록 갯수를 20개로 요청할 경우 20개를 리턴합니다.
          • “it returns 20 results when limit requested is 20″으로 테스트 케이스를 작성합니다.
          • FundraiserFactory 컨트랙트의 fundraisers() 함수를 변경하지 않아도 테스트가 통과됩니다.
        • 가져올 목록 갯수를 30개로 요청할 경우 30개를 리턴합니다.
          • “it returns 20 results when limit requested is 30″으로 테스트 케이스를 작성합니다.
          • 테스트를 통과시키기 위해서 FundraiserFactory 컨트랙트의 fundraisers() 함수를 변경합니다.
            • 한 번에 제공할 수 있는 최대 갯수 제한을 관리해야 합니다.
              • uint256 constant private _maxLimit = 20;
            • fundraisers()로 전달되는 limit은 최대 갯수 제한을 넘을 수 없습니다.
              • size = size < _maxLimit ? size : _maxLimit;
  • 가져오기 시작 위치 설정에 따라 기금 목록을 가져올 수 있어야 합니다.
    •  “it raises out of bounds error”와 “it adjusts return size to prevent out of bounds error”로 테스트 케이스를 작성합니다.
    • 테스트를 통과시킬 수 있도록 FundraiserFactory 컨트랙트의 fundraisers() 함수를 변경합니다.
      • offset은 fundraisersCount() 보다 작거나 같아야 합니다.
        • require(offset <= fundraisersCount(), “offset out of bounds”);
      • size가 offset에 따라 조정되어야 합니다.
        • uint256 size = fundraisersCount() – offset;
        • offset부터 size 만큼 반복해서 기금 모금을 가져오면 됩니다.

Setting Up the UI

이번 세미나 내용에서 제외합니다.

솔리디티 보강

상수는 키워드 constant를 사용합니다.

 

Address 타입

  • 계정 주소 값을 나타내는 데이터 타입
  • 0x로 시작하는 16진수, 20바이트
  •  0x0000000000000000000000000000000000000000로 초기화
    • 0x0, address(0), address(0x0)과 같음
  • 속성으로 balance를 가짐
  • low level 멤버 함수로 functionscall(), delegatecall(), staticcall()을 가짐
  • address와 address payable을 구분
    • address payable은 transfer()와 send()를 사용해서 이더를 전송할 수 있음
      • send 보다는 transfer 사용을 권장
    • msg.sender는 항상 address payable 타입의 주소를 리턴
      • msg.sender.balance는 잔고에서 트랜잭션 최대 수수료를 뺀 값을 제공

 

call, delegatecall

다른 컨트랙트나 라이브러리의 함수를 호출할 때 low-level call과 delegatecall 함수를 사용할 수 있습니다.

  • 갯수에 상관 없이 솔리디티가 지원하는 어떤 타입의 인자든 전달할 수 있습니다. 인자는 32바이트까지 패딩되고(padded), 트랜잭션 데이터를 만들기 위해 합쳐집니다.
    • 첫 번째 인자는 정확히 4바이트로 패딩되지 않습니다. 호출되는 함수 시그니처를 정의합니다.
  • 가능한 사용하지 않는 것을 권장합니다.
    • low-level 함수는 제어를 타겟 컨트랙트에게 넘겨 줍니다. 제어를 받는다는 것은 상태를 변경할 수 있다는 것을 의미합니다.
    • 솔리디티 0.5.0에서 staticcall  추가됨
      • <address>.staticcall(bytes memory) returns (bool, bytes memory)
      • staticcall은 호출되는 함수의 컨트랙트에서 호출하는 컨트랙트의 상태를 변경할 수 없도록 합니다.
  • call은 호출되는 함수가 자신의 컨트랙트 컨텍스트에서 실행되는 반면, delegatecall은 호출하는 컨트랙트 컨텍스트에서 실행됩니다.
    • 라이브러리 함수 호출은 delegatecall을 사용해서만 초기화 됩니다.
  • <address>.call(…) returns (bool), <address>.delegatecall(…) returns (bool)
    • 솔리디티 0.5.0 이상 버전에서는 매개변수와 리턴이 staticcall과 같아야 함
  • 다른 컨트랙트 함수를 호출할 때는 gas() 함수를 사용해서 가스 사용을 설정할 수 있습니다.
    • 예) otherContract.call.gas(1000000)(“methodToCall”, “param1”);
  • 다른 컨트랙트 함수를 호출할 때는 value() 함수를 사용해서 이더를 보낼 수도 있습니다. delegatecall은 지원하지 않음
    • 예) otherContract.call.value(1 ether)(“methodToCall”, “param1”);

 

전역 변수

  • block – 블록과 관련된 정보 제공
    • block.coinbase
    • block.difficulty
    • block.gaslimit
    • block.number
    • block.timestamp
  • msg – 트랜잭션 sender와 트랜잭션 데이터 관련 정보 제공
    • msg.data
      • 실행될 함수-해시의 첫 번째 4바이트를 포함
      • 함수 인자들이 뒤 따라옴
    • msg.sender
    • msg.sig
      • 함수 시그니처의 첫 번째 4바이트, bytes4 타입
    • msg.value
      • 전송되는 이더 금액
  • now
    • 현재 블록 타임스탬프, block.timestamp와 같음
    • current block timestamp
  • tx – 트랜잭션 관련 정보
    • tx.gasprice
    • tx.origin
      • 트랜잭션을 시작한 EOA 주소

 

전역 함수

  • blockhash(uint blockNumber) returns (bytes32)
    • 현재 블록을 제외하고, 가장 최근의 256 블록에 대해서만 동작함
  • gasleft() returns (uint256)
    • 트랜잭션에 대해 남아 있는 가스양
  • abi.encode(…) returns (bytes)
    • 주어진 인자들을 abi 인코딩 함
  • abi.encodePacked(…) returns (bytes)
    • 주어진 크기에 따라 pack해서 abi 인코딩 함
  • abi.encodeWithSelector(bytes4 selector, …) returns (bytes)
    • 함수 selector를 추구하고 인자들을 abi 인코딩 함
  • abi.encodeWithSignature(string signature, …) returns (bytes)
    • abi.encodeWithSelector(bytes4(keccak256(signature), …)와 같음
  • keccak256(bytes memory) returns (bytes32)
  • sha256(bytes memory) returns (bytes32)
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
    • 공개키를 가지고 주소를 구함
  • require, assert
    • 유효성을 검증하는 함수입니다.
      • require는 주로 선행조건과 후행조건 체크에 assert는 실행 중에 체크해야 할 조건과 invariant에 사용합니다.
      • 유효하지 않으면 예외를 발생시키고 컨트랙트 상태를 되돌립니다.
  • revert
    • 함수 실행을 종료하고 컨트랙트 상태를 명시적으로 되돌립니다.
  • selfdestruct(address recipient)
    • 배포된 컨트랙트 인스턴스를 소멸
    • 남은 잔고와 소멸로 부터 주어지는 보상을 받을 수 있음

 

this

  • 현재 계약 타입을 얻기 위해 계약 내에서 사용
  • address(this)를 사용해서 주소 타입으로 형 변환

 

Import

  • import “HelloWorld.sol”;
  • import “openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol”;
    • 로컬 라이브러리로 부터 임포트
  • import “http://github.com/OpenZeppelin/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol”;
  • import * as MyContract from “BaseContract.sol”;
    • import “BaseContract.sol” as MyContract;
  • import {symbol1 as alias, symbol2} from ” BaseContract.sol”;

import 경로는 절대경로와 상대경로를 사용할 수 있습니다.

 

이벤트

  • 블록체인 상에 트랜잭션 로그로 기록되어짐
  • indexed 인자는 topic 부분으로 저장됨
    • topic은 오프체인 클라이언트 사이드 애플리케이션에서 빠르게 이벤트를 필터하기 위해 사용되어집니다.
    • topic은 다음과 같은 방법으로 생성된 32바이트 한 단어 만을 갖습니다.
      • keccak(EVENT_NAME+”(“+EVENT_ARGS.map(canonical_type_of).join(“,”)+”)”)
  • non-indexed 인자는 로그의 데이터 부분으로 저장됨

Interface, 추상 컨트랙트, 상속

  • Interface
    • 객체지향 프로그래밍 언어의 인터페이스와 유사함
    • 다른 인터페이스를 확장할 수 없음
    • 인터페이스 안에 열거형, 구조체와 같은 사용자 정의 데이터타입을 정의할 수 있음
      • 상태 변수나 생성자는 가질 수 없음
    • 모든 함수는 external
  • 추상 컨트랙트
    • 객체지향 프로그래밍의 추상 클래스와 유사
  • 상속
    • is 키워드 사용
    • 다중 상속 지원
    • modifier를 사용해서 제약을 강화하는 쪽으로 함수 재정의 가능
About the Author
(주)뉴테크프라임 대표 김현남입니다. 저에 대해 좀 더 알기를 원하시는 분은 아래 링크를 참조하세요. http://www.umlcert.com/kimhn/

Leave a Reply

*