[Mastering Ethereum] 7. Smart Contracts & Solidity -(2)솔리디티 문법

Woojin Jeong
10 min readFeb 25, 2022

--

이 글은 Mastering Ethereum, by Andreas M. Antonopoulos, Gavin Wood를 참고 및 재구성하여 작성하였습니다.

(1)편에서 스마트 컨트랙트 작성 언어인 솔리디티에 대해 간단하게 살펴보았습니다. (2)편에서는 컨트랙트를 작성하기 위한 솔리디티의 기본 문법에 대해 알아보고자 합니다. 책에 수록된 Faucet.sol 예제 시리즈를 solidity v0.8.11에 맞추어 수정하여 사용하였습니다.

함수의 visibility 키워드

솔리디티에서는 함수의 접근성을 구체화하기 위해서 아래와 같은 키워드를 사용하고 있습니다. 이러한 키워드를 visibility 키워드라고 하며, 함수의 인풋과 반환 사이에 작성해줍니다.

  • public: 접근에 제한이 없는 함수에 쓰임. 이 키워드로 정의된 함수는 컨트랙트 내부/외부 모두에서 호출 가능 (디폴트)
  • external: 컨트랙트 외부에서만 호출 가능하며, 컨트랙트 내부에서 사용할 경우 ‘this’를 사용하여 접근해야 함.
  • internal: 컨트랙트 내부 또는 상속된 컨트랙트에서 호출 가능
  • private: 컨트랙트 내부에서만 호출 가능한 함수를 만들 때 사용
  • constant or view: 함수의 어떠한 상태 변화도 일으키지 않을 때 사용
  • pure: 스토리지에서 어떠한 변수들도 읽거나 쓰지 않는 함수를 정의할 때 사용
  • payable: 입금을 허용하는 함수에 사용. payable로 선언되지 않으면 입금된 돈을 받을 수 없음.

컨트랙트 생성 및 소멸

먼저, 컨트랙트를 생성할 때에는 line 11에서처럼 생성자를 작성하여 컨트랙트의 상태를 초기화해야 합니다. 생성자는 constructor()로 표기하며, 컨트랙트 이름과 동일한 함수 명칭을 사용하면 안됩니다. (v 0.4.22부터 반영된 내용)

한편, (1)편에서 잠깐 언급되었던 SELFDESTRUCT 연산코드를 통해 생성한 컨트랙트를 삭제할 수 있습니다. 이 연산코드는 남은 잔액을 돌려받을 계정인 owner 변수를 인자로 갖습니다. 아래 코드의 line 21에서 보다시피, 컨트랙트를 생성한 계정에서만 selfdestruct 함수를 호출할 수 있기 때문에, destroy 함수에서는 owner가 함수 호출한 자가 맞는지 확인(line 20)하고, 컨트랙트 삭제 후 남은 잔액은 owner 계정으로 전송합니다.

Faucet3.sol

함수 제어자(modifier)

제어자는 컨트랙트 내에서 함수에 적용되어야 할 여러 조건을 생성하기 위해 사용됩니다. 예를 들어, 위의 코드 line 19~22를 제어자를 사용하여 아래와 같이 바꿀 수 있습니다. 쉽게 말해서, 제어자는 함수가 실행되기 전에 요구조건을 만족하는지 확인해주는 역할을 합니다.

컨트랙트 상속(inheritance)

컨트랙트를 사용할 때는 is 키워드를 사용합니다. 다른 언어에서의 상속 개념과 유사하며, 상속을 받으려 하는 자녀 컨트랙트를 is 앞에 써야 합니다. 상속과 관련된 부분은 Faucet6.sol을 참고하시기 바랍니다.

에러 처리(assert, require, revert)

(1)편에서 소개되었던 트랜잭션의 원자성(atomicity)을 보장하기 위하여 솔리디티에서는 assert, require, revert 등의 함수를 도입하였습니다. assert는 결과가 참일 것으로 예상될 때 사용하고, 필요한 내적 조건들을 만족하는지 확인하는 키워드입니다. require는 위 코드에서도 사용되었듯이 입력값이 설정한 조건에 부합하는지 확인하기 위해 사용합니다. revert는 컨트랙트 실행을 중단하고 실행 전의 상태로 되돌리기 위해 사용하는 키워드입니다.

책에서는 가스 비용이 조금 더 들더라도 에러 메시지를 추가하는 것이 바람직하다고 말하고 있습니다. 솔리디티 0.6부터는 require 안에 에러 메시지를 추가해지는 것이 가능해졌는데, 아래 코드의 37~40 라인에서 이를 확인할 수 있습니다.

Faucet7.sol (Line 28~44)

event

