Intro
회사에서 Ethereum 지갑에 대해 공부하라고 작업했던 프로젝트이다.
해당 프로젝트를 확인할 수 있는 깃허브에 노션링크를 연결했는데 해당 노션 페이지를 private으로 설정해 둬 public으로 볼수 있는 벨로그에 해당 내용을 옮긴다.
해당 내용은 지갑 생성, erc20/erc721 거래에 관해 설명한다.
Project
ethers.js
ethereum 블록체인과 그 생태계와 상호작용하기 위한 라이브러리
Provider
- 이더리움 네트워크에 대한 연결을 위한 추상화(abstraction)를 제공하는 클래스.
- 블록체인에 대한 읽기 전용 액세스를 제공한다.
- 개인이 노드가 될수없기 떄문에 네트워크의 정보를 대신 제공해주는 것이다.
- 특정한 형식으로 항상 요청해야 하기 때문에, 형식으로의 변경을 도와주는 라이브러리가 Ethers.js이다.
Signer
- 직간접적으로 private key에 대한 접근권을 가지고 있는 클래스.
- 계정의 ether를 사용하여 트랜잭션을 수행할 수 있도록 한다.
- signer는 이더리움 계정을 통해서 트랜잭션에 사인을 해서 이더리움 네트워크 상의 정보를 변경하는 트랜잭션을 실행할 수 있게 해준다. (가스비 발생)
Smart Contract
- 서면으로 이루어지던 계약을 코드로 구현하고 특정 조건이 충족되었을 때 해당 계약이 이행되도록 하는 Script
Infura
- 블록체인(네트워크에) 접속을 쉽게 도와준다.
- provider(노트와 통신하기 위해)생성할 때 필요
- 직접 노드를 운영하는것은 디스크 용량과 리소스를 많이 잡아먹는다.
→ infura 사용시 직접 노드 운용 필요없이 네트워크 접근 가능
메타마스크는 ethereum.request(args)
method 사용해 RPC API를 래핑한다.
- ethereum은 window객체에 포함 되어 있다. (metamask extension이 설치되어있는 경우 존재)
- api 통신시 params의 값들은 hexString으로 보내야한다.
- 지갑 연결 및 전송
- MetaMask Extension에서 Account를 가져온다.
- { method: 'eth_requestAccounts’ }를 argument로 넣어 보낸다.
- send Transaction
- 최소한의 TxParams property 구성
- from: 연동한 acct의 address
- to: value를 보낼 계정의 address
- value(optional): 없는 경우 0으로 생성
- 그 외
- nonce(ignore): 난스 증가는 민감한 문제로, 사용자가 custom하게 하지 않고, 메타마스크에서 관리한다.
- Gas Price, Gas Limit (optional)
- Data(semi-optional)
- chainId(currently ignored): 해당 부분에 메타마스크와 다른 네트워크를 넣어도 무시한다.
- txParams를
eth_sendTransaction
메소드를 사용해 tx를 params에 값을 줘 api통신한다.
- 예시 코드
const gasPrice = await ethereum.request({ method: 'eth_gasPrice' });
const weiVal = ethers.utils.parseUnits(value);
const txParams = {
from: metaMaskAddr,
to: recipient,
value: ethers.utils.hexlify(weiVal),
};
try {
const sendTx = await ethereum.request({
method: 'eth_sendTransaction',
params: [txParams],
});
} catch (err) {
console.log(err)
}
- 그 밖의 메소드 및 이벤트
- 현재 metamask의 체인(네트워크)를 표시하고 싶을 때
- balance를 가져오고 싶을때
{ method: 'eth_getBalance’, params: [address, QUANTITY|TAG] }
- QUANTITY|TAG: blockNum(
eth_blockNumber
메소드 사용)이나 "latest"
, "earliest"
or "pending"
사용
- 메타마스크 extension에서 변경 했을 때 화면에 적용되게 하고 싶을 때
ethereum.on(eventName, function)
을 사용한다.
- event 사용후 지우는 것도 잊지 말자 (
ethereum.removeListener(eventName, function)
사용)
- 예시 코드
ethereum.on('chainChanged', handleChainChanged);
ethereum.on('accountsChanged', handleAccountsChanged);
ethereum.removeListener('chainChanged', handleChainChanged);
ethereum.removeListener('accountsChanged', handleAccountsChanged);
02_Ethers.js 사용
- 지갑 연결 및 전송
- JsonRpcProvider생성(
new ethers.providers.Web3Provider(window.ethereum)
)
- provider에서 주소 추출
- provider에서 JsonRpcSigner(signer) 생성
provider.getSigner()
: metamask로 생성한 JsonRpcProvider는 signer 생성 가능, infura로 생성한 JsonRpcProvider는 signer가 연결되어있지(?) 않아 생성할 수 없다.
- tx 생성
- to(recipient): to만 넣어도 전송 가능
- value(optional): 없는 경우 0으로 생성
- signer를 통해 transaction 전송
signer.sendTransaction(tx)
를 사용해도 메타마스크 extension이 뜨고 메타마스크에서 확인을 눌러야 거래가 끝난다.
- 메타마스크 연결시 메타마스크 안키고 따로 작동하는 방법은 아직 모름
Steps to Generate EOA
( EOA 계정 생성 과정 )
1. To create Private Key
- ethers/lib/utils에서 randomBytes를 불러와 32byte생성 → Unit8Array값으로 리턴
- buffer.from을 이용해 32바이트 값을 새로운 버퍼 인스턴스를 생성
- .toString(’hex’)로 인코딩해 64자리의 스트링값 생성 → private key 완성
2. 2 ways to create Address
-
To Create Wallet Instance
- *new ethers.Wallet( privateKey [ , provider* ] )
-
privateKey → publicKey → address
ethers.utils.computeAddress(privateKey)
const publicKey = ethers.utils.computePublicKey(privateKey);
const addrFromPublic = ethers.utils.computeAddress(publicKey);
Transaction
1. get / create Provider and Signer
- 메타마스크를 통해 Provider를 가져와 Signer 생성
3. signing Transaction with privateKey
- signer.signTransaction( transactionRequest ) ⇒ Promise< string< DataHexString > >
4. sending Transaction
- provider로 전송하는 방법
- signer로 전송하는 방법: signer.sendTransaction( transactionRequest ) ⇒ Promise< TransactionResponse >
- signer에 provider를 connect후 진행할 수 있다.
- transaction에 sign하지 않고 사용
ERC-20 Token
1. create and deploy ERC-20 with OpenZeppelin & Remix
- OpenZeppelin에서 Contract Wizard를 통해 ERC-20 token 코드를 만든다.
- 리믹스에서 열어서 Contract를 compile한다(Compile contract 파란 버튼 클릭~!)
- 버튼 클릭후 생성되는 Contract part에서 select box에서 생성자가 만든 contract를 선택후 ABI를 복사한다.
- deploy 메뉴에 들어가서 environment, contract 등을 설정 후 Deploy한다.
- 배포 후 생성된 CA(Contract Address)를 복사한다.
2. connecting Contract
- *new ethers.Contract( address , abi , providerOrSigner* )
3. Transferring ERC20 Token
- erc20_rw.transfer( target , amount [ , overrides ] ) ⇒ Promise< TransactionResponse >
- tx생성은 아마 transfer하기 전에 생성하는 거 같다…?
ERC-721 Token ( NFT )
1. ERC-20를 만들때 처럼 Contract를 배포하고, 새로운 인스턴트 생성
2. IPFS for data of Tx ( Feat. Pinata )
- image를 먼저 ipfs를 사용해 upload한다. → hash값
- name, desc, image hash를 json형태로 만들어 ipfs를 사용해 올린 후 tokenURI값을 받는다. → tokenURI를 민팅할때 인자값으로 보낼때
ipfs://
를 붙이지 않고, 되돌려받은 CID값만 인자값으로 보낸다.(그래야 메타마스크에서 이미지 확인 가능)
- CID(Calling Identification Display): 발신자 번호표시
3. Contract를 통해 Minting
- contract.safeMint( address, uri )
4. Minting된 NFT 전송
await contract.safeTransferFrom(fromAddr, toAddr, tokenId)
await contract['safeTransferFrom(address,address,uint256)']( fromAddr, toAddr, tokenId );
용어 정리 / 리서치
해당 프로젝트를 진행하며 이해 안간부분이나 모르는 부분을 정리했다.
Account
- 이더리움 상태는 어카운트라고 하는 오브젝트(Object)들로 구성되어있고,
어카운트 오브젝트는 두가지로 분류된다.
- 외부 소유 어카운트(EOA: Externall Owned Accounts)
- 일반적으로 알려져있는 지갑과 같다.
- 개인키 사용
- 코드를 담고 있지 않다.
- 프라이빗 키에 의해 통제되는 계정 정보
- EOA 생성 방법
1. Private Key: 랜덤한 256bit(32byte)데이터를 64자리의 Hex열로 인코딩
2. Public Key: 개인키에서 ECDSA(타원곡선전자서명 알고리즘)을 이용해 공개키 생성
3. Address:
1. 공개키를 Kecak256 Hash값으로 변환해 256bit의 바이너리 데이터를 생성
2. 생성된 바이너리 데이터의 앞쪽 96bit(12byte) 데이터 제거 후 남아있는 160bit binary data를 Hex열로 인코딩한 결과값이 Address다.
3. 160bit를 16진수로 변환한 20byte의 데이터이며, 16진수로는 40개의 문자열을 표현할 수 있다.
- 자체적으로 트랜잭션 생성 가능, 다른 외부 소유 어카운트에 이더 전송 가능, 메세지를 통해 컨트랙트 계정 실행 가능
- 컨트랙트 어카운트(CA: Contract Account)
- 이더리움 코드를 담고 있는 계정
- EVM 코드를 담고 있다.
- 컨트랙트 코드에 의해 통제되는 계정 정보
- 개인키(private key) 정보를 가지고 있지 않다.
- 오직 외부 소유 어카운트(EOA)에 의해 실행 가능, 자체적으로 트랜잭션 생성 불가능, 조건에 따라 또 다른 컨트랙트 계정을 참조하기 위해 Internal Tx를 생성 가능하다.
- 어카운트 오브젝트는 20바이트의 주소와 상태변화를 가지고 있다.
- Account는 총 4가지(Nonce, Balance, storageRoot, codeHash) 정보로 구성되어있다.
EOA
-
Nonce : Account에서 전송된 트랜잭션의 수
-
Balance : Account가 소유한 잔고 정보로써, wei 단위로 표시
-
storageRoot : Merkle Paticia Tree의 Root Hash
-
codeHash : 빈 문자열의 Hash 정보
CA
-
Nonce : Account에서 생성된 Contract 수
-
Balance : Account가 소유한 잔고 정보로써, wei 단위로 표시
-
storageRoot : Merkle Paticia Tree의 Root Hash
-
codeHash : Account에 포함된 이더리움 버츄얼 머신(EVM) code의 Hash
Transaction
- 발신자는 transaction 발송할 때, 개인 키로 서명해 암호화.
수신자는 발신자의 공개키로 transaction 내용을 복호화 한다.
Transaction 구조
- Nonce : replay attack을 막기 위한 용도로, 하나의 account가 있을 때 몇 개의 transaction을 전송했는지 의미함.
- 하나의 Acct에서 몇 개의 tx를 전송했는지 알고, 성공된 tx 개수만 카운트된다.(wallet에서 관리)
- nonce 이슈 : 하나의 account를 ~
- 하나의 wallet에서 관리 시: 짧은 시간에 여러개의 tx가 생성될때 nonce 2에서 문제가 발생되면 정상적인 tx 3, 4, 5도 보류 상태가 된다.
- 여러 개의 wallet에서 관리 시(주로 거래소 wallet 케이스): 동시에 tx가 만들어지다 보면 중복되거나 차이가 발생하는 nonce가 발생할 수 있는데 하루 단위로 nonce를 리밸런싱하는 간헐적 방식을 취한다.
- Gas price : 가스 하나의 가격(wei 단위)
- Gas limit : 최대 가스 소모량
- Recipient : target account / receiving address
- EOA인 경우: value 송금
- CA인 경우: contract code 실행
- Value : 이더 송금 금액(in Wei)
- Data : contract 코드의 실행을 위한 필드
- v, r, s : 전자서명 서명 값(ECDSA 알고리즘)
서명 전 필요 값
- nonce, gas price, gas Limit, recipient, value, data, v, r, s
- from address는 EOA 공개키를 v, r, s 구성 요소로부터 알아낼 수 있어 존재하지 않는다.
v
값에는 chainId
값이 포함된다.
ERC-20
- Ethereum Request for Comment 20의 약자이고, 20은 리퀘스트 숫자다.
ERC-721
- NFT(Non Fungible Token: 대체불가토큰)
- 대체불가능하다
- ERC-20은 단순히 어카운트 별로 잔액을 추적하는데 중점을 둔다.
- ERC-721은 토큰 아이디 별로 소유권자가 누구인지 추적한다.
- 디지털 소유권 인증서 개념
- 소유권의 이전 가능 but 다른 토큰으로 대체 불가하다(토큰 간에 대체 불가).
- 해당 토큰 아이디 별로 이미지 등이 저장되어있다.
- 이더리움 플렛폼에 이미지를 바로 올릴 수 있지만 엄청난 가스 비용으로 이슈가 발생하므로, IPFS를 사용한다.
- 숫자와 문자열만 존재, 가격X: Protocol(Wyvern, Ox)을 통해 NFT를 주고판 걸 묶는다
Contract
- contract를 배포하면 contract생성 → CA생성
- 블록체인에 배포된 코드의 추상화이다.
- 스마트 컨트랙트가 블록체인에 포함된다는 의미는 블록체인 네트워크 상 모든 노드들이 동일한 스마트 컨트랙트(코드)를 가지고 있는 것이다.
- 만드는 순서
- 스마트 컨트랙트 구현: 구현하고자 하는 내용을 솔리디티(대표적)나 다른 언어로 코딩
- 구현한 코드 컴파일: 컴파일 결과 EVM바이트 코드 생성
- 스마트 컨트랙트 배포
- 컴파일된 EVM코드를 하나의 트랜잭션처럼 블록에 추가시켜 블록체인에 등록시키는 작업이다.
- 마이너가 해당 블록을 채굴하게 되면 블록체인에 포함된다.
- 소스 컴파일 → EVM 바이트 코드 → 구체적인 작업은 ABI취득해 작업 → ABI로부터 contract 객체 생성 → transaction생성해 블록에 추가
- Smart Contract에 쓰는 것은 transaction을 발생시키지만, 값을 읽어오는 것은 transaction발생 X
IPFS(Inter-Planetary File System)
- 블록체인에 직접 저장하는 경우 수수료가 많이 부담되므로, 외부 저장매체로 쓰임
- 분산형 파일 시스템에 데이터를 저장하고 인터넷으로 공유하기 위한 프로토콜
- 원본 데이터의 내용을 변환한 해시값을 이용, 전 세계 여러 컴퓨터에 분산 저장된 콘텐츠를 찾아 데이터를 조각 나눠 빠른속도로 가져온 후 하나로 합쳐 보여주는 방식으로 작동
- 특징: 탈중앙화(분산화),
ABI
- Application Binary Interface
- 컨트랙트의 함수와 매개변수들을 JSON 형식으로 나타낸 리스트
JSON RPC
- RPC 를 Json 포맷으로 표현한 것
- RPC(Remote Procedure Call: 원격 프로시져 호출)
- 언어나 환경 등에 종속되지 않고 서비스 간 프로시저를 호출하도록 한 개념
- 2.0부터 Request-Response 뿐만 아니라 응답없는 Notification 지원한다.
- 4계층 TCP 에서 동작하여 Http 프로토콜을 얹을 필요없이, Json 만 전달하여 동작 가능하다.
const response = await fetch(URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: "update",
params: [1,2,3,4,5],
id: 1
}
})
await response.json()
Event
- 컨트랙트 내에 있는 함수들이 호출 될 때 블록에 기록을 남김으로써, 나중에 보기 편하게 기록을 찾아 볼 수 있도록 삽입하는 것
- Frontend가 Smart Contract 코드 함수에서 실행되는 것에 대해 소통을 하기 위해 이용되는 것이 바로 Event이다.
- Smart Contract가 Event를 send하면 Frontend code에서 Listen하는 식으로 동작한다.
- 트랜잭션 내에서 호출될 수 있는 리턴값이 없는 함수이다.
- 이벤트를 호출하면 그 호출한 기록이 Transaction Receipt에 저장된다.
→ 일종의 Log이다.
LOGS
- Smart Contract에서 발생되는 event에서 만들어진다.
- 로그들을 사용해 블룸필터를 만들고 그것을 통해 빠르게 로그들을 검색할 수 있다.
- 이벤트를 설명하는데 사용된다.
- 주로 topic[0]은 event signature(a keccak256 hash)이다.
-
예외사항은 익명함수를 발생할때 이 서명이 첫 번째(0번째) 항목으로 포함되지 않는다.
-
topic은 이벤트 인자에 따라 변경될 수 있다.
Transfer(address,address,uint256)

