[korean]OpenZeppelin 컨트랙트에 연결하기(Linking OpenZeppelin contracts)

OpenZeppelin 컨트랙트에 연결하기

첫번째 튜토리얼에서, 새로운 OpenZeppelin SDK 프로젝트 설정하는 법과, 간단한 컨트랙트를 배포하는 법과, 업그레이드하는 법을 배웠습니다. 이제, 다양한 컨트렉트를 이용해서 빌드해 보고, OpenZeppelin 컨트랙트 패키지를 활용하는 흥미로운 일을 해 보겠습니다. 이더리움 패키지 연결 , 업그레이드가능한 컨트랙트 작성하기 에 대해 배워보겠습니다.

우리가 빌드 할 것

TokenExchange 컨트랙트를 작성해 보겠습니다. 이 컨트랙트는 사용자가 이더리움으로 ERC20 토큰을, 고정된 교환비율로 구매할 수 있도록 합니다. 이를 위해, TokenExchange 컨트랙트 뿐만아니라, ERC20도 구현 해야 합니다. 시작 해 보기전에, 여기에 설명된것과 같이 새 프로젝트가 초기화 되어있어야 합니다.

npm install --global @openzeppelin/cli ganache-cli 
mkdir token-exchange && cd token-exchange 
npm init -y 
openzeppelin init

Note Github repo에서 이 프로젝트의 전체 코드를 이용할 수 있습니다.

Contracts Ethereum Package에 연결하기

첫번째로, ERC20 token을 직접 얻어보겠습니다. 처음부터 코딩하는 대신, OpenZeppelin Contracts Ethereum Package에서 제공하는것을 이용해 보겠습니다. 이더 리움 패키지는 OpenZeppelin SDK 프로젝트에 쉽게 포함되도록 설정된 계약 세트이며, 계약 코드가 이미 _이더리움 네트워크에 배포되어 있다_는 추가 보너스가 있습니다. 이는보다 안전한 코드 배포 메커니즘이며 배포시 가스를 절약하는 데 도움이됩니다.

Note 이 문서를 통해 이더리움 패키지에 대해 더 알아 보세요.

To link the OpenZeppelin Contracts Ethereum Package into your project, simply run the following:

openzeppelin link @openzeppelin/contracts-ethereum-package

이 명령어로 이더리움 패키지를 다운로드 하고(번들링된 일반 npm package), 여러분의 OpenZeppelin 프로젝트에 연결하세요. 이제 모든 OpenZeppelin 컨트랙트를 처리 할 수있게되었으므로, ERC20 토큰을 만들어 보겠습니다!

Note 단순히 @openzeppelin/contracts가 아닌 @openzeppelin/contracts-ethereum-package 를 설치 해 주세요. 전자는 일반적인 용도로 설정되는 반면 @openzeppelin/contracts-ethereum-package는 OpenZeppelin SDK와 함께 사용하도록 맞춤화되었습니다. 즉, 이 말은 컨트랙트가 이미 업그레이드 가능하도록 설정된 상태라는 의미 입니다.

ERC20 토큰 만들기

ERC20 토큰 컨트랙트를 우리의 개발 네트워크에 배포해 보겠습니다. 가나슈 인스턴스가 실행중인 상태인지 확인 하거나, 다음을 실행하여 시작해 주세요:

ganache-cli --deterministic

토큰을 설정하기 위해, OpenZeppelin 패키지에서 제공하는 StandaloneERC20 implementation을 사용합니다. 토큰 메타 데이터 (이름, 기호 및 소수)로 인스턴스를 초기화하고, 계정 중 하나에 대한 초기 공급량을 증가시키겠습니다.

Note 사용가능한 계정들은, 가나슈 프로세스를 시작할때 보이게 됩니다. Your available accounts are shown by ganache when you start the process. 지시에 따라 --deterministic 플래그를 사용하여 실행 한 경우 첫 번째 및 기본 계정은 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1입니다.