트랜잭션이 실행되면 성공 여부에 관계없이 ‘영수증(receipt)’이 생깁니다. 영수증에는 트랜잭이 실행되면서 생긴 로그 정보들이 포함됩니다. 프론트엔드에 이러한 로그 정보들을 전달해주기 위해 솔리디티는 ‘이벤트’를 활용합니다. 이벤트가 선언되면 컨트랙트는 특정 이벤트가 발생하는지 지켜보다가, 해당 이벤트가 호출되면 트랜잭션의 로그 정보가 기록됩니다. 또한, indexed 키워드를 사용하면 블록들 안에 출력된 이벤트들을 필터링하여 원하는 이벤트만을 검색할 수 있습니다. emit은 트랜잭션 로그에 이벤트 데이터를 포함시키기 위한 키워드입니다.

Faucet8.sol

Faucet 컨트랙트에서는 인출과 입금이라는 두 이벤트를 선언하였고, 각각의 이벤트는 송신인(수신인)과 금액을 인자로 갖습니다. 그리고 인출과 입금이 이루어지는 함수의 마지막에 ‘emit 이벤트’를 실행합니다. 참고로 receive() 함수는 fallback 함수의 일종으로, 하나의 account에는 하나의 receive 함수만 선언될 수 있습니다. receive() external payable은 외부에서 온 이더를 이 컨트랙트가 받아서 조치를 취할 수 있도록 합니다.

catching event

이벤트를 내보내는 컨트랙트를 만들었으면, 이 이벤트를 받는 방법은 Truffle 환경에서 web3.js 라이브러리를 통해 이루어집니다. 트러플은 컨트랙트가 존재하는 디렉토리 안에서 실행되어야 하고, truffle develop 명령어를 입력하면 10개의 accounts와 private key 정보가 임의로 부여됩니다.

truffle develop

트러플 환경에서 컨트랙트를 compile 하기 전에, truffle-config.js 파일을 개인의 설정에 맞게 변경해주어야 합니다. 설정 방법은 여기를 참고하시기 바랍니다.

truffle migrate — network development

이제 명령창에서 web3.js 라이브러리를 통해 로그 정보에 접근해보도록 하겠습니다. 우선 getAccounts 속성으로 계정 정보들을 얻을 수 있습니다.

첫번째 계좌로 1 이더를 입금하는 컨트랙트를 배포하는 과정은 아래와 같습니다. 이 때, toWei 안의 금액은 string 형태로 입력해야 합니다. 출금을 할 때는 send 대신 withdraw 함수를 사용하면 됩니다. 앞서 설명했던대로, 입금이나 출금 이벤트가 발생하면 해당 이벤트를 emit합니다. 로그는 event와 args라는 속성을 가지는데, 첫 로그 정보에 해당하는 logs[0]에 접근하여 발생한 이벤트 이름(logs[0].event)과 인자 정보(logs[0].args)들을 얻을 수 있습니다.

truffle(develop)> Faucet.deployed().then(i => {FaucetDeployed = i})
truffle(develop)> FaucetDeployed.send(web3.utils.toWei('1', "ether")).then(res => { console.log(res.logs[0].event, res.logs[0].args) })

추가적으로, web3.js 라이브러리를 활용하여 각 계좌 또는 해당 컨트랙트 주소의 잔액 정보에도 접근할 수 있습니다.

다른 컨트랙트 호출하기

컨트랙트에서 다른 컨트랙트를 호출할 때에는 new 키워드를 사용하여 새로운 인스턴스를 생성하는 것이 안전합니다. 또한, 주소를 컨트랙트 생성자의 인자로 전달하여 기존 컨트랙트의 인스턴스에 주소를 부여할 수도 있습니다.

_faucet = new Faucet(); //새로운 인스턴스 생성

_faucet = (new Faucet).value(0.5 ether)(); // 선택적으로 0.5 이더 전송

_faucet = Faucet(addr); //주소 부여

Gas

트랜잭션 실행 중 gas limit에 도달하면 다음의 이벤트들이 발생합니다.

  1. out of gas
  2. 실행 전 컨트랙트 상태로 복원
  3. 실행에 사용된 이더는 트랜잭션 수수료로 간주되어 환불되지 않음

가스는 트랜잭션을 호출한 사용자가 지급하는 것이기 때문에, 코드를 짤 때에는 이 가스 비용을 효율적으로 하기 위한 방안을 고려해야 합니다. 일반적으로 가스를 절약하기 위해 동적 크기 배열은 피하며, 다른 컨트랙트 호출은 자제하는 방안을 사용합니다.

Gas cost estimation

컨트랙트를 실행하며 함수를 호출하기 위한 가스 비용을 추정하기 위한 방법으로는 gas_estimates.js를 참고하시길 바랍니다.

gas estimation of Faucet.sol

마무리

7장 (2)편에서는 간단한 스마트 컨트랙트인 Faucet.sol 예제를 통해서 솔리디티 활용법과 이벤트 관련 내용, Truffle를 사용하여 Faucet 컨트랙트에 대한 테스트 트랜잭션을 실행해보는 것, 인스턴스, 함수 호출, 가스와 관련된 내용을 살펴보았습니다. 스마트 컨트랙트를 구성하는 함수들과 이벤트, 이벤트를 받기 위한 web3.js 라이브러리 사용법에 대한 구체적인 내용은 추후에 추가하도록 하겠습니다.

Reference

--

--