3. Solidity(거래)

정예찬·2022년 7월 17일
2

solidity

목록 보기
5/13

본 글은 freeCodeCamp.Org의 Youtube 영상 'Solidity, Blockchain, and Smart Contract Course – Beginner to Expert Python Tutorial'와 관련 코드인 SmartContract의 Github 코드를 기초로 작성되었다.

Youtube 영상 링크: https://www.youtube.com/watch?v=M576WGiDBdQ&t=10336s
Github 코드 링크: https://github.com/smartcontractkit/full-blockchain-solidity-course-py
오늘의 코드: https://github.com/PatrickAlphaC/fund_me

이번 포스팅은 유튜브 영상 02:26:35~03:26:48에 해당하는 내용이다.

오늘 포스팅을 본격적으로 시작하기에 앞서, 오늘 포스팅에 필요한 개념인 '오라클'에 대해 간단히 설명하겠다. 오라클은 블록체인 외부 정보를 블록체인 내부로 들여오는 시스템이다.
예를 한 가지 들어보자.(불법 도박과 관련한 법적 문제는 차치하자.) 금일 트윈스와 타이거즈의 경기 결과를 가지고 S와 Y가 내기를 했다. 트윈스가 이기면 Y의 이더리움(ETH) 지갑에서 1ETH가 S의 이더리움 지갑으로, 타이거즈가 이기면 S의 이더리움 지갑에서 1ETH가 Y의 이더리움 지갑으로 전송되는 스마트 컨트랙트를 체결했다고 하자. 이때 트윈스와 타이거즈의 경기 결과를 블록체인 내부로 가져오는 시스템을 오라클이라고 하고, '두 구단의 경기 결과와 같은 외부 정보를 어떻게 신뢰할 것인가?'와 같은 문제를 오라클 문제라고 한다.
경기 결과를 하나의 사이트에만 의존하여 가져오는 경우를 보자. 그 사이트가 제공하는 경기 결과가 해킹 등으로 조작되어 승패가 뒤바뀌면 엉뚱한 사람이 돈을 잃거나 받게 된다. 반면 30여 개의 사이트에서 최대 다수가 인정한 경기 결과를 가져오는 경우를 보자. 대부분의 사이트가 잘못된 경기 결과를 제공할 확률은 0에 가깝다. 따라서 한두 개의 사이트에서 잘못된 경기 결과를 제공하더라도 스마트 컨트랙트에 경기 결과가 잘못 입력될 일은 사실상 없다. 후자의 경우 탈중앙화된 오라클로 신뢰 문제를 해결했다고 볼 수 있다.
모든 오라클 문제가 방금 사례처럼 단순하지는 않다. 정보의 정량화가 어려울 수도 있고, 정보를 제공하는 주체가 소수일 수도 있다. 모든 오라클 문제를 해결할 수 있는 방안을 도출하기란 불가능하다. 그러나 방금 사례처럼 다수의 주체가 정량화된 데이터를 제공하는 경우에는 탈중앙화된 오라클을 활용하는 게 현재까지는 합리적으로 외부 정보를 블록체인 내부로 들여오는 방안으로 인정받고 있다.