Understanding event logs on the Ethereum blockchain 이미지를 참조했습니다.
Topics [usually]
- ERC-20
- 1st
indexed
topic is the sender address
- 2nd
indexed
topic is the recipient address
- 3rd
indexed
topic is the value
- ERC-721
- 1st
indexed
topic is the sender address
- 2nd
indexed
topic is the recipient address
- 3rd
indexed
topic is the token ID
Wallet
- 암호 화폐 지갑은 암호 화폐를 저장할 수 있는 계정을 의미한다.
Multi-Sig Wallet(멀티시그 월렛)
- Smart Contract를 기반으로 구현됐다.
보안이 일반지갑에 비해 우수하지만, 편이성이 부족하고, 많은 주의성 요구 및 사용이 쉽지 않아 종류 또한 많지 않다.
- 인출 혹은 송금 등을 처리할 때, 다수의 서명을 필요로 하는 지갑
- M-of-N거래: 하나의 주소에 n개의 개인키가 설정되어있고, 인출할 시 n개의 개인키 중 m개의 서명이 있어야 가능하다.
- 목적
- 자금의 공동관리: 주된 창업자 한 명이 모든 금액을 가지고 잠적하는 등의 이슈 사전 방지
- 보안 이슈 해소: 거래소의 비밀키가 해킹 당해도 추가 서명이 없으면 큰 거래가 이뤄지지 못하게 가능
- 종류
- Consensys
- 꾸준한 유지 관리 X → Gnosis 멀티시그 월렛에서 계속 개발이 되고 있다.
- Gnosis
- ConsenSys 멀티시그 컨트랙트를 기반으로 지속적 개발 이루어짐 → 많은 사람들 사용
- Argent
Guardians
기능으로 다른 Argent 사용자를 선택해 모든 출금에 그 사용자의 승인이 필요하게 한다.
- BitGo
- End User가 Off-chain에서 서명하고 이 데이터를 또 다른 Signer가 서명해 트랜잭션을 발생시켜 2 of 3을 만족해야 출금 되는 방식
비결정적 지갑(nondeterministic wallet)
- 개인정보 보호를 위해 암호화폐 주소는 재사용하지 않는 것이 바람직하다.
- 무작위로 선택된 Private 키가 저장되어있는 지갑입니다.
- 여러개의 키 사이에 규칙이나 연속성이 없이 무작위로 생성이 되는 것.
- 정기적인 백업 필요 → 백업 전 데이터를 잃어버리면 해당 계좌에 있는 자금 및 smart contract 접근 불가
결정적 지갑(deterministic wallet)
- 모든 키가 Seed라고 알려진 하나의 마스터 키에서 유도되어 나온다.
- 모든 키가 서로 연관되어 있으면 원본 시드가 있는 경우 복구 가능.
- Seed는 개인키를 만들기 위해 랜덤하게 생성된 숫자로, 인덱스 번호 또는 체인 코드와 같은 다른 데이터와 결합돼 개인 키 유도.
- Seed 보안은 매우 중요하다.
- Seed는 모든 파생된 키 복구 가능.
- 시드 하나만 있다면 전체 지갑에 접근 가능하다.(지갑 내보내기 가져오기에 활용 가능해 다른 지갑 간에 모든 키를 쉽게 이동 시킬 수 있다.)
- 보안 노력을 단일 데이터에 집중할 수 있다는 장점이 있다.
계층적 결정 지갑(HD Wallet: hierarchical deterministic wallet)
- 단일 시드에서 많은 키를 쉽게 유도하기 위해 만들어졌다.
- 부모키가 연속된 자식키를, 각각의 자식키는 손자키를 유도할 수 있는 구조인 트리 구조로 파생된 키를 포함한다.
- 부모키가 자식키를, 다시 자식키가 손자키의 시퀀스를 유도할 수 있다.
- 장점
- 특정 서브 키의 특정 분기는 입금을 위해 사용, 다른 브랜치는 출금의 잔돈을 받기 위해 사용가능
- 기업 설정과 같은 구조적인 의미를 표현하는 데도 사용할 수 있다.
- 사용자가 개인키에 접근하지 않고, 연속된 공개키 생성 가능
- 보안상 안전하지 않은 서버, 보기전용, 수신 전용의 용도로 사용할 수 있는데, 이때 지갑에 개인키(자금을 움직이는)가 들어있지 않게 만들 수 있다.
Hot Wallet
- 인터넷 연결 O (온라인)
- 실시간으로 편리하게 이용 가능하지만, 개인키를 온라인에 연결해서 입력하기 때문에 해킹 등 보안 문제에 취약하다.
- 유형
- 모바일 지갑: 편의성 때문에 많이 사용하나, 스마트 폰은 거의 항상 인터넷에 연결되어있기 때문에 보안 문제가 있다.
- 데스크탑 및 브라우저 지갑: 바이러스에 취약하고, 휴대 장치의 보안 시스템에 의해 안전성이 좌우된다.
Cold Wallet
- 인터넷 연결 X (오프라인)
- 암호화폐 금고라고 생각하면 된다.
- 고액투자자나 코인을 안전하게 보관하기 위한 목적으로 쓰인다.
- 유형
- 하드웨어 지갑:
- USB케이블 또는 블루투스를 통해 모바일, 컴퓨터 등에 연결해 사용하는 물리적 장치.
- 장치에 연결할때만 핫 월렛이 되기 때문에 보안성이 더 좋다.
- 종이 지갑:
- 온라인 상의 공격이 모두 방지 된다.
→ 지갑 주소와 개인 키가 프린트 된(보통 QR코드 형식) 한 장의 종이이다.
최근 규제 지침은 수탁형(예, 거래소 지갑)
지갑과 비수탁형(예, 메타마스크 지갑)
지갑을 구별하는것에 초점을 두고 있다.
참조