$ openzeppelin create 
? Pick a contract to instantiate: @openzeppelin/contracts-ethereum-package/StandaloneERC20 
? Pick a network: development 
✓ Deploying @openzeppelin/contracts-ethereum-package dependency to network 
? Call a function to initialize the instance after creating it?: Yes 
? Select which function: * initialize(name: string, symbol: string, decimals: uint8, initialSupply: uint256, initialHolder: address, minters: address[], pausers: address[]) 
? name (string): MyToken 
? symbol (string): MYT 
? decimals (uint8): 18 
? initialSupply (uint256): 100e18 
? initialHolder (address): 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1 
? minters (address[]): 
? pausers (address[]): 
✓ Setting everything up to create contract instances 
✓ Instance created at 0x2612Af3A521c2df9EAF28422Ca335b04AdF3ac66

우리가 위 명령어에서 어떤것을 했는지 분석해보겟습니다. 먼저 이전에 연결 한 @openzeppelin/contracts-ethereum-package 패키지에서 StandaloneERC20컨트랙트의 인스턴스를 생성하고, 로컬 개발 네트워크에서 생성하기로 결정했습니다. 그런 다음 토큰을 설정하는 데 필요한 초기 값으로 CLI를 _초기화_하도록 CLI에 지시합니다. 이를 위해서는 적절한 초기화` 함수를 선택하고 필요한 모든 인수를 입력해야합니다. OpenZeppelin SDK는 단일 트랜잭션에서 새로운 인스턴스를 자동으로 생성하고 초기화합니다.

우리의 개발 네트워크에서 동작하는 ERC20 토큰 컨트랙트를 가지게 되었습니다. balance 명령어로, 초기공급량이 제대로 할당 되었는지 확인가능합니다. ERC20 토큰 인스턴스가 작성된 주소를 사용해야합니다.

$ openzeppelin balance --erc20 0x2612Af3A521c2df9EAF28422Ca335b04AdF3ac66 
? Enter an address to query its balance: 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1 
? Pick a network: development 
Balance: 100 MYT

좋아요! 이제 교환 컨트랙트를 작성하고, 이를 배포 할 때 이 토큰에 연결할 수 있습니다.

교환 컨트랙트 작성

ETH를 받을 때마다 일정량의 토큰을 전송하기 위해, 우리의 교환 컨트랙트는 토큰 컨트랙트의 주소와 교환비율을 해당 상태에에 저장해야합니다. openzeppelin create로 인스턴스를 만들 때 초기화하는 동안이 두 값을 설정합니다.

컨트랙트의 업그레드를 지원하기위해, OpenZeppelin SDK 는 [솔리디티의 constructor의 사용을 허락하지 않습니다.](https://github.com/skqksh/openzeppelin-sdk/blob/master/packages/docs/modules/ROOT/pages/pattern.adoc#the-constructor-caveat). 대신에, initializers 를 이용할 필요가 있습니다. 이니셜라이저(initializer) 는 솔리디티의 일반 함수 이며, 한 번만 호출 할 수 있도록 추가 확인 되어야 합니다. 코딩 초기화 프로그램을 쉽게 만들기 위해 OpenZeppelin SDK는이를 처리하는 초기화 프로그램 수정자를 포함하는 기본 Initializable 컨트랙트를 제공합니다. 먼저 해당 컨트랙트를 제공하는 패키지를 설치해야합니다.

npm install @openzeppelin/upgrades

이제 _이니셜라이저_를 사용하여 초기 상태를 설정하는 Exchange 계약을 contracts/TokenExchange.sol에 작성해 보겠습니다.

pragma solidity ^0.5.0;

// Import base Initializable contract
import "@openzeppelin/upgrades/contracts/Initializable.sol";

// Import interface and library from OpenZeppelin contracts
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";

contract TokenExchange is Initializable {
  using SafeMath for uint256;

  // Contract state: exchange rate and token
  uint256 public rate;
  IERC20 public token;

  // Initializer function (replaces constructor)
  function initialize(uint256 _rate, IERC20 _token) public initializer {
    rate = _rate;
    token = _token;
  }

  // Send tokens back to the sender using predefined exchange rate
  function() external payable {
    uint256 tokens = msg.value.mul(rate);
    token.transfer(msg.sender, tokens);
  }
} 

initialize 메소드에서 이니셜라이저 수정자의 사용법에 유의하십시오. 이를 통해 컨트랙트를 배포 한 후에는 아무도 해당 기능을 다시 호출하여 토큰이나 비율을 변경할 수 없습니다

이제 새로운 TokenExchange 컨트랙트를 작성하고 초기화하겠습니다

$ openzeppelin create
✓ Compiled contracts with solc 0.5.9 (commit.e560f70d)
? Pick a contract to instantiate: TokenExchange
? Pick a network: development
✓ Contract TokenExchange deployed
? Call a function to initialize the instance after creating it?: Yes
? Select which function: initialize(_rate: uint256, _token: address)
? _rate (uint256): 10
? _token (address): 0x2612Af3A521c2df9EAF28422Ca335b04AdF3ac66
Instance created at 0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec

교환이 거의 준비되었습니다! 단지 자금을 조달하면되므로 구매자에게 토큰을 보낼 수 있습니다. send-tx 명령을 사용하여 전체 토큰 잔액을 계정에서 교환 컨트랙트로 전송 하세요. 수령자의 주소는 이전 명령에서 얻은 TokenExchange 주소로 바꿔 주세요.

$ openzeppelin send-tx
? Pick a network: development
? Choose an instance: StandaloneERC20 at 0x2612Af3A521c2df9EAF28422Ca335b04AdF3ac66
? Select which function: transfer(to: address, value: uint256)
? to (address): 0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec
? value (uint256): 10e18
Transaction successful: 0x5863c8a8e122fcda7c6234abc6e60fad3f5a8108a3f88e2d8a956b63dbc222c2
Events emitted: 
 - Transfer
    from: 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1, 
    to: 0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec, 
    value: 10000000000000000000

모든 설정이 완료되었습니다! 우리의 새 토큰을 가지고 놀 수 있습니다.

우리의 교환 이용하기

교환 컨트랙트를 초기화하고 자금으로 시드 했으므로, 토큰을 구매하여 테스트 할 수 있습니다. 우리의 교환 컨트랙트는, ETH를 보낼 때 자동으로 토큰을 돌려 보내므로 openzeppelin transfer 명령을 사용하여 테스트하십시오. 이 명령을 사용하면 모든 주소로 자금을 보낼 수 있습니다. 이 경우, 이를 사용하여 ETH를 TokenExchange 인스턴스로 보냅니다.

$ openzeppelin transfer
? Pick a network: development
? Choose the account to send transactions from: (1) 0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0
? Enter the receiver account: 0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec
? Enter an amount to transfer 0.1 ether
✓ Funds sent. Transaction hash: 0xc85a8caa161110ba7f08134f4496a995968a5aff7ae60ad9b6ce1c824e13cacb

Note 수신자 계정을 TokenExchange가 작성된 해당 주소로 바꾸십시오.

이제 openzeppelin 잔액을 다시 사용하여 구매 한 주소의 토큰 잔액을 확인할 수 있습니다. 0.1 ETH를 보냈고 1:10 환율을 사용 했으므로 1 MYT (MyToken)의 잔액이 표시됩니다.

$ openzeppelin balance --erc20 0x5f8e26fAcC23FA4cbd87b8d9Dbbd33D5047abDE1
? Enter an address to query its balance: 0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0
? Pick a network: development
Balance: 1 MYT

성공입니다! 우리는 토큰을 교환하여 ETH를 수집하고 교환을 시작했습니다. 그러나 우리는 어떻게 우리가 번 돈을 모을 수 있을까요…​?

교환 컨트랙트 업그레이드 하기

토큰 교환 계약에서 자금을 인출하는 방법을 추가하는 것을 잊었습니다! 이것은 일반적으로 자금이 영원히 잠겨 있음을 의미하지만, OpenZeppelin SDK와의 계약을 업그레이드하여 자금을 모을 수있는 방법을 추가 할 수 있습니다.

Note 계약을 업그레이드하는 것은 버그를 수정하거나 누락 된 기능을 추가해야하는 이와 같은 상황에서 확실히 유용하지만, 여전히 게임 규칙을 변경하는 데 사용될 수 있습니다. 예를 들어, 토큰 교환 컨트랙트를 업그레이드하여 언제든지 교환 비율을 변경할 수 있습니다. 이 때문에 적절한 업그레이드 거버넌스 메커니즘을 갖추는 것이 중요합니다.

소유자가 호출 할 수 있는 인출 방법을 추가하도록 TokenExchange 계약을 수정하겠습니다.

contract TokenExchange is Initializable {
  uint256 public rate;
  IERC20 public token;
  address public owner;

  function withdraw() public {
    require(msg.sender == owner, "Address not allowed to call this function");
    msg.sender.transfer(address(this).balance);
  }

  // (existing functions not shown here for brevity)
}

컨트랙트를 수정할 때 다른 변수 에, 소유자 변수를 추가해야합니다 (여기서 이 제한 사항을 볼 수 있음). 잊어 버려도 걱정하지 마십시오. CLI는 업그레이드를 시도 할 때 이를 확인합니다.

Note OpenZeppelin Contracts에 익숙하다면 왜 Ownable에서 단순히 확장하지 않고 onlyOwner 수정자를 사용했는지 궁금 할 것입니다. 사실 OpenZeppelin SDK는 컨트랙트의 extend 수정을 지원하지 않습니다 (자체 상태 변수를 선언 한 경우). 이 작업을 시도하면 CLI가 다시 경고합니다. 자세한 내용은 여기를 참조하십시오.

이제 오직 잊은것은 컨트랙트의 소유자설정 하는 것 입니다. 이를 위해 업그레이드 할 때, 한 번만 호출 할 수 있는, 다른 함수를 추가하겠습니다.

contract TokenExchange is Initializable {
  uint256 public rate;
  IERC20 public token;
  address public owner;

  function withdraw() public {
    require(msg.sender == owner, "Address not allowed to call this function");
    msg.sender.transfer(address(this).balance);
  }

  // To be run during upgrade, ensuring it can never be called again
  function setOwner(address _owner) public {
    require(owner == address(0), "Owner already set, cannot modify!");
    owner = _owner;
  }

  // (existing functions not shown here for brevity)
}

이제 토큰 교환 컨트랙트를 새 버전으로 업그레이드 할 수 있습니다. 업그레이드 프로세스 중에 setOwner를 호출합니다. OpenZeppelin SDK는 단일 트랜잭션에서 업그레이드와 호출을 모두 처리합니다.

$ openzeppelin upgrade
? Pick a network: development
✓ Compiled contracts with solc 0.5.9 (commit.e560f70d)
- New variable 'address owner' was added in contract TokenExchange in contracts/TokenExchange.sol:1 at the end of the contract.
✓ Contract TokenExchange deployed
? Which proxies would you like to upgrade?: Choose by name
? Pick a contract to upgrade: TokenExchange
? Call a function on the instance after upgrading it?: Yes
? Select which function: setOwner(_owner: address)
? _owner (address): 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1
✓ Instance upgraded at 0x26b4AFb60d6C903165150C6F0AA14F8016bE4aec.

되었어요! 이제 기본 주소에서 withdraw을 호출하여 교환 컨트랙트로 전송 된 모든 ETH를 인출 할 수 있습니다.

$ openzeppelin send-tx
? Pick a network: development
? Pick an instance: TokenExchange at 0xD86C8F0327494034F60e25074420BcCF560D5610
? Select which function: withdraw()
✓ Transaction successful. Transaction hash: 0xc9fb0d3ada96ec4c67c1c8f1569f9cfaf0ff0f7b241e172b32a023b1763ab7ab

Note 이더리움 패키지에서 계약을 업그레이드 할 수도 있습니다.@openzeppelin/contracts-ethereum-package의 새로운 릴리스에서 최신 수정 사항을 포함하도록 ERC20을 업데이트하려면 openzeppelin을 새 버전으로 링크하고 openzeppelin upgrade를 사용하여 인스턴스를 최신 코드로 가져 오십시오.

마무리

이 튜토리얼에서 더 복잡한 설정을 구축했으며 그 과정에서 몇 가지 개념을 배웠습니다. 우리는 프로젝트에 대한 종속성으로 Ethereum Packages를 도입하여 적은 노력으로 새로운 토큰을 만들 수 있습니다. 또한 생성자 대신 ref:writing-contracts.adoc#initializers[초기화 메서드]나 소스 코드를 수정할 때 스토리지 레이아웃을 유지하는 등 OpenZeppelin SDK가 사용하는 업그레이드 패턴의 몇 가지 제한 사항을 제시했습니다. 또한 컨트랙트를 업그레이드 할 때 마이그레이션으로 기능을 실행하는 방법도 배웠습니다.

사이트의 나머지 가이드를 자유롭게 탐색하여 계속 학습하거나 OpenZeppelin SDK로 바로 코딩을 시작하십시오!

2 Likes