오늘 포스팅에서는 Metamask가 활용된다. 오라클로는 'Chainlink'의 인터페이스가 활용된다. 영상에서는 Kovan Network를 활용하여 실습을 진행했다. 그러나 필자가 확인해보니 영상과는 다르게 Rinkeby Network를 활용하는 방식으로 코드가 바뀌었다. 이유는 모르겠지만 영상 업로드 이후 시점에 코드를 변경한 듯하다. 따라서 영상의 Kovan Network 대신 Rinkeby Network 기반으로 설명을 진행하겠다. 진행 방식에 큰 차이는 없다.
먼저 Rinkeby Network를 활용하기 위해 Rinkeby ETH를 지갑에 받아보자.
https://faucet.rinkeby.io/
먼저 트위터에 로그인 후 위 사이트(Rinkeby에 들어가자. 파란색 tweet 글씨를 클릭하면 아래와 같이 자동으로 글이 작성될 텐데 이때 0x0000...으로 된 주소만 지우고 본인 Metamask지갑의 주소를 지운 자리에 붙여넣어주면 된다.

트윗을 업로드하고 그 트윗의 링크를 아래와 같이 붙여넣어주자. 'Give me Ether'에서 '18.75 Ethers / 3days'를 눌러주면 지갑으로 Rinkeby ETH가 들어온다. 저번 포스팅할 때는 'insufficient funds for gas*price+value'라 뜨면서 안 됐었는데 이번에는 되길래 이유를 고민해보았다. 두 가지 가설이 있는데 하나는 사이트의 오류가 고쳐졌다는 것이고 두 번째는 트윗을 올린 후 3일이 지나야 Rinkeby ETH가 들어온다는 것이다. '18.75 Ethers / 3days'가 처음에는 3일 동안 해당 ETH를 사용할 수 있다는 의미인 줄 알았는데 트윗을 올리고 3일을 기다려야 한다는 의미일 수도 있다.(필자는 일주일 전에 같은 내용의 트윗을 올렸다.) 테스트용 ETH를 받지 못했다면 3일 후에 다시 시도해보자.

Rinkeby ETH를 받으면 아래 사진처럼 Metamask 지갑에서 확인가능하다.(필자는 여러 사이트를 돌며 0.3 Rinkeby ETH를 더 받았다.)

Remix 좌측 상단 Environment를 JavaScriptVM에서 Injected Web3로 변경하자. JavaScriptVM은 블록체인 네트워크에 연결되지 않은 상태의 환경이고 Injected Web3는 블록체인 네트워크에 연결된 환경이다. 따라서 Environment를 변경하면 메타마스크 팝업창이 뜨며 지갑을 연결할 것인지 묻는다. 연결하라. 추후 함수 실행 과정에서 Rinkeby ETH가 Metamask 지갑을 통해 활용되는 모습을 확인할 수 있다.

세팅은 마쳤고 본격적으로 코드 분석에 돌입하자. 오늘 코드가 많은데 하나하나 분석해보자!

pragma solidity >=0.6.6 <0.9.0;

코드로 버전 설정은 이렇게 되어 있는데 좌측 창에서 컴파일러 버전을 0.6.12로 설정해주자. 다른 버전으로 하면 코드를 변경해야 하는 등의 수고로움이 있을 수 있다.

import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";
import "@chainlink/contracts/src/v0.6/vendor/SafeMathChainlink.sol";

오라클이 등장했다. 첫 코드는 chainlink에서 ETH 가격을 가져오기 위해 관련 파일을 import하고 있다. chainlink에서는 ETH 가격 정보 제공 사이트 수십 개를 활용하는 탈중앙화된 오라클을 제공한다.
두 번째 코드는 숫자 비트제한으로 인한 오입력 방지를 위해 import된 파일이다. 예컨대 uint8 변수는 비트 8개로 구성되어 있어 0부터 255까지 2^8개의 숫자를 입력할 수 있다. 정신 없이 코딩하던 중 uint8 변수에 300이라는 숫자를 넣었다고 쳐보자. 표현할 수 없는 숫자가 입력되었기에 컴퓨터가 오류를 감지하여 알려준다. 그러나 복잡한 연산 과정에서는 컴퓨터가 이러한 오류를 잡아내지 못할 때가 있다. SafeMathChainlink를 사용하면 컴퓨터가 감지하지 못하는 오류를 잡아낼 수 있다. 그러나 0.8.0 버전 이상부터는 해당 파일을 import하지 않아도 위에 설명한 오류를 잡아낼 수 있어 해당 코드를 입력하지 않아도 된다.```
코드를 입력하세요


```cpp
contract FundMe {
...
}

먼저 FundMe라는 이름의 계약을 생성하고 내용을 채워보자.

using SafeMathChainlink for uint256;

아까 위에서 import된 파일과 같이 외부에서 import된 파일을 library라고 한다. 'using library명칭 for 대상;'을 입력하면 대상에 대해 해당 library가 적용된다. 위 코드에서는 uint256에 대해 SafeMathChainlink를 적용하고 있다. 이 코드로 uint256에 대한 연산 오류를 감지할 수 있다.

mapping(address => uint256) public addressToAmountFunded;
address[] public funders;
address public owner;

address로 정수를 참조하는 'addressToAmountFunded' mapping이 선언되었다. mapping 이름으로 유추하건대 주소를 입력하면 해당 주소에 얼마나 펀딩이 이루어졌는지 알 수 있도록 하는 mapping이다.
다음으로 funders라는 주소 배열과 owner라는 주소를 각각 선언하고 있다.(funders를 펀딩하는 사람, owner를 펀딩받는 사람으로도 보겠다.) 본 계약의 주 목적은 funders가 owner에게 펀딩을 하고 owner가 펀딩된 자금을 회수하는 것이다. 변수의 선언 형태를 봤을 때 다수의 funders와 1명의 owner가 존재함을 알 수 있다.

constructor() public {
	owner = msg.sender;
}

constructor()라는 새로운 함수가 등장하였다. 이는 생성자로, 객체지향 언어에서 활용된다. 생성자는 계약 내에서 단 1번 작성할 수 있고, deploy 때 호출된다. 위 코드에서는 owner라는 주소에 'msg.sender'라는 값을 입력하고 있다. msg.sender는 해당 계약을 호출한 계정의 주소이다. 본 계약의 경우 아까 연결한 우리 Metamask의 계정 주소이다.

function fund() public payable {
	uint256 minimumUSD = 50 * 10 ** 18;
	require(getConversionRate(msg.value) >= minimumUSD, "You need to spend more ETH!");
	addressToAmountFunded[msg.sender] += msg.value;
	funders.push(msg.sender);
}

펀딩을 위해 fund()라는 함수가 정의되었다. 'payable'이라는 새로운 키워드가 등장했는데, 이 키워드는 ETH를 전송하기 위한 키워드이다. 해당 키워드가 붙어야만 ETH를 전송할 수 있다.
위 코드에서는 minimumUSD에 50*(10^18)를 입력하고 있다. 이는 전송되는 최소 금액을 50 USD로 설정하기 위한 목적인데 뒤에 10^18을 곱한 이유는 ETH의 단위 체계를 통해 알 수 있다.
'1 ETH = 10^9 GWEI = 10^18 WEI'이다. WEI 기준으로 단위를 환산하다 보니 50에 10^18이 곱해진 것이다.
다음으로 require 함수가 등장하였다. require 함수 첫 번째 입력값이 참이 아니면 두 번째 입력값인 메시지와 송금액, 송금수수료를 전부 반환한다. 예를 들어 30달러만큼의 ETH를 펀딩하면 minimumUSD인 50달러보다 작으므로 송금이 취소되고, 메시지와 송금액, 송금수수료가 반환된다.(getConversionRate(uint256 ethAmount)는 추후 설명하겠지만 ETH를 입력하면 그에 해당하는 달러 값으로 변환해주는 함수이다.) 반환 후 함수가 종료된다.
require 함수가 참이면 다음 코드가 실행된다. 다음 코드는 mapping을 활용하여 msg.sender(송금자)의 주소가 참조하는 양의 정수에 msg.value만큼을 더해주고 있다. msg.value는 Remix 좌측 창의 'VALUE'에 입력된 값이다. 즉, 송금자가 얼마를 보냈는지 주소와 송금액을 매핑하여 기록하는 함수이다.
마지막으로 msg.sender, 즉 송금자를 funders 배열에 입력하고 다음 입력 공간이 될 배열 요소를 하나 추가하고 있다.

function getVersion() public view returns (uint256){
	AggregatorV3Interface priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
	return priceFeed.version();
}

다음으로 우리가 사용하는 AggregatorV3Interface라는 library의 버전을 확인하는 getVersion() 함수를 보자. AggregatorV3Interface(주소)는 입력된 주소에 해당하는 인터페이스이다. 이 인터페이스 안에는 여러 정보가 있는데, 해당 인터페이스의 version 또한 그 정보 중 하나이다. 0x8A... 이 주소는 아래 링크에서 가져온 것으로, 이더리움과 USD의 가격 비율, 즉 (ETH / USD)에 해당하는 주소이다.
https://docs.chain.link/docs/ethereum-addresses/
코드에서는 (ETH / USD)를 포함하는 인터페이스를 priceFeed라는 변수에 저장하고 있다.
그 후 priceFeed 인터페이스에 포함된 version을 호출하여 해당 인터페이스의 버전을 가져오고 있다. 어떤 대상의 내부 변수/함수를 불러올 때는 '대상.내부 변수/함수' 형태로 호출한다. priceFeed.version()은 priceFeed 내부의 version 함수를 호출하는 선언이다.

function getPrice() public view returns(uint256){
	AggregatorV3Interface priceFeed = AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
	(,int256 answer,,,) = priceFeed.latestRoundData();
	return uint256(answer * 10000000000);
}

getPrice() 함수는 1 ETH의 가격을 (USD*10^18)로 반환하는 함수이다.
getVersion과 마찬가지로 priceFeed에 (ETH / USD)를 포함하는 인터페이스를 저장하고 있다.
다음으로 answer에 priceFeed 안의 함수인 latestRoundData()가 반환하는 5가지의 값 중 2번째 값을 answer라는 정수에 저장하고 있다. 쉼표로 구분된 (,int256 answer,,,)의 형태를 통해 어떻게 2번째 값만 저장되는지 유추할 수 있다. 2번째 값을 제외하고 불필요한 나머지 반환값은 빼다 보니 쉼표 사이에 공백이 생겼다. answer에 저장된 값은 1ETH에 10^8을 곱한 값을 USD로 환산한 값이다. 따라서 최종 값을 반환할 때는 단위를 맞추어주기 위해 10^10을 answer에 곱하고 있다.

function getConversionRate(uint256 ethAmount) public view returns (uint256){
	uint256 ethPrice = getPrice();
	uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
	return ethAmountInUsd;
}

다음으로 ETH를 입력하면 그만큼에 해당하는 USD를 반환하는 getConversionRate 함수를 살펴보자.
getPrice()함수를 통해 1ETH에 해당하는 (USD*10^18) 값을 불러와 이를 변수 ethPrice에 저장하고 있다.
그리고 ethPrice와 ethAmount(입력된 ETH의 양)을 곱해 10^18로 나누어 이를 ethAmountInUsd 변수에 저장하고 있다. ethPrice와 ethAmount에 모두 10^18이 곱해져있으므로 단위를 맞추어주기 위해 두 변수의 곱을 10^18로 나누어준 것이다.
ethAmountInUsd를 반환하며 함수는 종료된다.

modifier onlyOwner {
	require(msg.sender == owner);
	_;
}

modifier는 solidity 언어의 특징적인 함수이다. modifier를 함수의 선언에 추가함으로써 modifier 내부의 내용을 함수 실행 전후로 실행할 수 있다. 위의 예시에서는 onlyOwner라는 modifier가 정의되었는데, 함수에 onlyOwner라는 키워드를 추가하면 modifier 안의 require함수가 실행되고 그 후 해당 함수가 실행된다. '_;'의 위치가 해당 함수의 실행 시점을 결정한다. 위 코드에서는 require 함수 뒤에 등장하여 require 함수 이후에 해당 함수가 실행되도록 한다. 만약 코드가 아래와 같이 작성되었다면 해당 함수 실행 이후 require 함수가 실행된다.

modifier onlyOwner {
	_;
	require(msg.sender == owner);
}
function withdraw() payable onlyOwner public {
	msg.sender.transfer(address(this).balance);
	for (uint256 funderIndex=0; funderIndex < funders.length; funderIndex++){
		address funder = funders[funderIndex];
		addressToAmountFunded[funder] = 0;
	}
	funders = new address[](0);
}

마지막으로 withdraw() 함수를 살펴보자. 이 함수는 펀딩 받은 자금을 owner의 계좌로 회수하는 함수이다. 먼저 함수 선언을 살표보자. payable 선언이 붙어 송금이 가능해졌음을 알 수 있다. onlyOwner라는 modifier가 붙어 withdraw() 함수 실행 전 msg.sender와 owner, 즉 전송자와 owner가 일치하는지 검증이 이루어진다. '대상1 == 대상2'은 논리 연산자로 대상1과 대상2가 일치하면 참, 일치하지 않으면 거짓을 반환한다.
검증이 통과되면 msg.sender에게 지금까지 받은 펀딩이 모두 전송된다. 'this'는 solidity만의 키워드로, address(this)는 본 계약의 주소를 반환한다. '주소.balance'는 해당 주소의 잔고를 의미한다. '주소.transfer(값)'은 값을 주소로 전송한다. 따라서 'msg.sender.transfer(address(this).balance);'는 본 계약의 주소에 저장된 금액을 msg.sender에게 송금한다.
for문이 등장하였다. for문은 반복문이다. for문은 내용을 조건식이 만족되는 동안 반복한다. 반복의 조건을 설정하기 위해 for문의 괄호에는 초기화식, 조건식, 증감식이 순서대로 들어간다. 예시에서는 'uint256 funderIndex=0'이 초기화식, 'funderIndex < funders.length'가 조건식, funderIndex++가 증감식이다. for문이 처음 실행될 때는 초기화식에 따라 변수의 초기화가 이루어지고, for문이 한 차례 진행 종료될 때마다 증감식이 실행된다. for문은 반복을 실행하기 전 조건식이 충족되는지 확인한다. 조건식이 충족되면 for문을 실행하고, 충족되지 않으면 건너뛴다.
조건식을 살펴보자. funderIndex라는 양의 변수를 0으로 초기화하고 있다. for문의 반복 조건은 funderIndex가 funders.length(funders 배열의 길이를 나타냄)보다 작아야 한다는 것이다. for문이 종료될 때마다 funderIndex의 값이 1씩 증가하는 증감식이 실행된다. '변수++'는 해당 문장의 마지막에서 변수의 값을 1 증가시킴.
for문의 내용을 보자. funder라는 주소를 선언하고 k번째 funders 배열 요소(k=1, 2, ..., funders.length)의 주소를 funder에 저장한다. 그 후 funder에 매핑되어 있는 값을 0으로 초기화한다. 즉, 모든 funders에 대해 funders가 송금한 금액을 0으로 초기화한다.
for문 탈출 이후 funders에 주소 배열을 할당하고 이를 0으로 초기화한다. 배열의 대괄호 뒤 소괄호 내 값이 초기화값이다. 위 코드에서는 0이다.

오늘 내용이 많아서 작성하는데 오래 걸렸다. 이제 집에 갈 날이 딱 30일 남았다. 보고 싶은 사람들이 많다. 포스팅 응원해주는 사람들에게 정말 고맙다. 지인들에게 조금씩 블로그의 존재 사실을 알리고 있는데 다들 재밌다고 이야기를 해줘서 힘이 난다. 다음 포스팅부터는 파이썬을 써야 해서 걱정이다. 필자는 파이썬 하나도 몰라서 공부하면서 해야 한다. 그래도 반드시 일주일 2편의 약속은 지키도록 하겠다. 이촌한강공원에서 돗자리 펴놓고 맥주 한 캔 하고 싶은 밤이다. 그럴 날이 얼마 안 남았다!!

Github Code License:

MIT License

Copyright (c) 2021 SmartContract

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
profile
BlockChain Researcher

0개의 댓글