앞서 몇번 다루었던 내용이다.
간단하게 하면 이더리움 전용 가상머신으로 공통된 환경에서 실행 가능하게 해주는 도구이다.
🔨 설치
mac OS를 기준이다.
brew update
brew tap ethereum/ethereum
brew install solidity
버전확인
solc --version
간단한 solidity를 만들어 보자
pragma solidity >=0.4.15 <0.9.0;
contract SimpleStorage{
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData
}
}
입력값을 받고 해당값을 통해서 변수를 바꾸어 주고 return할떄에는 변수값을 return해주는 간단한 코드이다.
이후 코드를 컴파일 해주어야 한다.
solc --optimize --bin simpleStorage.sol
을 통해 간단하게 컴파일 가능하다.
solc --bin 이름
은 솔리디티 파일을 이진형식으로 컴파일 하라는 명령어
--optimize
컴파일 전 작성한 솔리디티 코드가 약 200회 실행된다고 가정하였을떄의 기준으로 최적화 하는 것이다.
이렇게 실행시키면 16진수의 이진코드가 출력이 된다.
ABI는 스마트 컨트랙트 코드에 대한 설명이 담긴 JSON형식의 인터페이스이다.
이더리움 생태계의 노드들은 지갑을 통해 상호작용 하게 되는데 이때 JSON-RPC형식의 데이터로 상호작용을 하게 되고 이 상호작용을 위한 데이터가 바로 ABI이다.
solc --abi simpleStorage.sol
를 입력하면 ABI가 만들어 지며 객체 형식으로 만들어 지게 된다.
이더리움 스마트 콘트랙트 코드를 작성하는 데에는 다양한 IDE가 있다.
우리가 지금까지 사용한 VsCode는 데스크톱 IDE로, solidity언어 익스텐션을 설치하여 컴파일 가능하다.
우리가 일반적으로 알고 있는 데스크톱 IDE에는 자체적으로 솔리디티 코드를 디버깅하거나 컴파일하는 기능이 없다.
웹 기반 IDE인 Remix는 조금 다르다.
자체적으로 디버깅, 배포, 테스트 등을 내장하고 있기 떄문에 현재로써는 데스크톱 IDE보다 편하다고 할 수가 있다.
이쪽을 통해 접속을 해보자.
왼쪽부터 순서대로
파일 익스플로러
, 컴파일러
, 배포 및 트랜잭션 실행
, 플러그인 매니저 탭
이 구성되어 있다.
[1] 파일 익스플로러
새로운 파일, 폴더, 깃허브 연동, 파일 업로드등을 할 수가 있다.
[2] 솔리디티 컴파일러
작선항 컨트랙트 코드를 컴파일 한다.
[3] 배포 및 트랜잭션 실행
컴파일한 코드를 배포하고, 배포한 컨트랙트를 실행한다.
[4] 플러그인 매니저
컨트랙트 개발에 필요한 모듈을 설치 및 관리한다.
앞서 VsCode에서 작성했던 코드와 같은 코드를 작성하고 컴파일을 해보자.
[1] COMPILER
컴파일 버전을 지정할 수가 있다.
[2] LANGUAGE
나는 솔리디티를 사용할 것이기 떄문에 솔리디티를 선택하였다.
[3] EVM VERSION
EVM버전은 버전마다 다른 특징을 가지고 있다.
- byzantium이 디폴트 값이다.
문제가 없다면 컴파일이 잘 완료가 될 것이다.
컴파일이 완료가 되면 아래에 Compilation Details
버튼을 통해서 컴파일에 대한 정보를 알 수가 있고 값또한 복사가 가능하다.
그럼 이제 3번쨰 탭임 배포 탭을 들어가 보자.
[1] ENVIRONMENT
배포할 네트워크를 말한다.
지갑과 연동하거나 Web3 Provider를 선택하여 Geth를 토해 접속한 네트워크와 연결 가능하다.
우리는 가상 네트워크를 사용해야 하기 떄무네 JavaScript VM을 선택한다.
[2] ACCOUNT
배포할 계정이다.
- 가상 네트워크에서는 자동으로 가상계정이 선택된다.
[3] GAS LIMIT
실행시에 사용할 가스의 한도이다.
[4] VLUE
전송할 ㅇ더 양이다.
[5] CONTRACT
트랜잭션으로 올릴 컨트랙트를 선택한다.
- 나 같은 경우에는 만든 SimpleStorage를 배포한다.
이후 배포를 하게 되면 바로 배포가 이루어지며 간단한 배포 정보가 나오게 된다.
작성한 코드에는 set함수와 get함수가 있다.
set함수를 통해 변수 값을 수정하기 떄문에 set에 3을 담아서 보내면 트랜잭션이 발생을 하고 이후 get을 실행하면 3이 호출이 된다.
Remix IDE는 웹 브라우저에서 작동하는 IDE이기 떄문에, 브라우저의 캐시가 지워지는 경우 작성된 파일도 함께 삭제될 수 있다.
그러기 떄문에 우리는 로컬컴퓨터에 있는 파일 또는 폴더를 Remix IDE와 웹소켓 통신으로 연결해야 한다.
일단 기본적으로 npm install -g @remix-project/remixd
를 설치 해준다.
그후 우리는 연결해주는 작업이 필요하다.
remixd -s 폴더의 절대경로 --remix-ide https://remix.ethereum.org
폴더의 절대경로를 알아내는 방법은 VsCode를 활용하면 된다.
이후 터미널을 통해서 앞서 적은 명령어를 통해 경로를 설정하고 연결을하면
서버 연결이 된 것과 같은 상태가 되게 된다.
그후 다시 remix.ethereum.org에 접속해서
workspaces를 localhost로 바꾸어 주면 끝난다
이렇게 작업을 해놓으면 VsCode를 통해 수정하여도 브라우저 IDE에도 같이 적용이 된다.
일단 기본적으로 MetaMask에 연결되어 있는 상태여야 한다.
이후 배포하는 메뉴에 가서 ENVIRONMENT를 Web3로 바꾸어 준다.
이후 간단하게 연결을 한 뒤에 MetaMask부분에서는 Ropsten테스트 네트워크로 연결이 되어 있는지,
Remix에서 ACCOUNT가 메타마스크 주소와 같은지를 확인해주면 연결이 완료된 것이다.
이 부분에서는 우리가 작성한 컨트랙트 코드를 테스트넷에 배포하는 과정을 진행해 볼 것이다.
https://urclass.codestates.com/cadc7be1-08dd-4be5-b94b-6fc0e95adf95?playlist=995
나의 메타마스크 주소를 입력하면 간단하게 테스트 이더를 주며
이곳에서 나의 주소를 입력하여 트랜잭션을 확인해 볼수가 있다.
이렇게 설정한뒤에 배포를 시작하면 메타마스크에서 확인창이 뜨고 확인을 누르게 된다.
그러면 이후 트랜잭션 정보가 Remix터미널에 보이게 된다.
이후 트랜잭션에서 이더스캔에서 검증 및 등록을 해야하기 떄문에
transaction hash
값을 복사해 두자
이후 다시 이더스캔으로 돌아가 네트워크를 Ropsten으로 바꾼뒤에 해당 해시값을 검색을 하면 거래 내역이 보이게 된다.
거래내역을 보면 TO
부분이 있을 것이다.
그곳을 클릭하여 들어가면 내가 배포한 컨트랙트에 대한 정보가 나오게 된다.
Verify & Publish 링크를 누른다.
single file, 0.8.7버전을 선택하자.
- 내가 이렇게 코드를 작성 하였으니
이후 앞서 작성한 코드를 붙여넣기 해주고 끝내면 된다.
두서없이 막 적기만해서 정리가 안되있을꺼 같아 천천히 다시 정리하면서 적어보고자 한다.
일단 테스트넷에 배포를 하게 된다.
배포를 하게 되면 당연히 트랜잭션이 발생을 하게 될 것이다.
이 트랜잭션은 테스트넷 생태계에 영향을 주는 것일 것이다.
- 테스트이기 떄문에 영향을 준다고 하기는 좀 그렇지만;;
발생하는 트랜잭션을 보면 from과 to가 있다.
from은 나의 메타마스크 테스트넷 주소를 말한다.
transaction hash는 생태계(테스트넷)으로 가는 트랜잭션을 말한다.
여기까지 진행을 하게 된뒤 to트랜잭션 주소를 이더스캔에 검색을 해보면 단순히 트랜잭션 하나만 나오게 될 것이다.
- 왜냐면 따로 지갑이 없기 떄문에
이곳에서도 from과 to를 확인가능하며 from은 마찬가지로 지갑, to는 컨트랙트 자체를 가르킨다.
이후 to를 클릭하여 이동을 하면 컨트랙트가 보일 것이고 이곳에서 Contract태그를 활용하여 나의 소스코드를 배포하는 것이다.
개인적으로는 작성을 하면서 나도 얼추 정리가 되었다!!
나같은 경우에는 솔리디티를 처음사용하는 것이기 떄문에 이 부분에 대해서 많이 파악하고 넘어가야할 필요성이 있다.
주석으로 처리를 하는 부분이다.
코드의 맨 상단에 처리를 하며 필수가 아니다.
이 주석의 목적은 간단하다.
스마트 컨트랙트에 대한 신뢰를 높이고, 저작권과 같은 문제를 해소하기 위해 라이센스를 명시해야 하는 것이다.
SPDX-License-Identifier: MIT
이런식으로 작성을 한다.
라이센스 리스트는 https://spdx.org/licenses/ 에서 확인 가능하다
특정 컴파일러의 버전을 표기할 떄 사용한다.
이 부분은 필수이며 모든 소스코드 파일에 있어야 한다.
특정 버전을 사용할떄에는 파일 최상단에 다음과 같이 작성을 한다.
pragma solidity 0.8.7; // 0.8.7버전을 사용한다.
특정 버전 이상을 사용할떄에는 이렇게 작성을 한다.
pragma solidity ^0.8.7 // 0.8.7 이상의 버전을 사용한다.
이외에도 범위를 할당 할 수도 있다.
pragma solidity >=0.4.16 < 0.9.0;
이 부분은 JS에서 사용하는 방식과 동일하다.
import "파일이름";
import * as hojin from "파일이름"
import "파일이름" as hojin
- 임포트 하는 파일 이름을 hojin으로 사용하겠다.
import {a as hojin, b} from "파일이름"
- 파일이름에서 a와 b만을 사용하는데 a는 hojin이라는 이름으로 사용하겠다.
🔨 상태변수
상태변수는 일반적인 변수를 말한다.
let hojin = 1;
이와 같이 데이터 타입 + 변수명 = 값
으로 구성이 되어 있다.
예를들면
pragma solidity ^0.8.7;
contract Hojin {
uint a = 1; // 상태변수를 선언 및 초기화
uint b; // 상태변수를 선언
}
앞서 말한 상태변수에서 맨 앞부분(uint)이 부분을 말한다.
🔨 [1] 불(bool)
true/ false값을 말한다.
bool isOpen = true;
bool isClosed = false;
🔨 [2] 정수(int, uint)
부호(+,-)가 있는 경우에는 int, 없는 0이상의 값에는 uint를 사용한다.
- 뒤에는 비트 단위를 지정 가능하다.
int16은 -32768 ~ 32767사이의 값을 담을수 있고
uint16은 0 ~ 65535사이의 값을 담을수 있다.
int8 a = -20;
uint8 b = 30;
🔨 고정 바이트 배열
bytes1에서 bytes32까지의 고정된 크기의 배열을 선언한다.
bytes3 alp = 'abc'
alp[0] // 'a'
alp[1] // 'b'
alp[2] // 'c'
🔨 주소
다른 언어와 다르게 주소가 따로 존재를 한다.
- 계정간의 거래를 해야 하기 떄문에 주소가 따로 있다.
0x로 시작을 하게 되면 크기는 20바이트이다.
- 이더리움 지갑의 주소가 20바이트인 이유이다.
주로 개정의 잔액을 반환하는 balance() 함수와 이더를 계정으로 전송하는 transfer() 함수에서 사용이 된다.
address myWallet = 0x10abb5efEcdc01234f7b2384912398798E089Ab2;
중요한점은 0.8버전 이후로 address형식은 송금이 불가능한 주소값이라는 점이다.
이 이후의 버전에서는 **address payable** 형식을 사용해야 한다.
- 이더 송금을 위한 transfer()와 send()함수가 내장되어 있다.
address addr1 = 0x10abb5efEcdc01234f7b2384912398798E089Ab2;
address payable p_addr1 = payable(addr1)
이와 같이 한번 변환을 해주어야 한다.
int 또는 bytes형식의 데이터를 address payalbe로 바꾸기 위해서는 일단 address() 를 사용하여 한번 변환 과정을 거쳐야 한다.
uint16 num = 3;
address addr = address(num)
address payable p_addr = payable(addr)
만약 컨트랙트 자체가 이더를 받을 수 있는 컨트랙트인 경우에도 사용 가능하다.
contract C {
constructor () payable{ }
}
address payable addr = payable(C) // 주소값이 반환된다.
만약 이더값을 받지 못하는 형태의 컨트랙트라면 한번 address로 변환을 해주어야 한다.
contract C {
contructor() { }
}
address addr = address(C)
address payable p_addr = payable(addr)
참조형 변수는 배열과 같이, 데이터를 저장하는 영역에 연속되어 저장되어 있는 값의 첫번쨰 메모리의 주소를 값으로 가지는 변수 타입이다.
🔨 배열
저장하고자 하는 데이터 형식에 []
를 붙여 선언한다.
정적배열과 동적배열 두가지 형태가 있다.
[1] 정적 배열
uint[4] 배열이름
이와 같은 형태로 사용할 배열의 크기를 지정하여 선언한다.
[2] 동적 배열
uint[] 배열이름
이와 같은 형태로 따로 크기를 지정하지 않는다.
🔨 문자열
bytes와 동일하지만, 길이와 push()
는 없다.
string name = 'hojin'
🔨 구조체
구조체(struct)는 서로 다른 유형의 항목을 포함하는 집합으로, 사용자 정의 형식이다.
contract C{
struct UserInfo{
address account;
string lastName;
string first Name;
}
}
구조체를 사용할떄에는 각 항목에 대한 값을 객체 형식으로 추가해야 한다.
contract C {
struct UserInfo {
address account;
string lastName;
string firstName
}
function newUser (address newAddress, string newLastName, string newFirstName){
UserInfo memory newOne = UserInfo({
account : newAddress,
lastName : newLastName,
firstName : newFirstName
});
}
}
🔨 맵핑(mapping)
스토리지 데이터 영역에서 키-값
구조로 데이터를 저장할 떄 사용하는 참조형이다.
mapptin({키 형식} => {값 형식}) {변수명}
mapping(address => int) public userAddress;
솔리디티 언어에 내장되어 있는 변수이며, 각 변수는 다양한 멤버를 가지고 있다.
[1] block
블록에 대한 정보를 가지고 있다.
[2] msg
컨트래트를 시작한 트랜잭션 콜이나 메시지 콜에 대한 정보를 가지고 있다.
[3] tx
트랜잭션 데이터를 가지고 있다.
[4] This
현재 컨트랙트를 참조한다.
- 현재 컨트랙트 주소로 암시적으로 변환이 된다.
🔨 block
[1] blockhash(uint blocknumber)
주어진 블록의 해시를 bytes32 형태로 변환한다.
[2] coinbase
블록의 채굴자 주소(address형식)
[3] gaslimit
블록의 가스 한도(uint형식)
[4] number
블록의 번호(uint형식)
[5] timestamp
블록의 타임스탬프(uint형식)
🔨 msg
[1] gasleft()
남아있는 가스의 양을 반환한다.
[2] data
전체 콜데이터 본문
[3] sender
현재 수행하고 있는 메시지 발신자
[4] gas
남은 가스의 양
[5] value
메시지와 함께 보낸 이더 금액
🔨 tx
[1] gasprice
트랜잭션 가스 비용
[2] origin
트랜잭션 발신자
Java에 비해서 3가지로 나뉘어 진다.
internal(default)
public
private
상태변수를 변하지 않는 상수값으로 선언도 가능하다.
pragma solidity ^0.8.7;
contract C {
uint public constant maxLimit = 1000;
}
function 함수이름(매개변수...) { 함수 내용 }
이와같이 JS에서 활용하듯이 선언이 가능하다.
만약 값을 반환하는 경우에는
function 함수이름(매개변수...) return(반환 타입) {...}
이렇게 return을 추가해 주어야 한다.
return이 없는 함수는 아무런 값도 반환하지 않는다.
🔨 함수 접근 수준
external
public(default)
internal
private
contract C {
function changeName(address account, string newName) internal {...}
function checkGas() private returns (uint) {...}
}
🔨 view, pure
view
pure
pragma solidity ^0.8.7
contract C {
uint256 public constant maxLimit = 1000;
mapping(address => uint) public frozenAccount;
function checkGas(uint256 amount) private pure returns(bool) {
if(amount > maxLimit) return false;
return true;
}
function validateAccount(address account) interval view returns(bool){
if(frozenAccount[account]) return true;
return false;
}
}
간단하다.
최대값을 지정해 두고 mapping을 적어둔다.
첫번쨰 함수는 단순히 들어오는 값이 상수값보다 큰지 작은지를 파악하고
두번쨰 함수는 들어오는 값이 frozenAccount
에 키값으로 있는지를 파악하는 것이다.
🔨 payable
payable을 선언하면 함수에서 이더를 받을 수 있다.
pragma solidity ^0.8.7;
contract C {
function getEther() payalbe returns(bool){
if(msg.value === quoteFee){
...
}
}
}
🔨 생성자 함수(constructor)
생성자 함수를 활용하여 컨트랙트가 생성될떄 컨트랙트의 상태를 초기화할 수 있다.
pragma solidity ^0.8.7;
contract C {
address public account;
constructor(address _account) internal{
account = _account
}
}
컨트랙트가 생성이 될떄 _account
를 받아서 자동으로 account
변수를 초기화하는 것이다.
🔨 함수 변경자(modifier)
함수 선언에 modifier
를 추가하여 함수에 변경자를 적용할 수 있다.
변경자를 작성할 떄에는 _;
를 활용한다.
_
이전의 코드는 함수가 실행되기 전에 실행이 되고int public num = 0;
modifier changeNum{
num++;
_;
num--;
}
function func() public changeNum{
if(num == 1){
...
}
}
이와 같은 코드를 보면 num++은 함수 실행되기 전에 실행이 되고
이후 함수 실행이 종료가 되면 num--가 실행이 되게 된다.
이후 func라는 함수에서 changeNum이라는 변경자를 사용 가능하다.
작동 방식
일단 changeNum으로 인해서 num이 1이 되기 떄문에 func안에 있는 if문이 작동을 한다.
그후 func함수가 종료가 되면 changeNum이 작동을 하여 다시 num의 값을 0으로 되돌린다.
contract
라는 객체는 상속을 지원한다.
is
키워드를 사용하여 지정해준다.contract ChildContract is ParentContract {
...
}
다중 상속 또한 가능하다.
contract ChildContract is ParentContract1, ParentContract2, ParentContract3{
...
}
상속을 하게 되면 부모 컨트랙트에 있는 내용을 자식 컨트랙트에서 사용 가능하다.
에러 처리를 할 떄에는 assert
, require
, revert
함수를 주로 활용한다.
revert
: 해당 함수를 종료하고 에러를 리턴한다.require
, assert
: 설정한 조건이 참인지 확인하고, 조건이 거짓이면 에러를 리턴한다.pragma solidity ^8.7.0
contract C {
function checkFunc(uint amount) public payable{
if(amount > msg.value / 2 ether)
revert ("not enough!!")
}
}
위와 같은 코드는 if문에 해당되면 함수를 종료하고 해당 구문을 return 하는 것이다.
pragma solidity ^8.7.0
contract C {
function checkFunc(uint amount) public payable{
require(
amount <= msg.value / 2 ether
"Not enough!!"
)
}
}
이 코드는 주어진 조건이 참이면 넘어가고 아닐시에는 에러메시지를 리턴하는 것이다.