반응형

Terminology

  • signal
    • input, output, intermediate로 정의할 수 있음
    • main component에서만 public, private구분하면 됨
  • constraint
    • signal들의 관계를 나타낸 것
    • constraint = set of gates 정도로 이해하고있긴 함
    • A * B + C = 0의 형태로 나타내야 함(근데 컴파일 과정에서 이렇게 변환해준다는 말인듯)
      • R1CS꼴로 나타내려면 이렇게 해야 함
  • witness
    • constraint를 만족하는 input signal의 set으로 이해하고 있음

Circom

circom을 통해 circuit을 컴파일하고, r1cs 형식으로 formatting하여 zkp를 생성 및 검증할 수 있다. 

Prerequisite: circom, snarkjs 설치

0. circom을 통한 zkp 생성 순서

  1. upside.circom 작성
  2. upside.circom 컴파일 (by circom)
    output files: upside_cpp / upside_js / upside.sym / upside.r1cs
  3. upside.circom에 대한 witness 생성(valid/invalid한 input에 대한 signal set을 binary로 생성)
    output files: witness.wtns
  4. Groth16을 이용한 zkp 생성 (by snarkjs)
    1. trusted setup을 통한
      1. Powers of Tau: general한 초기 설정
      2. Phase2: circuit 특화 설정

1. upside.circom 작성

각종 signal에 대한 constraint를 표현한 upside.circom작성

pragma circom 2.0.0;

template Add() {
    signal input a[3];

    a[0] === a[1] + a[2];
}

component main  = Add();

2. 컴파일 및 input값 generate

circom compiler를 통해 compile

circom upside.circom --wasm --c --sym --r1cs
  • wasm: witness 생성 위한 wasm 디렉토리 생성 (upside_js)
  • c: cpp witness 생성 위한 cpp 디렉토리 생성 (upside_cpp)
  • sym: 디버깅용 symbolic 파일 생성 (upside.sym)
  • r1cs: constraint를 R1CS형식으로 변환한 파일 생성 (upside.r1cs)

verifier에 verify를 하기 위한 input값 생성(json으로)

echo '{"a": [3, 1, 2]}' >> input.json

3. witness 생성 (아래 두 방법 중 택1)

constraint를 만족하는 input값을 알고있다는 것을 증명하기 위한 signal set인 witness.wtns를 생성. snarkjs에서 zkp를 생성할 때 사용한다.

3-1. wasm

cd upside_js/
node generate_witness.js upside.wasm input.json witness.wtns # 실행

3-2. cpp(wasm보다 큰 circuit 처리속도 빠름)

cd upside_cpp
make # Makefile 통해 build
./upside input.json witness.wtns # 실행

4. zkp 생성 및 검증

circuitwitness.wtns에 대한 증명, 검증을 하는 단계

Groth16 protocol

zk-SNARK의 가장 많이 사용하는 구현.

trusted setup, prove, verify 이렇게 세 단계로 나뉜다.

1. Trusted Setup

Power of Tau, Phase2 이렇게 두 단계로 나뉜다.

1-1. Power of Tau

groth16을 사용하기 위한 기초적인 환경 세팅 단계

1-1-1. Lagrange Interpolation, 다항식 연산 효율적 수행을 위한 기초 데이터 생성
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v

- bn128: 사용된 곡선
- 12: circuit 크기 (2122^{12}212크기까지 처리 가능)
- pot12_0000.ptau 파일 생성

1-1-2. contribution: 보안 강화를 위해, 여러 사람 또는 system의 contrbution을 통해 data를 생성(각 참여자는 개인 entropy를 추가)
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v

- —name을 통해 entropy 추가 X, 이거 하면 입력하라고 뜸
- pot12_0001.ptau 파일로 내용 업데이트

1-1-3. Phase 2로 전환
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v

- pot12_final.ptau 생성됨

1-2. Phase2

제작한 circuit에 최적화된 data를 생성하는 단계. Powers of Tau에서 생성된 데이터를 바탕으로, 각 circuit의 constraint를 만족하기 위한 proving key, verification key를 생성하는 단계. R1CS를 GQP로 변환하는 과정도 포함된다.

1-2-1. .zkey 파일 생성
snarkjs groth16 setup multiplier2.r1cs pot12_final.ptau upside_0000.zkey

- r1cs 파일을 이용해 data를 생성함
- proving key: Prover가 zkp생성할 때 필요한 key
- verification key: verifier가 zkp를 검증하는 데 필요한 key
- proving key, verification keyupside.zkey

1-2-2. contribution

이 또한 여러 사람, 시스템이 기여할 수 있다.

snarkjs zkey contribute upside_0000.zkey upside_0001.zkey --name="Contribute" -v
1-2-3. 검증키 추출
snarkjs zkey export verificationkey upside_0001.zkey verification_key.json

- verifier가 verify과정에 사용할 수 있는 verification key를 추출

2. 증명 생성

snarkjs groth16 prove upside_0001.zkey witness.wtns proof.json public.json

- zkey, witness.wtns 이용해 zkp, public input 생성

3. 증명 검증

3-1. local에서 검증
snarkjs groth16 verify verification_key.json public.json proof.json

- ok 라고 나오면 증명 된 것임

3-2. verifier.sol 만들기
snarkjs zkey export solidityverifier upside_0001.zkey verifier.sol

- verifier.sol 파일 생성됨 (solidity)

snarkjs generatecall

- witness.wtns를 calldata형식으로 만들어주는 명령어
- verifyProof()함수 호출해서 True 반환하면 성공

 

(그런데 .sol에서 public signal의 수가 0일 때 함수의 매개변수로 uint[0]을 넣도록 코드가 작성되는데, 이 경우 solc에서 error를 띄우기 때문에, 이를 해결할 방법을 찾아야할 것 같긴 하다.)

반응형
반응형

스시스왑, 유니스왑과 같은 허가없이 유동성 풀을 생성하거나 거래할 수 있는 open dex에서 최근 들어서인지, 원래 그랬는지 모르겠는 허니팟 Scammer들한테 자꾸 연락이 와서, 혹시나 속아넘어가는 사람이 있을까봐 글을 작성해본다.

25/1/2: https://dexscreener.com/arbitrum/0x186aa6185a5c0f7bceda657511aa8ae555df59e3

FOMO 오게 생긴 차트. 이게 도대체 왜 문제인지... 어떻게 이로 인한 스캠 피해가 생기게 되는지를 코드 분석과 실제 사례를 통해 알아보도록 하자.

(+ 나한테 왜그러는지 모르겠는데, 오늘(25.1.5) 새로운 스캠메시지(같은 패턴)이 왔다.

 

https://dexscreener.com/base/0xba83db06677bbe976687429696bbe19a1c872d56

 

주요 특징 세줄 요약

  • pump&dump: 끊임없는 가격 상승, 그리고 순간 폭락
  • arbitrum: 내가 본 모든 사례는 아비트럼에서 발생했다(오늘 사례를 통해, 이 원칙이 깨졌다.)
  • open dex: uniswap, sushiswap 등 open dex에서 발생한다. (100%)

공격(?) 방식

나도 실제로 이로 인한 피해를 보기도 했고(뭔지 모르는거에 큰 돈을 투자하고 싶지 않아 10불정도의 피해를 봤다.), 주변에서 1000불 이상 피해를 본 경우도 보는 등 총 다섯 번의 사례를 확인하였다. 그 결과, 공통적인 특징을 확인할 수 있었다.


#1. 피해자 모집(케바케)

일반적으로, 텔레그램 DM을 통해 접근한다. dex와 같은 플랫폼에서 온체인 거래를 하는 사람들은 대부분 텔레그램 암호화폐 정보 공유방에 들어가있을 것이다. 이러한 텔레그램 방에서 피해자를 선별하고, 해당 인원에게 DM을 하여 피해자를 모집한다.

 

우선, 그들은 친목을 한다. 해당 시즌/메타에 핫한 코인을 주제로 이야기를 하거나, 본인의 수익률을 자랑하거나, 특정 기술에 대한 이야기를 하며(솔직히 기술적 이해도가 높아보이지는 않다) 유대감을 형성하는 것으로 보인다.

 

그러다가 슬슬 본심을 드러내는데, 이런 느낌이다.

오늘 저녁에 발표되는 엄청난 코인을 발견했으며, 자기 말대로만 하면 엄청난 수익을 가져다준다는 이야기이다.

 

또 다른 예시에서는, 실제로 존재하거나 토큰 출시를 앞둔 프로젝트의 트윗, 웹사이트를 가져와 해당 프로젝트의 토큰 출시라고 시뻘건 거짓말을 자행하는 경우도 있었다.


#2. 구매 유도(케바케)

다음 단계로는, 어떤 코인인지, 어떤 프로젝트인지에 대한 설명보다는 몇시에 코인이 등장하는지를 설명해주고 본인의 가이드대로 구매를 하라고 요구한다.

그렇게 해서 코인을 구매했다고 하면, 추가 구매를 유도하는데…

이런식으로 토큰 에어드랍이 예정되어있으며, 에어드랍을 위해 추가 구매를 하라고 한다. 만약 유동성이 부족해 추가 구매가 어렵다면, 가지고 있는 다른 코인을 판매하거나 가족, 친구에게 돈을 빌리라고 한다.

 

(솔직히 이렇게 말하는데 넘어갈 사람이 얼마나 있을까? 라는 생각이 들긴 한다. 싸가지가 없다.)


#3. Pump

이렇게 피해자들을 모집한 결과, 다음과 같이 가격의 계속된 상승이 이어지게 된다.

 

그런데, 왜 계속 오르기만 하는 것일까? 그 이유를 알아보도록 하자.

앞선 부분은 그저 피해자를 모으는 방식이었다고 한다면, 지금부터가 이 스캠의 메인이 되는 부분이다.

결론부터 이야기하자면, 이런 종류의 허니팟 스캠 코인은, “인가된 사용자만이 판매할 수 있는 토큰”이다.
  • $Fabric(CA): 0x9020E73AfeABAbc479F7DE356b85Bb6E4825756E
  • open dex: sushi swap

어떤 방법을 사용한 것일지, 코드를 통해 분석해보자. 컨트랙트 원본을 찾을 수 없어 bytecode를 dedaub을 통해 decompile하였다. swap이 이루어질 때 swapExactTokensForTokens() 함수를 통해 swap이 이루어지며, 대부분 sushiswap의 pair, router 컨트랙트에서 로직을 처리한다. 스캠코인이 동작에 영향을 주는 부분은 transfer()함수를 호출하는 과정에서 영향을 주게 된다.

dedaub을 통해 decompile한 결과를 보고 어떠한 방법으로 진행되는지, 살펴보자.

 

1. transfer()함수에서 0xc08로 jmp

2. 0xc08에서…

  • recipient, msg.sender 주소 0인지 체크 (그렇다면 revert)
  • 0x12f9로 jmp. 인자에는
    • _getRoleAdmin[0].field0[msg.sender]
    • amount
    • recipient
    • msg.sender를 넣는다.
  • 아래 부분은 일반적인 transfer의 동작과 같다. (erc20:_update()와 같다고 하는게 맞으려나?)

3. 0x12f9를 보면 ...

stor_0_0_19와 msg.sender주소를 비교하는 것이 보인다.

해당 주소를 검색해보니, sushiswap의 pair address가 나온것을 확인하였다. 즉, pair가 transfer를 할 때의 로직과, 일반 사용자가 transfer를 할 때 로직에 차이가 있는 것이다. 아무래도 AMM이다보니, 유동성풀에서 거래를 하려면 pair는 자유롭게 거래를 하도록 해야하니.. 이렇게 한 것으로 보인다. 좀 더 들여다보자.

 

if문으로 들어와서 첫 번째 if문은 _getRoleAdmin[0].field0[msg.sender]의 결과가 0일 경우에 들어오는 조건문이다(if(!varg0)). 해당 _getRoleAdmin[0].field0[msg.sender] 변수는 어떻게 세팅되는것인지를 찾아보면 아래와 같다.

즉, grantRole함수를 통해 0x73ajmp를 하고, (아마 grantRole권한을 확인하려는 듯 하다) role을 부여할 자격이 있는지 확인한 후 role을 부여한다.

 

이처럼, msg.sender에게 특정 role이 있는지를 확인한 뒤, role이 있다면 그저 통과, 아니라면 if문에 들어간다.

owner_1[varg3].field_1[varg3]은 0으로 세팅되어있으니, varg1(amount)를 통해 조작해보면,

  • amount가 0일 경우: v0는 0 → v1, v2도 0 → if(!v1)에서 revert 발생
  • amount가 양수일 경우: v0는 양수 → v1, v2는 1 → v1은 다시 0 → if(!v1)에서 revert 발생

이로 인해, 절대 통과할 수 없게 된다.

그렇다면, owner_1[varg3].field_1[varg3]를 조작하는 방법은 없을까?

owner_1[varg3].field_1[varg3]을 수정하는 함수가 있는데, 이 함수도 … 위의 if문을 통과해야 도달할 수 있는 함수이기 때문에, 결국 이를 수정할 수는 없다.

결과적으로, 등록을 할 수 있는 role을 가진 사람이 아니라면, whitelisting을 할 수 없고, whitelisted되지 않았다면 transfer를 할 수 없어 결과적으로 판매를 할 수 없게 된다!

이처럼 판매를 할 수 없으면, 경제학적 원리에 따라, 수요만 존재하고, 판매는 없는 현상이 발생해 토큰의 가격은 하늘높이 치솟게 되는 것이다.


#4. Dump

가격을 하늘높이 치솟게한 후, scammer가 전량 매도하면 scammer는 모든 상승분을 가져갈 수 있게 되며, 드디어 끝을 모르던 펑핑은 끝을 보게 된다.

 

여담 (Scammer와 채팅내용)

속아주는척 -> 밥먹는데 자꾸 디엠해서 읽씹하다가 -> 지갑은 비탈릭 지갑


 

좀 찾아보다보니, 이 컨트랙트를 만들어서 판매하는 사람이 있는 것도 발견하여 공유한다.

상세 설명(스캠 개발자의 설명 영상)
https://www.youtube.com/watch?app=desktop&v=OZqDC_2Wq3I

컨트랙트 코드 구매 링크
https://cryptokoki.com/honeypot-token-v4.html 

 

총평.

이런 천하의 썩을놈들은 없어져버려야한다고 생각한다.

 

 

  1.  
반응형

'web3' 카테고리의 다른 글

2024 Crypto 회고  (2) 2024.12.31
[Web3] 앞으로 Web3 관련 게시물들을 좀 올려볼까나?  (0) 2024.04.05
반응형

최근 Base 맥시가 된 것 같다. Base, 솔라나쪽에서 AI관련 섹터가 상당히 핫한 것 같은데, Base에서 떠오르는 AI-agent들이 어떠한 것이 있는지, 그리고 어떻게 사용하는지 알아보고자 Virtuals Protocol에 대한 공부를 해보려 한다. 이를 위해 Virtuals Protocol Whitepaper를 읽어보며 AI라는 토픽의 현위치와 기술적 이해를 해보고자 한다.

글을 쓰기 시작하는 시점에서, Virtuals에 대해 아는 것은 AI-agent 관련 token에서의 펌프펀이라는 것만 알고 있다.

 

이제, 시작해보자.

 

Our one liner | Virtuals Protocol Whitepaper

Last updated 2 months ago

whitepaper.virtuals.io

virtuals protocol whitepaper


Virtuals Protocol

Virtuals Protocol은 Game, Entertainment 분야의 AI agent를 블록체인을 통해 ‘공동소유’할 수 있도록 하는 플랫폼이며, AI-agent를 통해 자율적으로 운영하고 다양한 application에서 수익창출이 가능한 자산으로 변환한다.

 

대부분의 프로젝트는 어떠한 Pain Point에서 시작한다. Virtuals는 어떠한 것을 해결하기 위해 출시되었을까.

Virtuals Protocol이 해결하고자 하는 세 가지 Pain Point

  • Complexity in implementing AI agents into consumer applications
    • > Plug&Play 방식으로 소비자 application이 AI-agent를 간편하게 사용할 수 있도록 함
  • Lack of revenue for AI finetuners and dataset contributors
    • > contributor에 대한 수익 분배를 제공하도록 함
  • Limited access for non-AI experts to capitalize on AI agent opportunities
    • > 비 전문가도 AI-agent에 대한 소유, 참여를 가능하게 함

그래서 Virtuals는 무엇을 하고자 하는가?

AI를 “수익을 창출하는 자산”으로 간주하여, 사용자들이 이에 투자하여 지분을 갖고, 공동소유할 수 있도록 한다.

  • 그렇다면, 어떻게 수익을 창출한다는 것일까?예를 들어 Virtuals에서 가장 잘나가는 agent 중 하나인 $AIXBT의 경우, 분석서비스를 제공하면서 이를 통해 데이터 판매 수익, 서비스 이용료 등을 거두며, 또한 소셜 활동을 통해 yaps 포인트를 쌓아 수익을 창출하는 활동을 한다.
  • Whitepaper의 설명을 조금 더 읽어보면, 도파민을 뿜어내는 컨텐츠의 무제한 생산을 통해 사용자들을 모으고, 맞춤화된 컨텐츠 생산을 통해 끊임없는 도파민 분비를 촉진하겠다는 의지를 보이고 있다. 즉, 더 많은 컨텐츠, 초개인화를 통해 사용자당 평균 수익, 그리고 유지율의 기하급수적 증가를 하고자 한다. (도파민이라는 표현이 있긴 하지만, 자극적인 컨텐츠 뿐 아니라, $AIXBT의 경우와 같이 지식, 데이터를 통한 도파민까지 포괄하는 내용으로 해석할 수 있다.)
  • AI-agent를 Game, Entertainment 분야에서 수익창출을 하도록 한다는 설명이 있었다. 즉, 로블록스, 틱톡 등 플랫폼에서 가상의 인플루언서 활동을 통해 수익을 창출할 수 있도록 한다는 이야기이다.

사용자들은 이를 통해 누구나 AI-agent를 소유하고, 개발에 참여할 수 있으며, 이를 통한 인센티브를 공정하게 분배할 수 있다.

그래서 Virtual Agents가 뭔데?

스스로 학습하고, 계획하고, 결정을 내리는 “자율적인” agent.

  • 3D 공간에서 활동
  • 물리적, 디지털 방식으로 상호작용
  • 자체 지갑을 통한 트랜잭션 생성

이러한 활동을 할 수 있다고 하는데, 잘 와닿지는 않는다. 계속 자세히 알아보자.


Agent

G.A.M.E 프레임워크를 이용해 자율 AI-agent를 만들 수 있다. player(사람, agent 모두를 포함)는 각종 상호작용을 할 수 있으며, 이러한 상호작용 안에서 새로운 연쇄반응이 유발된다. 이를 통해 결과적으로 각 플레이어가 고유한 스토리라인을 경험할 수 있게 된다.

모든 player들과의 상호작용에 대한 진행 과정은 Virtuals에 의해 기록이 되며, 이를 통해 더 깊고 지속적인 UX를 제공한다.

 

Agent의 두 가지 종류인 IP Agent와 Functional Agent에 대해 알아보자.

IP Agents

원래 IP는 Intellectual Property를 뜻하며, 지식재산, 혹은 지식재산권을 이야기한다.

Virtuals에서는, 특정 캐릭터를 나타내는 가상의 agent로, 사람들이 친숙하게 느낄 수 있는 대상을 기반으로 한다.

  • Frog, Meme, Trump, Vitalik, Anime 등

이들은 사용자들과의 정서적 연결을 위해 설계되며, 게임, 소셜, 엔터 등에서 흥미를 유도한다.

  • 사용 예시: 가상 인플루언서, 게임NPC, 교육, 이벤트, 마케팅 등

Functional Agents

IP Agent를 서포트하는 agent로, 기능 중심의 작업을 수행하여 UX 최적화를 목표로 한다.

  • 데이터분석, 데이터 처리, 환경분석, 자동화 등

IP Agent가 프론트엔드라면, Functional Agent는 백엔드라고 생각하는 것이 좋다.

  • 사용 예시: AI-agent간 통신, 작업, 알고리즘 실행, player와 상호작용 지원

즉, IP agent는 스토리와 엔터테인먼트 / Functional Agent는 기술적 지원과 UX에 초점을 두고있다고 볼 수 있음.

대표적인 IP Agent인 LUNA와, Functional Agent인 G.A.M.E을 통해 살펴보자.

Agents 예시

Luna - IP Agent

 

AI 걸그룹의 리드보컬인 Luna.

사진은 Luna가 스쿼트를 하는 틱톡 영상이다.

틱톡 50만+ 팔로워를 보유한 디지털 인플루언서이다.

Luna와 상호작용하는 사용자는 아래의 경험을 할 수 있다.

  • 틱톡을 통해 Luna를 접함.
  • Telegram을 통해 Luna와 대화.
  • Roblox에서 함께 게임을 하며 관계를 발전.

단순 인플루언서를 넘어, 사용자 맞춤화된 디지털 인플루언서를 경험할 수 있는 것이다.

 

 

 

 

 

G.A.M.E - Functional Agent

G.A.M.E은 Luna의 백엔드라고 할 수 있다. 전체적인 디자인은 아래와 같다.

https://whitepaper.virtuals.io/what-are-virtual-agents/ip-agents-vs-functional-agents/highlight-g.a.m.e.-functional-agent

 

개발자들이 AI-agent를 API, sdk를 통해 동작시킬 수 있도록 설계된 프로덕트. 이를 통해 AI-agent를 손쉽게 Plug&Play할 수 있는 경량 프레임워크를 제공한다.

 

아래는 위의 그림에 설명되어있는 G.A.M.E의 각 모듈이 어떻게 동작하고 통신하는지에 대한 내용이다.

  • Agent Prompting Interface
    • AI-agent의 Agentic Behavior에 접근하는 게이트웨이 → 이를 통해 G.A.M.E을 통합할 수 있음
  • Perception Subsystem
    • Agent Prompting Interface와 상호작용.
      • Agent Prompting Interface로부터 메시지를 수신해 Agent의 Behavior생성을 위해 Strategic Planning Engine에 전달하는 역할을 수행(request를 전달).
      • 그 반대의 역할도 수행한다(response를 전달).
    • Module과 상호작용
      • action, game state, 또는 sensory information과 같은 여러 feedback 정보들을 Learning Module에 전달.
  • Strategic Planning Engine
    • AI-agent의 고급 의사결정, 계획 수립을 담당
    • High Level Planner
      • Dialogue Processing Module을 통해 반응/대답 등의 반응을 생성함.
    • Low Level Policy
      • High-level의 명령을 해석하고, 실행가능한 action으로 만들어내고, On-chain wallet과 연결하여 이를 실행할 수 있음.
  • Learning Module
    • general knowledge를 생성하기 위해(학습데이터 가공 및 학습을 의미함), feedback 등 information을 분석 및 가공을 함.
    • 이는 Long Term Memory로 전달 및 저장된다.
  • Long Term Memory Processor
    • LTM Processor는 world agent, working memory에서 필요한 정보들을 추출하고, 저장하는 역할을 한다.
    • 기억 유형: Experience, Reflections, Personality, World context, Working memory
    • 프로세스: Importance score, Recency, Relevancy

이처럼, 여러 모듈 간 통신을 통해 학습데이터에 대한 피드백, 상호작용에 대한 결과 생산 등 많은 작업을 수행할 수 있다.


이처럼 1편에서는 Virtuals의 AI-agent가 어떠한 방식으로 작동하는지, 무엇을 하고자 하는 프로토콜인지에 대해 알아보았다. 2편에서는 Virtuals의 AI token을 사면 user들에게 어떠한 이점이 있는지 등, 경제적인 내용을 다뤄보도록 하겠다.

반응형
반응형

블록체인, Web3라는 세계에 입문한지 어느덧 1년이라는 시간이 되었다. 처음에는 블록체인 기술이라는 것을 정말 “탈중앙화된 금융” 그 이상도, 이하도 아닌 것으로 인식하고 있었다. 때문에 이더리움, 솔라나와 같은 모든 종류의 코인을 그저 비트코인과 대동소이한 암호화폐 라고만 생각했다. 하지만, 온체인 상에 어플리케이션을 올리고, 탈중앙화된 방식으로 서비스를 운영하며, 꽤나 high-level의 프로그래밍 언어도 존재하고, 해킹 방식 또한 기존에 접하던 것들과는 다소 차이가 있는, 흔히 말하는 2세대 블록체인의 여러 요소들이 나에게 꽤나 매력적으로 다가왔던 것 같다. 일년간의 경험을 통해 나의 미래먹거리는 Web3라는 생각을 굳힐 수 있었고, 블록체인밸리 학회를 시작으로, 회사 인턴십을 거쳐 업사이드 아카데미를 지난 시점의 내가 crypto씬에서 1년동안 어떠한 공부를 했는지, 그 행적을 다시금 돌아보고자 2024 crypto와 관련된 기억들을 적어본다.

  1. 사실 2023 3~4분기정도부터 적게될 것 같다.
  2. crypto에 대한 내용이기 때문에 암호화폐와 암호시스템에 대한 내용을 모두 적어보려 한다 ㅎㅎ

Blockchain Valley

blockchain valley 로고

고려대학교 블록체인 학회인 블록체인밸리(줄여서 블밸이라고 부른다).

2023년 3분기가 시작할 무렵까지 내가 정보보안, 더 나아가 IT라는 분야에서 어느 부분에 자신이 있는지 긴가민가했다. 시스템해킹, 암호학, 리버싱, 개발 모두 공부해보았지만 내 적성에 과연 맞는지, 또는 남들보다 내가 잘 할 수 있는 분야인지에 대한 확신이 부족했다. 그러던 중 여름방학이 되어 학교 커뮤니티에 각종 학회, 동아리 공고가 올라오는것들을 눈팅하고 있었는데, 그 중 가장 내 눈에 띈 것이 블밸 신입 학회원 모집 공고였다. 위에서도 언급했듯, 나는 블록체인 기술을 탈중앙화된 금융 정도의 개념으로만 받아들이고 있었는데, 공고에는 리서치팀, 개발팀을 따로 나누어 뽑는 것을 보고 ‘엥? 블록체인에서.. 개발..?’이러한 의문이 들었다. 화폐 시스템 위에서 개발을 한다니, 이게 무슨 말인가. 이러한 의문에서 시작된 서칭을 통해 처음 알게 된 것이 바로 DApp 즉, 블록체인상에서 동작하는 Application이었다. 이건 대체 뭘까.. 라는 막연한 흥미를 가지고 정말 아무것도 모른채로 지원했는데,, 정말 운좋게도 합격을 해 블밸 4기 멤버로 활동을 할 수 있었다.

 

블록체인 밸리에서 나는 블록체인, 더 나아가 Web3라는 새로운 생태계를 맞이하였다. 난생 처음 메타마스크를 설치해 개인 웹지갑을 생성하고, 업비트를 설치해 이더리움을 전송하는 등 정말 걸음마 단계부터 시작했던 것 같다. 더 나아가 Web3생태계에 존재하는 여러 구성요소(Defi, DAO 등), 메인넷(Ethereum Mainnet)에 대해 공부하고 여러가지 프로젝트를 만들어보는 등 새로운 것을 계속해서 쌓는 과정이 상당히 즐거웠다. 하지만 ‘보안’을 공부하는 나에게는 딱 fit하지 않다는 생각이 들었는데, 그 이유는 1) 개발 과정에 맞추어진 커리큘럼, 2) 백엔드보다 프론트엔드에 맞춰진 교육, 3) 원리를 모두 알고 넘어가는 것 보다 포트폴리오 생산에 맞추어진 것 같은 느낌. 이 세 가지 느낌을 많이 받았던 것 같다. 그래서 학회보다는 혼자만의 공부가 필요할 것 같다는 생각을 하게 되었던 것 같다. (전해듣기로 지금은 리서치, 개발, 보안 이 세 가지 팀으로 쪼개져 더 다양하고 심화된 커리큘럼이 제공되는 것 같다.)

 

그렇게 혼자 공부를 하면서 Uniswap 분석, EVM 분석 등 이론공부와 Ethernaut 등 워게임을 통한 Attack vector 학습 등 개인적인 공부를 진행했다. 하지만 혼자 공부를 하게 되니 계속해서 텐션도 떨어지고, 영어울렁증으로 인해 해외 자료는 찾아볼 생각도 안하는 등 여러모로 생산적인 공부를 하지는 못했던 것 같고, 사실, 당시에는 Web3에 대한 흥미가 좀 떨어졌었다.

 

2023년에는 이정도인 것 같은데, 이더리움, DApp, 유니스왑. 이정도 말고는 더 뭔가 공부한 느낌은 없던 것 같다. (Remix가 없으면 컨트랙트 배포를 하지 못하고, hardhat은 쓸줄도 모른다. foundry는 들어본 적도 없었다.)


LG전자 CSEU 인턴십

산학협력의 일환으로 우리 학과에 티오 4명이 제공되어 ‘설마 되겠어~’ 하고 지원했던 LG전자 인턴십. 2024년 1월~2월동안 LG전자 VS사업부에서 CSEU(Cyber Security Engineering Unit)에서 Key Management 관련 업무를 진행하였다.

이 기간동안은 잠시 Web3와 멀어져 C, C++ 기반의 KMS(키 관리 시스템) 개발에만 몰두했다.

 

VS사업부는 자동차 전장 관련 하드웨어 부품을 만드는 부서로, 조명, 인포테인먼트시스템 등 제품을 생산한다. 내가 속해있던 CSEU는 사이버보안 관련 개발을 하는 조직이었으며, 그중에서도 Key management 부서는 암호화 키를 관리 및 안전하게 보관하는 솔루션을 개발하는 부서였다.

 

나는 해당 조직에서 실제로 사용하는 제품을 개발하는 것 보다는(아마 인턴에게 제공할 수 있는 보안 수준 때문인 듯..) OS레벨에서의 message queue를 이용한 프로세스-KMS 간 key 연산 프로토콜을 개발하는 일종의 프로젝트를 진행하였다.

솔직히 결과물에 부족한 점도 많았고, 경험 부족에서 나오는 애로사항도 정말 많았지만, 요구사항 분석부터 시작하여, 프로젝트 설계, 구현 과정 설계, 테스트케이스 작성까지 여러 단계를 거치면서 정말 많은 것을 배웠다. 개발 실력을 넘어 프로젝트 설계 및 구현에서의 라이프사이클을 배운 것이 가장 컸다.

당시에 작성했던 명세. 지금 와서 다시 보니 정말 조잡하고 쏘큣 하다

KMS 개발을 하는 과정에서 key를 보관하고 안전한 연산을 수행하는 HSM, 그리고 이에 대한 표준인 pkcs#11을 공부해서 책임급 직책을 가진분들 앞에서 발표도 해보고, 실제로 암호화 통신이 작동하는 과정과 모든 요소에서 고려해야 할 각종 공격벡터에 대해서도 직접 개발해보지 않으면 알 수 없는 고려사항 등을 이해할 수 있었다. 무엇보다도, 진짜 “대기업”의 여러 사내 문화와 복지, 그리고 업무 방식 등을 이해할 수 있었던 것이 가장 귀중한 경험이었다.

 

그리고 얼마전에 느낀 것인데,,, 이 때 KMS-HSM간 통신, 그리고 HSM 모듈을 공부할 때의 기억이 TEE를 이용한 AI-agent에 대해 깊게 이해하는 데 많은 도움이 되었다.

함께 인턴십 과정 거친 4인방 한컷


업사이드 아카데미 1기(Dunamu x Theori)

업사이드 아카데미 로고

2월 말 인턴십을 끝내고, 또 정신없는, 많은 것을 배우는 한 학기가 시작됐다. AI보안이라는 과목을 통해 선형대수학, 통계학과 AI에 대한 이론적 바탕을 공부하였다. 또한 암호프로토콜이라는 과목을 통해 Commitment scheme, 다양한 signature, threshold signature등 암호학적 개념과, Secure MPC, ZKIP, 그리고 암호화폐에 적용된 프라이버시 기술 등 프라이버시 및 암호화폐의 기초를 이루는 개념들을 공부하였다(처음 이 수업을 들을 때는 이런 수업인줄 몰랐는데, 듣다보니 점점 암호화폐였다).

이러한 공부를 거치며 점점 Web3에 대한 흥미를 되찾기 시작했고, 그 무렵 인터넷 기사에서 두나무X티오리에서 Web3 보안 아카데미 1기 모집을 한다는 글을 보고 냅다 지원해버렸다. 서류, 실기, 면접 세 가지 전형을 거쳐 2024년 모든 경험 중 가장 값진 경험이 될 업사이드 아카데미 1기에 합격하였다.

굿즈, 맥북, 냉장고, 스낵바

RAMY볼펜, Stanley 텀블러, 두나무 굿즈, 아카데미 티셔츠 및 바람막이, 그리고 대망의 맥북 프로 M3 제공까지 미친 혜택. 그리고 무료 스낵바와 무제한 음료수를 제공하며 개인&팀별 프로젝트룸과 모니터까지 제공해주는 미친 복지까지. 두나무, 그리고 티오리가 이 아카데미에 얼마나 진심인지를 보여주는 대목이라 생각한다. 홍보성 멘트이기도 하다. 다들 업사이드 하자.

총 4개월간 진행되는 아카데미 생활. 2개월간의 교육과정과 2개월간의 프로젝트 과정으로 나뉘어있다. 정말 밀도, 퀄리티 모두 높은 시간이었던 것 같다. 이더리움 오버뷰, 암호화폐가 가지고 있는 내재가치에 대한 수업(개인적으로 가장 좋았다), Defi, Crypto 오버뷰까지, 내가 Web3를 왜 공부하고있고, 어떠한 것을 공부하고자 하는지를 이해할 수 있도록 기초적인 부분으로 시작하였다. 이 다음으로는 Web3 “보안”아카데미답게, EVM, 그리고 Debugging 등 low level부터 EVM을 공부하기 시작하였다(C언어 배우기 전에 어셈블리 배우는 느낌이려나). 이후 AWS, k8s 등 클라우드에 대해 배우고(이 부분에서 좀 더 열심히 배웠으면 하는 후회가 남는다), 드디어 Solidity와, Smart contract vuln을 학습하면서 본격적인 Web3보안을 배웠고, 암호학을 배우며 동형암호, 서명, ZKP를 공부했다(이 부분은 사전지식 없이는 상당히 힘들었을 것 같다). 이후에는 솔라나 오버뷰, 실제 사례 fork test, 크로스체인보안 등 학습을 하였다.

업사이드 아카데미 교육기간에서 개인적으로 가장 좋았던 것은, 양질의 콘텐츠, 강의도 있겠지만 “재미있게 설계된 과제”가 학습욕구를 계속해서 끌어올려줬다. 교육기간 과제를 할 때 가장 퍼포먼스가 좋았던 것 같은데, 나름 열심히 살았고, 잘 해낸 것 같아 정말 즐겁게 수행했던 과제(?)를 몇 가지 사진으로만 나타낸다.

#1

#2

(이때 정말 행복하게 공부했던 것 같다…)

 

두 달간의 교육과정이 끝나고, 나머지 두 달 동안 Lending Protocol Audit을 주제로 팀프로젝트를 진행하였다. 위협모델링을 위해 정말 많은 리포트들을 읽어보며 사례조사, 공격벡터 조사, 코드분석, 테스트코드 작성 등을 진행하였고, 이를 통해 취약점을 발견해보고자 하였지만, 의미있는 취약점을 발견하지는 못한 것이 아쉬웠다.

그래도 이를 진행하면서 Lending protocol에 대한 전반적인 이해와, compound, aave, euler, venus 프로토콜 등 여러 메이저 프로토콜과, 이들을 fork한 프로토콜 또는 소규모 프로토콜은 어떻게 구성되어있는지, 그리고 use case들을 살펴본 것, 그리고 실제로 audit을 경험해본 것 등이 의미있는 활동이었다.

 

업사이드 아카데미를 통해 Web3, 그리고 Web3 보안에 “재미”를 붙일 수 있었다. 활동을 진행하면서 생각보다 Web3생태계가 거대하다는 것을 알았고, 탈중앙화와 기관에 대한 불신에 대해 이로부터 벗어나고자 하는 움직임이 지금의 이런 거대한 시장을 형성했다는 것이 너무나 인상적인 것 같다.

활동을 하면서, 그리고 활동 이후에 ethernaut의 모든 문제를 문제없이 모두 해결한 것에서 실력(피지컬?)이 향상된 것을 느꼈고, 이제는 규모가 큰 코드베이스를 읽을 때도 겁이 나는 것이 아닌, 새로운 것을 배울 수 있다는 기대감이 생기는 것에서 내가 정말 나에게 잘 맞는 분야를 찾아왔다는 것을 느낀다.

 

업사이드 아카데미 정식 과정이 종료된지 어느덧 한달 반 정도가 지났다. 아카데미 활동을 하면서 다양한 dex, defi, 그리고 multichain등 여러 개념을 접하고 경험했다고 생각한다. 하지만 이쪽 판에 있는 사람은 모두 느끼는 것 처럼 오늘 내가 A를 공부하고 있어도 다음주만 되면 언제 그랬냐는듯 다른 기술인 B가 주목받는. 너무도 빠르게 변화하면서 또 빠르게 새로운게 생기는 흥미로운 생태계이다. 마치 n년차 기술자들이 새로운 생태계에서 새로운 프로덕트를 만드는 데 신나있는 것 처럼 너무도 빠르게, 많이 새로운 것이 생긴다.

나는 이제 막 아카데미를 졸업한. Web3 생태계에서는 각 고등학교를 졸업한 것 같다. 고등학생들이 과학을 공부할 때 과학적 개념을 이해는 (어느정도) 하고 있지만, 어떤 산업이 존재하는지 세상물정을 “잘”알지는 못하듯, 지금의 나도 Web3에서는 같은 위치에 있다는 생각을 한다. 그래서 기술적인 이해를 넘어, 생태계 트렌드를 이해하고 현재 어떠한 기술이 각광받고 있는지에 대한 것을 이해하고, 또 트렌드에 발맞추기 위한 “생태계 공부”를 한달 반동안 계속 진행한 것 같다.

 

스테이블코인, 펌펀, 하이퍼리퀴드, AA, NFT, AI, AI-agent, 그리고 어제까지는 TEE… 그 잠깐의 시간동안 너무 많은 것들이 훅훅 지나가고 또 거치는 중에 있는 것 같다. 이 모든게 신기하면서, 그 정보량에 압도당하는 기분이다. $ENA, pump.fun, $HYPE. $PENGU, pudgy penguin, ai16z, eliza, virtuals, sendAI, pha, ata, pond 등등… 이렇게 나열해보니 정말 어질어질한 것 같다. 한달 반이라는 시간동안 이 모든걸 접했다는 사실이.

앞으로 또 어떤 새로운 기술과 메타가 기다리고있을지 기대가 되면서도, 무섭기도 하다(ㅋㅋ..). 이제 곧 군입대를 하는데, 훈련소에 가 있을 동안 또 얼마나 많은 것들이 등장할지, 다시 Web3를 접했을 때 지금 내가 알고있는 Web3 생태계와 다를 수 있겠다는 생각이 들기도 한다.

그럼에도 불구하고 계속해서 새로운 것들이 생겨나고, Web2에서 이루어질 수 없는 여러 요소들이 Web3에서 가능해지는 것을 보며, 그리고 계속해서 온체인 트잭이 늘어나는 것을 보며 ‘40살쯤 되면 이걸로 돈 많이 벌겠다~’ 라는 생각을 하게되는 것 같다.

 

Web3, 그리고 crypto, 앞으로도 친하게 지냅시다.

반응형
반응형

들어가기에 앞서…

Compound v2를 구성하는 주요 기능에는 mint, redeem, borrow, repay, liquidate가 있다. 이러한 기능들이 어떻게 작동하는지, 더 나아가 어떠한 방식으로 구현되어있는지 등을 살펴보려 한다.

이번 편에서는 Compound에서 자금을 관리하는 cToken을 주요 기능을 살펴보려 한다. 중간중간 등장하는 추가적인 개념들은 이 글에서 가볍게 다루고, 추후에 더 자세히 살펴보는 것이 좋을 것 같다. 

각 기능에서는 CErc20/CEther 컨트랙트에서 상속받는 CToken 컨트랙트의 CToken::~Internal() 함수를 호출하고, 이는 CToken::~Fresh() 함수를 호출하며, 또 이는, Comptroller::~Allowed() 함수 및 Comptroller::~Verify()->Optional함수를 호출하는 흐름을 이해하고 있으면 더욱 편할 것 같다.

 


Mint(uint mintAmount)

이전 포스팅에서도 언급했던 것과 같이, mint(uint mintAmount)함수는 CErc20/CEther 컨트랙트에 구현되어있으며, cToken 컨트랙트의 mintInternal(uint mintAmount) 함수를 호출하여 실제 로직을 실행한다.

1. CErc20 & CEther::mint()

mint를 하기 위한 entry point인 CErc20과 CEther를 살펴보자.

function mint() external payable {
    mintInternal(msg.value);
}
function mint(uint mintAmount) override external returns (uint) {
    mintInternal(mintAmount);
    return NO_ERROR;
}

이러한 구현적 차이 이외에는 두 컨트랙트에서 별도의 차이가 존재하지 않는다.

CEther는 네이티브 Token을 처리하기 위한, CErc20은 ERC20 Token을 처리하기 위한 컨트랙트라는 정도만 이해하고 넘어가도 된다.

Logic

각각의 mint 함수에서는 상속받는 컨트랙트인 CToken의 mintInternal(mintAmount)를 호출한다.

2. CToken::mintInternal()

function mintInternal(uint mintAmount) internal nonReentrant {
    accrueInterest();
    mintFresh(msg.sender, mintAmount);
}

이 함수에서는 보이는 것과 같이 두 개의 주요 로직을 실행한다.

accrueInterest()

accrueInterest함수는 프로토콜이 현재 가지고 있는 token 수량, 사용자들의 borrow 총량, 준비금 등을 바탕으로 새로운 index 값을 계산, borrow 총량을 업데이트, 준비금을 업데이트 하는 로직을 수행한다. 이에 대한 자세한 로직은 다른 글을 통해 설명하였다.

 

Defi Lending 해체분석: Compound v2 - helper functions

들어가기에 앞서 ...Compound v2에서 사용되는 여러 helper function들의 로직을 정리한 포스팅이다. 사실 Lending 프로토콜에서 가장 중요한 것은 사용자들이 직접 호출하는 entry point라고도 할 수 있겠지

nullorm.tistory.com

accrueInterest() 실행 이후에 mintFresh() 함수를 실행하게 되는데, 

mint의 주요 로직이 구현되어있는 함수이다. 자세히 들어가보도록 하자.

3. CToken::mintFresh()

function mintFresh(address minter, uint mintAmount) internal;

mint와 관련된 주요 로직이 실행되는 mintFresh()함수이다.

 

차례대로 로직을 살펴보자.

 

mintFresh() - 1

uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount);

if (allowed != 0) {
    revert MintComptrollerRejection(allowed);
}

해당 cToken에 대해 minter가 mintAmount만큼 mint를 할 수 있는지 여부를 리턴해주는 mintAllowed 함수를 호출한다. (이러한 목적인 것 같기는 한데, 실제로 이렇지는 않다.)

3-1. CToken::mintAllowed

function mintAllowed(address cToken, address minter, uint mintAmount) override external returns (uint) {
    require(!mintGuardianPaused[cToken], "mint is paused");

    // Shh - currently unused
    minter;
    mintAmount;

    if (!markets[cToken].isListed) {
        return uint(Error.MARKET_NOT_LISTED);
    }

    updateCompSupplyIndex(cToken);
    distributeSupplierComp(cToken, minter);

    return uint(Error.NO_ERROR);
}
  1. guardian에 의해 pause되어있는지를 검사한다.해당 조건문은 이를 통해 pause되어있는지 검사하고, pause되어있다면 revert를 발생시킨다.
    compound에는 governance에 의해 임명된 guardian이라는 주체(wallet)이 있는데, 이들은 프로토콜상에 문제, 해킹, 또는 어떠한 사정이 있을 경우에 특정 cToken market의 기능을 pause할 수 있는 권한을 가지고 있다.
  2. compound만의 특징이라고 할 수 있을 것 같은데, ~Allowed 함수 내에 로직이 비어있는 것이 보인다. 추후 업데이트 시에 인터페이스가 변하지 않게 하려고 minter와 mintAmount를 함수 arg에 넣기만 하고 이용하지는 않은 것으로 보인다.
  3. 인자로 넣은 cToken의 주소가 market에 listing되어있는지 검사하여 입력값 검증을 실행하고 있는 것을 확인할 수 있다.
  4. updateCompSupplyIndex(cToken), distributeSupplierComp(cToken, minter) 을 통해 supplier에게 돌아가는 보상을 업데이트해준다. 상세한 내용은 보상 관련 포스팅을 추후 업로드 하도록 하겠다.

mintFresh() - 2

if (accrualBlockNumber != getBlockNumber()) {
    revert MintFreshnessCheck();
}

accrualBlockNumber 즉, accrueInterest()함수에서 이자율 및 관련 state변수들을 업데이트한 block number가 현재의 block.number와 같지 않다면 revert를 실행한다. 즉, accrueInterest함수를 실행하지 않고는 mintFresh()를 실행할 수 없다는 것.

 

mintFresh() - 3

Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()});

exchangeRateStoredInternal() 함수를 통해 exchangeRate를 불러온다.

function exchangeRateStoredInternal() virtual internal view returns (uint) {
    uint _totalSupply = totalSupply;
    if (_totalSupply == 0) {
        return initialExchangeRateMantissa;
    } else {
        uint totalCash = getCashPrior();
        uint cashPlusBorrowsMinusReserves = totalCash + totalBorrows - totalReserves;
        uint exchangeRate = cashPlusBorrowsMinusReserves * expScale / _totalSupply;

        return exchangeRate;
    }
}

cToken의 totalSupply, totalCash, totalBorrows, totalResserves를 바탕으로 exchangeRate즉, cToken과 underlying token의 교환비율을 반환해주는 함수이다.

 

사용처: underlying amount = cToken amount * exchangeRate

  1. totalSupply가 0보다 클 때
    totalSupply가 0보다 크면, cToken이 발행된 적이 있는 즉, mint수량이 있다는 말이다. 이 경우 exchangeRate는 $\frac{totalCash + totalBorrows-totalReserves}{totalSupply}$가 된다.
    즉, 프로토콜의 전체 자산(보유중인 token + borrow된 token - 준비금)을 총 cToken발행량으로 나눈 값이다.
  2. totalSupply가 0일 때
    totalSupply가 0이라는 것은, cToken 발행량이 0이라는 것이다. 즉, market에 유동성이 존재하지 않는 것이기 때문에, initialExchangeRateMantissa를 반환해준다. cETH의 intialExchangeRateMantissa값은 $2 \times 10^18$ 이다.

 

mintFresh() - 4

uint actualMintAmount = doTransferIn(minter, mintAmount);
uint mintTokens = div_(actualMintAmount, exchangeRate);

이제, doTransferIn(safeTransferFrom을 실행한다)을 통해 minter로부터 mintAmount만큼의 underlying token을 송금받고, 송금받은 token 수량인 actualMintAmount 을 exchangeRate로 나누어 mint 즉, 공급해야 할 cToken수량을 계산한다.

 

mintFresh() - 5

totalSupply = totalSupply + mintTokens;
accountTokens[minter] = accountTokens[minter] + mintTokens;

mint를 하는 절차.

totalSupply를 증가시켜주고, minter주소의 accountTokens를 증가시킨다.

 

mintFresh() - 6

emit Mint(minter, actualMintAmount, mintTokens);
emit Transfer(address(this), minter, mintTokens);

이벤트 로그를 발생시키고 함수가 종료된다.

 

mint 요약

  • mint(mintAmount)
  • mintInternal(mintAmount)
    • accrueInterest()
    • mintFresh()
      • mintAllowed()
      • mint logic
        • mintAmount만큼 doTransferIn()
        • mintAmount / exchangeRate 만큼 cToken mint

계속 추가할 예정

redeem(uint redeemTokens) & redeemUnderlying(uint redeemAmount)

borrow(uint borrowAmount)

repayBorrow(uint repayAmount)

liquidateBorrow(address borrower, uint repayAmount, CTokenInterface, cTokenCollateral)

반응형
반응형

들어가기에 앞서 ...

Compound v2에서 사용되는 여러 helper function들의 로직을 정리한 포스팅이다. 사실 Lending 프로토콜에서 가장 중요한 것은 사용자들이 직접 호출하는 entry point라고도 할 수 있겠지만, 실제로 사용자들의 자금을 관리, 이자를 업데이트, 프로토콜에 대한 관리 등을 수행하는 것은 이러한 helper function들이 있기에 가능한 것이기 때문에 따로 글을 빼 포스팅을 작성하였다.

목차 (Helper functions)

  • CToken::accrueInterest()
  • … (계속해서 추가해나갈 예정이다)

CToken::accrueInterest()

function accrueInterest() virtual public returns (uint);

역할

cToken의 borrow 및 deposit 자산에 대해 시간이 지남에 따라 발생하는 이자를 계산하고, 이를 기반으로 잔액, 이자율 및 관련 지표(borrowIndex, totalBorrows)를 최신 상태로 업데이트하는 역할을 한다. 

주요 기능들에서 로직을 실행하기 이전에 accrueInterest 함수를 실행하면서 시작한다.

변수 Update

  • accrualBlockNumber
  • borrowIndex
  • totalBorrows
  • totalReserves

로직 deep dive

accrueInterest() - 1

function accrueInterest() public returns (uint) {
    uint currentBlockNumber = getBlockNumber();
    uint accrualBlockNumberPrior = accrualBlockNumber;

    if (accrualBlockNumberPrior == currentBlockNumber) {
        return NO_ERROR;
    }
  1. ethereum mainnet의 compound v2는 block.timestamp가 아닌, block.number를 기반으로 이자 등 관련 지표를 관리한다. 따라서 이전 업데이트 이후 발생한 이자를 계산하기 위해 현재 block.number값을 currentBlockNumber에 받아온다.
  2. 전역으로 저장된 가장 최근에 Index가 업데이트된 block.number를 accrualBlockNumberPrior변수에 저장한다.
  3. currentBlockNumber == accrualBlockNumberPrior 를 비교하여 같다면 return을 시켜준다.
    block number를 기반으로 state 업그레이드가 발생하기 때문에 같은 block에서 이미 update가 되었다면 여러 index들을 새로 업데이트해줄 필요가 없기 때문.

문득, (3)의 내용이 gas를 얼마나 줄여주는지 궁금해져 foundry testcode를 작성해 테스트를 진행해보았다.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";

contract CounterTest is Test {
    testCompoundV2Gas target;

    function setUp() public {
        vm.createSelectFork("eth_mainnet");
        target = new testCompoundV2Gas();
    }

    function test_compoundV2GasOp() public {
        console.log(target.getGasAmount());
        console.log(target.getGasAmount());
    }
}

contract testCompoundV2Gas {
		// cETH address
    address cETH= 0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5;
    
    function getGasAmount() public returns (uint gasUsed) {
        gasUsed = gasleft();
        (bool suc, ) = cETH.call(abi.encodeWithSignature("totalBorrowsCurrent()"));
        require(suc, "function call failed");

        gasUsed = gasUsed - gasleft();
    }
}

Result

test 결과를 통해 알 수 있듯, 약 40000 wei 즉, $0.7884 (24.11.16 기준)의 gas 최적화가 되는 것을 알 수 있다.

 

계속 해보자.

accrueInterest() - 2

uint cashPrior = getCashPrior(); // cToken컨트랙트의 underlying balance
uint borrowsPrior = totalBorrows; // underlying token을 user들이 borrow한 총량
uint reservesPrior = totalReserves; // 총 준비금
uint borrowIndexPrior = borrowIndex; // 현재 borrowIndex

이와 같은 네 개의 memory변수에 state값들을 저장하는 것을 확인해볼 수 있다. 자세한 내용은 주석 참고

function getCashPrior() internal view returns (uint) {
    EIP20Interface token = EIP20Interface(underlying);
    return token.balanceOf(address(this));
}

getCashPrior는 이와 같이 cToken 컨트랙트의 underlying token 잔액(balance)를 불러온다.

 

accrueInterest() - 3

uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior);
require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high");

여러 변수들을 업데이트 해주기 위해 compound v2는 현재의 underlying balance, 빌린 금액, 준비금 총액을 이용해 borrowRate를 계산해주게 된다.

interestRateModel::borrowRate()함수를 살펴보자.

function getBorrowRate(uint cash, uint borrows, uint reserves) public view returns (uint) {
    uint ur = utilizationRate(cash, borrows, reserves);
    return (ur * multiplierPerBlock / BASE) + baseRatePerBlock;
}

utilizationRate 즉, 활용율을 계산한 뒤 여러 가중치를 반영한 borrowRate를 리턴해주는 함수이다.

활용율은 전체 예치(deposit)된 자산 중, borrow에 쓰인 비율을 나타내는 비율이라고 알면 된다. 아래 코드를 보면 바로 이해가 될 것이다.

function utilizationRate(uint cash, uint borrows, uint reserves) public pure returns (uint) {
    // Utilization rate is 0 when there are no borrows
    if (borrows == 0) {
        return 0;
    }

    return borrows * BASE / (cash + borrows - reserves);
}

이러한 활용율에 multiplierPerBlock(자산 이용율에 이자율의 증가 속도를 제어하는 상태변수) 를 곱한 후, 최소 이자율인 baseRatePerBlock을 더해주어 최종적으로 borrowRate를 반환해준다.

이렇게 얻은 borrowRate는 borrowRateMax값보다 작아야함을 검사하며 조건검사를 끝낸다.

 

accrueInterest() - 4

uint blockDelta = currentBlockNumber - accrualBlockNumberPrior;
Exp memory simpleInterestFactor = mul_(Exp({mantissa: borrowRateMantissa}), blockDelta);
uint interestAccumulated = mul_ScalarTruncate(simpleInterestFactor, borrowsPrior);
uint totalBorrowsNew = interestAccumulated + borrowsPrior;
uint totalReservesNew = mul_ScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior);
uint borrowIndexNew = mul_ScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior);

다소 복잡하고 어질어질해보이는데, 각 internal function들에 대해 가볍게 짚고 넘어가보는걸로 해보자.

 

피연산자(A, B, C)

  • mul_(A, B): A * B
  • mul_ScalarTruncate(A, B): A * B / BASE (BASE스케일만큼 스케일링)
  • mul_ScalarTruncateAddUInt(A, B, C): A * B / BASE + C

이를 이용해 위 계산식들을 수식으로 표현해보면,

uint blockDelta = currentBlockNumber - accrualBlockNumberPrior;
Exp memory simpleInterestFactor = borrowRateMantissa * blockDelta
uint interestAccumulated = simpleInterestFactor * borrowsPrior / BASE
uint totalBorrowsNew = interestAccumulated + borrowsPrior;
uint totalReservesNew = reserveFactorMantissa * interestAccumulated + reservesPrior
uint borrowIndexNew = (simpleInterestFactor + 1) * borrowIndexPrior

이와 같이 나타낼 수 있다. 이제 로직을 살펴보자.

  • blockDelta: 이전 accrueInterest시점으로부터 지금까지 지난 block 수를 계산한다.
  • simpleInterestFactor: borrowRate에 blockDelta를 곱해 UtilizationRate에 따른 이자율에 시간 스케일을 반영한 값을 계산한다.
  • interestAccumulated: simpleInterestFactor에 borrow된 총량을 곱해주어 현재까지 대출된 총액에 추가로 발생한 이자를 반영해준다. 이를 통해 누적된 이자를 계산한다.
  • totalBorrowsNew: interestAccumulated에 borrowsPrior를 더해 총 부채를 업데이트해준다.
  • totalReservesNew: 준비금을 업데이트해준다. 여기서 사용된 reserveFactorMantissa값은 거버넌스에 의해 설정된 값이다.
  • borrowIndexNew: 마지막으로, borrowIndex를 simpleInterestFactor만큼의 증가분을 반영하여 업데이트 해준다.

accrueInterest() - 5

accrualBlockNumber = currentBlockNumber;
borrowIndex = borrowIndexNew;
totalBorrows = totalBorrowsNew;
totalReserves = totalReservesNew;

emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew);

return NO_ERROR;

 

마지막으로, 계산된 memory변수들을 state에 반영한다.

event로그를 발생시키면서, NO_ERROR를 반환하여 로직을 종료한다.

아래는 전체 코드이다.

accrueInterest()

function accrueInterest() virtual override public returns (uint) {
    /* Remember the initial block number */
    uint currentBlockNumber = getBlockNumber();
    uint accrualBlockNumberPrior = accrualBlockNumber;

    /* Short-circuit accumulating 0 interest */
    if (accrualBlockNumberPrior == currentBlockNumber) {
        return NO_ERROR;
    }

    /* Read the previous values out of storage */
    uint cashPrior = getCashPrior();
    uint borrowsPrior = totalBorrows;
    uint reservesPrior = totalReserves;
    uint borrowIndexPrior = borrowIndex;

    /* Calculate the current borrow interest rate */
    uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior);
    require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high");

    /* Calculate the number of blocks elapsed since the last accrual */
    uint blockDelta = currentBlockNumber - accrualBlockNumberPrior;

    /*
     * Calculate the interest accumulated into borrows and reserves and the new index:
     *  simpleInterestFactor = borrowRate * blockDelta
     *  interestAccumulated = simpleInterestFactor * totalBorrows
     *  totalBorrowsNew = interestAccumulated + totalBorrows
     *  totalReservesNew = interestAccumulated * reserveFactor + totalReserves
     *  borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex
     */

    Exp memory simpleInterestFactor = mul_(Exp({mantissa: borrowRateMantissa}), blockDelta);
    uint interestAccumulated = mul_ScalarTruncate(simpleInterestFactor, borrowsPrior);
    uint totalBorrowsNew = interestAccumulated + borrowsPrior;
    uint totalReservesNew = mul_ScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior);
    uint borrowIndexNew = mul_ScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior);

    /////////////////////////
    // EFFECTS & INTERACTIONS
    // (No safe failures beyond this point)

    /* We write the previously calculated values into storage */
    accrualBlockNumber = currentBlockNumber;
    borrowIndex = borrowIndexNew;
    totalBorrows = totalBorrowsNew;
    totalReserves = totalReservesNew;

    /* We emit an AccrueInterest event */
    emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew);

    return NO_ERROR;
}

 

 

이후에 계속 helper function 들을 추가해야겠다.

반응형
반응형

들어가기에 앞서..

이번 편에서는 compound v2의 전반적인 구조를 알아보고자 한다.

1편과 다소 순서가 뒤바뀐 면이 없지않아 있는 것 같긴 한데, 1편의 내용은 compound에서 사용되는 핵심적인 개념들에 대한 설명이었기 때문에 필수적이었고, 이 또한 전체적인 구조를 파악할 수 있기 때문에 필수적인 것 같다. 그래서 어느 것이 먼저인지는 중요하지 않은 것 같다. 이번 편에서 전체적인 구조를 살펴보고, 다음편 부터 코드레벨까지 뜯어보며 프로토콜 전반에 대한 내용을 살펴보도록 하자.

시작해보자.


Compound v2 구조

출처:  https://deephigh.gitbook.io/deephigh/architecture/compound

위 그림은 업사이드아카데미 1기 - DeepHigh팀으로 활동하면서 만든 Compound v2의 전체적인 호출 flow이다. 위 도식에는 Compound v2를 구성하는

  1. external entities
  2. internal storage
  3. contract 및 function

의 여러 동작flow가 담겨 있다. 모든 flow에서 어떻게 동작하는지에 대한 조금 더 자세한 내용을 살펴보기 위해 일단 end user들과 가장 가까운 파트부터 보는 것이 가장 직관적으로 와닿을 것 같아 그렇게 순서를 구성하였다.

순서

  1. 전체 구조 Overview
  2. cToken
  3. Comptroller
  4. Governance
  5. Price Oracle

전체 구조 Overview

Compound v2의 핵심적인 기능들은 아래와 같이 구성되어있다고 볼 수 있다.

근거

  • cToken: 사용자가 예치한 자산에 대한 권리 & 예치 자산에 대한 이자 수익 반영하는 토큰.
    • CErc20/cEther: cToken의 서로 다른 두 형태
    • cToken: 대출, 예치, 청산 등 기능을 수행하는 컨트랙트
  • Comptroller: 대출, 예치, 청산 등 기능을 관리하고 담보 비율 및 이자율 모델 등을 조정하는 중앙 관리 역할.
    • Unitroller: Comptroller의 로직을 분리하여 업그레이드 가능하게 만든 프록시 컨트랙트
    • Comptroller: 본체
  • Governance: Comp 토큰을 보유한 사용자들이 프로토콜의 주요 정책(이자율 모델, 담보 요건, 새로운 자산 추가 등)을 propose하고 vote할 수 있도록 하는 역할.
    • Comp: 거버넌스 토큰. 보유자는 propose 및 vote에 참여해 프로토콜 운영에 참여 가능.
    • Governor: 거버넌스의 핵심 컨트랙트. propose, vote 진행을 담당해 처리.
    • Timelock: 승인된 거버넌스 propose가 실행되기 전에 일정 시간 지연을 적용하는 컨트랙트
  • Price Feed: compound v2는 chainlink에서 feed data를 받아온다.

이제, 위의 각 구성요소들이 어떠한 역할&기능을 수행하는지 알아보도록 하자.


cToken

프로토콜에 예치(또는 공급)된 자산을 EIP-20표준에 맞춰 표현한 개념으로, Compound v2의 핵심 개념이다.

 

그래서 이걸로 뭐하는데

  1. cToken은 사용자가 공급한 자산의 가치를 나타낸다. 이를 통해 이자를 얻을 수 있다(예금)
  2. cToken을 담보로 대출 등 작업을 수행할 수 있다. (대출)

cToken은 Compound와 user가 상호작용하는 주요 수단이다.

사용자가 담보 입금(mint), 출금(redeem), 대출(borrow), 상환(repay), 청산(liquidate), 또는 전송(transfer)을 할 때 cToken 컨트랙트를 통해 이루어진다.

EIP-20표준을 따른다는 것을 통해 알 수 있듯, 내가 입금한 담보자산을 제3자에게 전송하여 제3자가 이를 이용해 대출 등의 작업을 할 수 있기도 하다.

CErc20/CEther

이들은 각각 cToken컨트랙트를 상속받는 user entry point이다. 컨트랙트의 주요 기능에 대한 Overview를 살펴보자.

functions visibility description
mint external 담보 입급하여 cToken발행
redeem external cToken소각하여 담보 인출(cToken기준)
redeemUnderlying external cToken소각하여 담보 인출(underlying기준)
borrow external cToken을 담보로 대출
repayBorrow external 대출받은 자산을 상환
repayBorrowBehalf external A가 대출받은 자산을 B가 상환
liquidateBorrow external A가 대출받은 자산을 C가 청산(청산보너스 get)
sweepToken external underlying이 아닌 token이 컨트랙트에 존재하면 해당 token을 sweep
doTransferIn internal EIP20 transferFrom
doTransferOut internal EIP20 transfer

두 컨트랙트 모두 위와 같은 기능들을 제공한다. 차이는 오직 Native ETH를 다루는가, ERC20 token을 다루는가의 차이.

예시

CErc20
CEther

cToken flow

end-user가 어떠한 entry point를 통해 프로토콜과 상호작용하는지 알아봤으니, cToken의 로직들이 어떻게 동작하는지 알아보자.

출처: https://deephigh.gitbook.io/deephigh

위의 그림을 보며, 예시로 mint함수를 들어 따라가보도록 하자. (다른 함수들도 다 똑같다.)

  1. cErc20/CEther를 통해 user가 mint(uint mintAmount)를 호출하면,
  2. mint는 internal call을 통해 mintInternal(mintAmount)를 호출한다.
  3. mintInternal는 accrueInterest()를 통해 다음 네 가지 state variable을 업데이트한다.
    1. borrowIndex: 이자계산에 사용되는 Index 업데이트
    2. totalBorrows: borrow된 총량 업데이트
    3. totalReserves: 준비금 업데이트
    4. accrualBlockNumber: 마지막으로 위의 값들을 업데이트한 block number
  4. 업데이트 이후, mintFresh를 통해 mint작업에 들어간다.
    1. comptroller.mintAllowed(address(this), minter, mintAmount)를 통해 minter가 mintAmount만큼 mint할 수 있는지 체크하고, CompSupplyIndex를 업데이트하며, Supplier들에게 comp를 distribute한다.
    2. 이후, 실제mint와 관련된 로직이 실행된다.

cToken의 다른 기능에 대한 로직(mint, redeem, borrow, repay)또한 위와같은 방식으로 실행된다.

하지만 liquidate에는 약간의 차이가 존재한다. 이 차이를 이해하기 위해서는 청산, 나아가 Compound의 청산에 대한 이해가 조금 필요하니 간단하게 짚고 넘어가보자.

liquidate 핵심만 빠르게

function liquidateBorrow(address borrower, uint repayAmount, CTokenInterface cTokenCollateral)

liquidate함수의 입력값은 1. 청산의 대상인 borrower, 2. 청산 규모인 repayAmount, 3. 담보자산의 cTokenInterface이 입력된다.

  1. 청산(liquidate)은 대출자(borrower)의 담보를 “싸게 파는”행위이며, 이를 통해 프로토콜의 재정적 안정성을 유지하고자 하는 기능이다. (처음 들으면 무슨말인지 잘 이해가 안된다. 일단 계속 읽자)
  2. borrower가 대출(borrow)을 할 때, 담보자산(cToken)들의 총 가치가 borrow하고자 하는 자산의 총 가치보다 커야한다.
  3. 담보자산의 가치가 빌린 자산의 가치보다 작아지는 현상(shortfall)이 발생하면, 청산의 대상이 된다.
  4. **청산인(liquidator)**은 이를 발견하고, cToken마켓에 liquidateBorrow를 실행하여 borrower의 **담보자산(cTokenCollateral)**을 청산한다.
  5. 이 때, repayAmount만큼의 “빌린 자산”을 청산하게 되고, repayAmount만큼의 가치에 할인율(liquidationIncentiveMantissa)을 곱한 수량의 담보자산(cTokenCollateral)을 그 대가로 받아 청산에 대한 보상을 얻게 된다.

그래서 liquidate는 어떤 flow로?

사실 flow자체는 비슷할 수 있는데, 위의 내용들을 토대로 하여, borrower가 빌린 자산과, 청산의 대상이 되는 borrower가 담보로 맡긴 자산 모두에 대해 이자율 업데이트 등 작업을 수행해야하는 것을 알 수 있다. 따라서, 그러한 차이가 존재한다.

실제 liquidate 로직 그리고 cToken 기능들에 대한 전반적인 세부사항은 다음에 올릴 포스팅에서 함께 확인해보도록 하자.

Comptroller

comptroller의 사전적 정의를 살펴보면, 재정 관리자라는 뜻으로 주로 사용되는 것 같다. Compound에서도 마찬가지이다.

Comptroller는 Compound v2의 리스크 관리계층으로, 사용자가 유지해야 할 담보비율을 결정하고, 사용자의 재무건전성을 판단한다. 재무건전성은 청산과 직접적으로 연관되며 사용자가 청산될 있는지, 그리고 얼마나 청산될 수 있는지를 판단하는 데 사용된다.

또한 사용자가 cToken과 상호작용할 때 마다 거래가 타당한지를 검토하는 데 사용된다.

Comptroller는 업그레이드 가능한 컨트랙트로, Unitroller라는 별도의 프록시컨트랙트에서 ComptrollerStorage와 Comptroller를 관리한다.

이제, 어떠한 기능들을 수행하는지 전체적으로 살펴보도록 하자.

Comptroller

enter/exitMarket(s)

function enterMarkets(address[] calldata cTokens)

함수 이름 그대로 market(들) 목록에 들어가는 함수이다. 특정 market에 예치한 자산이 담보로 인정되도록 하거나, 특정 market에서 자산을 borrow하기 위해 enterMarket을 해야 한다.

반대로 exitMarket은 이름 그대로 특정 market에서 exit하는 함수이다.

Allowed Functions

cToken컨트랙트에서 각종 Fresh함수들(mintFresh, redeemFresh, ….)은 내부적으로 accrueInterest()를 호출한 이후, comptroller의 ~Allowed 함수(mintAllowed, redeemAllowed, …)를 호출하게 된다. ~Allowed 함수에서는

  1. 해당 기능에 들어가는 여러 Input value를 검증하고
  2. updateCompSupplyIndex() 를 통해 보상과 관련된 Index를 업데이트하며
  3. distributeSupplierComp() 를 통해 supplier에게 comp를 분배한다.

Verify Functions

cToken컨트랙트에서 각종 Fresh함수들(mintFresh, redeemFresh, ….)은 내부적으로 ~Allowed 함수를 호출해 Input값들을 검증한 후, 함수 로직을 수행한 다음, 마지막 단계에서 ~verify함수를 호출한다.

현재 verify함수는 사용되지 않지만, 추후에 다시 사용될 수도 있기에.. 일단 기록한다.

mintFresh() 내부로직

각종 변수 및 유틸리티

  • collateralFactor이 값은 Governance에 의해 증/감 할 수 있다.
  • 기능이라기보다는 값에 가까운데, 공급한 담보자산의 실제 담보가치(대출 한도)의 비율을 나타내는 값이다. 대형자산(ETH 등), 유동성 높은 자산은 collateralFactor가 크고, 반대는 작다. 0%가 되면 해당 자산은 담보로서 활용할 수 없지만 borrow는 가능하다.
  • getAccountLiquidity
  • 특정 계정이 사용할 수 있는 유동성의 총 USD가치를 계산한다. 기본적으로 enterMarket을 통해 listing되어있는 담보 및 대출 자산에 대해서만 그 값을 계산한다. 또한, 사용할 수 있는 자산의 총량이 0보다 작아지면 shortfall이 생겨 해당 계정은 청산 가능한 상태에 도달하게 되고, 이에 따라 shortfall값도 리턴해준다.

Governance

Compound는 COMP토큰 보유자가 세 가지 컨트랙트(COMP, Governor Bravo, Timelock)를 컨트롤하며 관리 및 업그레이드된다.

이 컨트랙트들은 커뮤니티가 propose, vote를 통해 cToken, Comptroller의 관리기능을 통해 변경사항을 실행할 수 있도록 한다.

또한 COMP토큰 holder는 자신의 투표권을 다른 주소에 delegate할 수 있어 25,000 COMP이상 delegatae받은 주소는 제안을 propose할 수 있고, 누구든 100 COMP를 lock하여 propose를 생성할 수 있다.

Governance Timeline

출처: Compound

  • 최초 Propose시, 2일간의 review기간이 주어지며,
  • 이후 투표 가중치가 기록되고 투표가 시작된다.
  • 투표는 3일간 진행되고,
  • 다수이면서 최소 400,000표가 찬성하면 Timelock에 pending상태로 추가되고,
  • 해당 propose는 2일 뒤에 실행된다.

결과적으로, 프로토콜의 변경 사항은 최소 일주일이 소요된다.

Price Oracle

프로토콜의 여러 자산 가격 데이터를 Chainlink Price Feed를 통해 제공받는다.

이에 대한 코드베이스: https://github.com/smartcontractkit/open-oracle/blob/master/contracts/PriceOracle/PriceOracle.sol

  • multisig를 통해 price feed주소가 업데이트될 수 있음
  • getUnderlyingPrice함수를 통해 자산의 USD가격을 반환받을 때, 10^(36 - underlying.decimals())로 스케일 업된다. 예를들어 WBTC는 decimals가 8이므로, 반환값은 1e28이 됨.

여기까지 Compound v2의 기본적인, 그리고 전체적인 구조를 살펴보았다. 다음 글부터는 드디어 코드를 해체분석해보도록 하자.

반응형
반응형

들어가기에 앞서..

설명하지 않아야 할 부분까지 설명하는것은 귀찮으니 필요한 부분이나 빌드업을 위한 내용정도만 작성해보겠다.

이번 편에서는 compound v2에서 핵심 기능을 구현하기 위해 어떠한 내용을 고려했는지, 또 어떻게 구현했는지를 알아보도록 하자.

어떠한 방식으로 compound와 end-user가 소통하는지, 어떠한 수학적 로직이 사용되는지를 먼저 알아보고, 각종 기능들에 대해서는 다음편에서 알아보도록 하자.


Compound v2

compound프로토콜은 2019년에 출간된 Compound whitepaper(https://compound.finance/documents/Compound.Whitepaper.pdf)를 기반으로 하며, 오픈소스, 그리고 커뮤니티에 의해 관리된다. 이번 포스팅에서는 compound의 가장 기본적인 기능을 다루는 compound v2를 다뤄보도록 하겠다. 

제공하는 기능 오버뷰

  • Lending의 기본 기능
    • 담보입금(mint): 담보를 입금하고 이에 대한 cToken을 받는다
    • 담보출금(redeem): cToken을 돌려주고 이에 해당하는 양의 담보(underlying token)을 돌려받는다.
    • 대출(borrow): 담보의 가치(cToken의 가치)에 따라 LTV를 곱한 가치만큼의 자산을 빌릴 수 있다.
    • 상환(repay): 빌린 자산을 상환한다.
    • 청산(liquidation): 사용자가 borrow한 가치가 담보가치보다 커질 경우, 담보를 청산한다.

이외에도 거버넌스 등 기능이 있지만 그건 자세한 설명에서 다뤄도 괜찮을 것 같다.

 

아래 링크를 통해 실제 배포되어있는 각종 컨트랙트 주소들을 확인할 수 있다.

https://github.com/compound-finance/compound-config

 

GitHub - compound-finance/compound-config: Compound Protocol Configuration for Developers

Compound Protocol Configuration for Developers. Contribute to compound-finance/compound-config development by creating an account on GitHub.

github.com

Compound v2 Diagram

프로젝트를 진행하며 그렸던 compound v2의 함수호출 diagram이다.

출처:  https://deephigh.gitbook.io/

  1. 각 엔드유저들은 CErc20, CEther 컨트랙트를 통해 프로토콜과 상호작용하고,
  2. CErc20, CEther는 핵심 기능이 구현되어있는 cToken컨트랙트와 상호작용하여 각종 기능들을 수행한다.
  3. 또한 각 cToken들은 comptroller라고 불리는 프로토콜 관리자 컨트랙트와 상호작용하며 각종 조건검사 등을 수행하게 되는 과정으로 프로토콜이 작동하는 것을 확인해볼 수 있다.

compound v2의 구조에 대한 내용은 바로 다음 포스팅에서 더 자세히 살펴보도록 하자.

이번 포스팅에서는 Compound에서 사용되는 수학적 개념 등 가장 기초가 되는 내용들을 먼저 다루어보겠다.

Protocol Math

compound는 프로토콜에서 사용되는 각종 계산의 정밀도를 위해 ExponentialNoError.sol 라이브러리를 사용한다.

많이들 아는 것과 같이, Solidity에서는 소수점 계산이 부동소수점 방식이 아닌 고정소수점 연산을 이용한다(아무래도 소수계산 자체가 존재하지 않으니..). 따라서 정밀하거나 중요한 연산을 수행할 때 계산 로직이 왜곡될 수 있다는 위험이 존재한다.

아래 예시와 같이 나눗셈 → 곱셈의 순서로 연산을 하거나, 혹은 그렇게 했더라도 나눠지는 수의 스케일과 나누는 수의 스케일이 비슷할 경우 계산 결과의 왜곡이 생길 수 있다.

1200 * 5 / 1000 = 6
1200 / 1000 * 5 = 5

 

따라서, 이러한 위험을 방지하기 위해, 대부분 연산에서 Mantissa를 이용해 1e18을 곱한 수로 연산을 수행한다.

(Mantissa이외에 Double(1e36)이라는 개념도 사용된다.)

cToken, Underlying Token의 Decimals

모든 ERC20, Native Token의 Decimal은 18로 통일되어있는 것이 아니라 모두 다른 decimal을 이용하고 있다. 예를들어 USDC의 decimal은 6이고, WBTC는 8이다. 그리고 ETH는 18이다.

이처럼 모든 토큰별로 매핑되는 각 cToken의 가치와 관련된 계산을 할 때, 모든 Underlying Token의 decimal을 고려하기는 다소 불편한 감이 있다. 따라서 compound에서는 모든 cToken의 decimal을 아래와 같이 6으로 고정해 이용한다.

사진출처: https://docs.compound.finance/v2/#networks

하지만, 이럴 경우 underlying token과 cToken간 decimal 차이가 발생하여 계산과정에서 불편함이 생길 수 있다. compound에서는 이를 다음과 같은 방법으로 계산하여 해소한다.

exchangeRate = (getCash() + totalBorrows() - totalReserves()) / totalSupply()
// getCash: underlying.balanceOf(address(this))
// totalBorrows: User들이 borrow한 총량
// totalReserves: 준비금 총량
// totalSupply: cToken 총 발행량
underlyingTokens = cTokens * exchangeRate / 10^(18+underlyingDecimals-cTokenDecimals)

Calculate Accrual Interest

실제 은행에서도 그렇지만, web3 Defi에서도 이자율이자는 정말 중요하다. 사용자들이 납부해야 할 이자와, 예치자들에게 돌려줄 수익이 보장되고, 정확히 계산되어야 시장이 건전하게 돌아갈 수 있으니 말이다.

그렇다면, 이러한 이자는 어떻게 업데이트해주는걸까? web3에서는 모든 계산이 user가 호출하는 로직에서 수행되고, 매 호출마다 supply, borrow를 한 모든 user의 이자가 업데이트된다면 user들은 프로토콜을 이용하려하지 않을 것이다. (gas fee감당불가) 이를 해결하기 위해, compound에서는 index라는 개념을 통해 이자를 누적시킨다. index가 어떻게 사용되는지, 그리고 어떻게 계산되는지를 알아보자.

계산 로직

function accrueInterset() {
	...
	borrowRate = interestRateModel.getBorrowRate(cash, borrows, reserves)
	...
	simpleInterestFactor = borrowRate * blockDelta // 마지막 업데이트 이후 지난 블록 수 
	borrowIndex = borrowIndex * (simpleInterestFactor + 1)
	...
}

baseRatePerYear: The approximate target base APR, as a mantissa (scaled by BASE) : 연이자율

multiplierPerYear: The rate of increase in interest rate wrt utilization (scaled by BASE) : Utilization 가중치

function getBorrowRate(cash, borrows, reserves) returns (borrowRate){
	ur = borrows * 1e18 / (cash + borrows - reserves)
	borrowRate = ur * multiplierPerBlock / BASE + baseRatePerBlock
}

말로 풀어보자.

우선, borrowRate는 baseRate에 utilizationRate에 multiplier를 곱한 값을 더한 결과이다.

utilizationRate(ur)는 활용률 즉, 프로토콜에 예치된 자산 중 대출에 활용된 자산의 비율을 말하며, 활용이 많이 되고 있다는 것은 해당 자산이 많이 사용된다는 말이므로, 이에 대한 borrowRate가 커지게 된다.

이를 통해 도출된 borrowRate와 마지막 업데이트 이후 지난 블록 수(blockDelta)를 곱해 simpleInterestFactor를 계산하고, 현재 borrowIndex에 이 값을 곱한 후 borrowIndex에 더하여 업데이트한다.

이러한 방식으로 borrowIndex는 처음 세팅 이후, 예치된 자산의 활용율에 따라 점점 커지는 방식이라고 생각할 수 있다.

활용 로직

function borrowBalanceStoredInternal() returns (borrowBalance) {
  ...
  borrowSnapshot = accountBorrows[account]
  borrowBalance = borrowSnapshot.principal * borrowIndex / borrowSnapshot.interestIndex
  ...
}

user가 borrow한 token의 이자를 포함한 총량을 계산하는 과정에서 사용된다.

borrow 원금에 현재 borrowIndex를 곱한 후, borrow했을 때의 index(애매한데, 일단 그렇다 칩시다)로 나눠주면 borrow한 총량을 나타낼 수 있다.

이처럼, 매 호출마다 모든 유저의 이자를 업데이트해주는 것이 아닌, index값만 업데이트해주고, borrow한 총량을 알고싶을 때 index값과, borrow할 때의 index값만 알면 borrowBalance를 알 수 있으니 간단하게 최적화 문제를 해결할 수 있다.

 

ref

https://docs.compound.finance/v2

 

Compound v2 Documentation

Getting Started Introduction to Compound v2 The Compound protocol is based on the Compound Whitepaper (2019); the codebase is open-source, and maintained by the community. The app.compound.finance interface is open-source, and maintained by the community.

docs.compound.finance

https://deephigh.gitbook.io/

 

Welcome | DeepHigh

Welcome

deephigh.gitbook.io

반응형
반응형

 

4개월간의 업사이드아카데미 1기 DeepHigh팀 팀장으로서의 활동을 끝내고, 어떤 공부를 해야할지를 고민해보다가 프로젝트 내용에 대해 내가 정말 완벽히 이해하고있는가를 생각해보았다.

작업했던 모든 산출물을 완벽하게 이해하고있는지에 대해 우선적으로 생각해 보았고, 더 공부하고 발전시킬 부분이 있다고 생각하였다.

프로젝트의 주제는 Defi Lending Protocol Audit이었으며, 이에 대한 산출물은 EVM기반 Lending 프로토콜에 대한 위협모델링 및 관련 자료들이다. 구체적인 내용은 아래 내용을 통해 알 수 있다. 

https://deephigh.gitbook.io/deephigh

 

Welcome | DeepHigh

Welcome

deephigh.gitbook.io

실제 위협모델링을 통해 Lending 프로토콜상의 취약점을 찾아보고자 했지만, 정말 사소한 몇 가지 말고는 찾지 못한 것이 다소 아쉽다.

업사이드아카데미 활동에 대한 내용은 추후 시간이 된다면 작성해보고싶다.


내가 생각하는, 프로젝트 기간동안(교육기간 제외) 공부 및 산출한 결과는 다음과 같다.

 

공부 (docs, code 포함)

  1. EVM-based Lending 생태계에 대한 전반적인 이해
  2. Compound v2, Venus protocol에 대한 90% 이해
  3. Euler v2에 대한 70%정도의 이해
  4. BIFI (Bifrost Lending), BendDAO v2에 대한 30~50% 이해

산출물

  1. Defi Lending protocol threat modeling framework 제시 및 threat modeling
  2. Lending Invariant Checklist 제시
  3. 위협모델링, Checklist에 대한 Validation

이처럼, Lending Protocol에 대해, 그리고 다소 정책(?)에 대한 내용인 위협모델링이라는 것에 대해 공부해보면서, Defi에 대한 정말 전체적인 공부를 해보고싶은 생각이 들었다.


본인은 이제 3개월정도 뒤면 병역의무를 수행하러 군대에 가야하는데, 그 전까지 최대한 많은 공부를 해보고싶다는 생각이 들었다. 그래서 내가 어떠한 것을 더 배워야할지 정리해보았다.

  1. 공부한 내용 기록 남기기
    1. Compound v2는 무조건 남길 것 같다. (마지막에 별첨 느낌으로 Venus도 다루지 않을까?)
    2. Euler v2도 남길 것 같다. (compound v2와 구조적으로 다르면서 독특한 특징이 여럿 있어 꽤나 흥미로운 녀석이다.)
  2. 공부할 내용 남기기
    1. Aave v3 (TVL 1위인 프로토콜은 보는게 맞지…)
    2. Leverage Lending, NFT 담보대출 등 Lending의 특수기능을 구현한 프로토콜에 대해 공부할 것 같다.

본 스터디에 대한 나의 목적은, 공부한 프로토콜에 대해 누군가 질문을 했을 때, 해당 질문에 대한 정확한 설명, 그리고 어떻게 구현되어있는지까지 모든 것을 답할 수 있을 정도로 공부를 해보고자 한다.

 

추가하고 싶은 내용, 추가하지 않아도 될 것 같은 내용 등이 있으면 언제든 이 글을 수정해보도록 해야겠다.

많은 사람들, 특히 한국인들이 내 포스팅을 통해 Lending protocol에 대한 흥미를 키울 수 있다면 좋을 것 같다.

 

대략적인(예상되는) 앞으로의 포스팅 순서

1. Compound v2: 아무래도 나는 보안공부를 하는사람이니... Compound v2 fork에서 발생했던 사고사례분석도 더해볼까 한다.

2. Aave v3

3. Euler v2

4. BendDAO (이건 안할수도 있을 것 같다.)

반응형
반응형

블록은 이전블록의 해시(prevHash라고 부르도록 하겠다.)와 트랜잭션의 묶음이라고 생각할 수 있다. 해시는 블록의 데이터를 통해 계산되기 때문에 체인의 형태로 구성될 수 있다. 만약, 블록이 생성된 이후에 블록의 데이터를 변경하게 되면, 해당 블록의 블록해시(block hash)가 바뀌고, 이는 이후 생성된 다른 블록들에 영향을 주어 모든 검증자들이 알아차릴 수 있기 때문에, 다음 블록이 모두 무효화되어 임의 변조를 막을 수 있다.

네트워크의 모든 참가자들이 동기화된(Syncronized) 상태(State)를 유지하고, 모든 트랜잭션에 동의를 하기 쉽게 하기 위해 다수의 트랜잭션들을 한 개의 블록으로 묶어서 Commit, agree, Syncronize를 한 번에 처리한다.

출처:  https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf

 

모든 블록들이 적절하게 검증을 받을 수 있도록, 네트워크는 참여자들에게 검증을 할 수 있는 충분한 시간을 부여한다. 트랜잭션은 초당 수십~수백개씩 발생할 수 있지만, 블록은 12초에 한 번 이더리움에서 생성되고 commite된다.

즉, 블록이 없다면, 모든 validator(트랜잭션(혹은 블록)을 검증하는 객체)는 매 초 모든 트랜잭션을 검증해야하며, 네트워크 상태에 따라 트랜잭션이 누락되는 경우에도 이를 검증하고, 블록을 네트워크에 추가해야하기 때문에, 블록을 사용할 때에 비해 Fork의 수가 수없이 많아지게 될 것이다.

(이정도면 블록이 있는 이유에 대해 충분히 알아본 것 같다…!)

블록의 작동 방식

transaction history를 보존하기 위해 블록은 상위 블록에 대한 참조를 가지고있어야 하며(prevHash에 대한 정보가 기록되어야 함), 블록 내에 있는 트랜잭션 또한 엄밀한 과정을 거쳐 블록에 정렬된다.

→ 추후에 더 알아볼 예정

PoS시스템에서, 네트워크에서 무작위로 선택된 검증자(PoS에서는, Proposer라고 부른다.)가 블록을 빌드하면, 이를 전체 네트워크에 Broadcast하게 되고, 합의의 과정 이후에 모든 노드는 이 블록을 블록체인의 끝에 추가하고 새로운 Proposer가 선출되어 다음 블록이 생성되는 과정을 통해 블록 추가해 대한 commitment와 consensus 프로세스를 명시하고 있다.

블록의 구조

slot 블록이 속한 슬롯

proposer_index Proposer의 ID
parent_root prevHash
state_root state root hash
body 블록 데이터를 담고있는 객체 (바로 아래에서 설명)

Body

randao_reveal 다음 block의 Proposer를 선택하기 위한 값(RANDAO seed)

eth1_data deposit contract에 대한 정보
graffiti 블록 태그를 위한 임의의 데이터
proposer_slashings slash당할 validator의 리스트
attester_slashings slash당할 attestor의 리스트
attestations 이 블록에 대한 attestation 리스트 (바로 아래에서 설명)
deposits list of new deposits to the deposit contract
voluntary_exits 네트워크를 떠난 validator 리스트
sync_aggregate light client에게 serve하는 validator subset
execution_payload execution client에서부터 넘어온 정보(트랜잭션 데이터 등) (아래에서 설명)

Attestations

→ list of attestations

aggregation_bits 어느 validator가 이 attesttation에 참여했는지에 대한 목록

data a container with multiple subfields (아래에서 설명)
signature 모든 attester들의 aggregate signature
  • data (in attestation)slot attestation이 실행된 slot
    index validator의 ID
    beacon_block_root 이 object를 포함하는 비콘블록의 루트해시
    source the last justified checkpoint
    target the latest epoch boundary block

Execution Payload header

parent_hash parent block의 hash

fee_recipient 트랜잭션 수수료를 받는 주소
state_root 이 블록으로 인한 state 변화를 적용한 global state의 root hash
receipts_root tx_receipt trie의 root hash
logs_bloom 이벤트로그를 포함하는 데이터 구조
prev_randao random validator selection에 사용된 value (RANDAO seed)
block_number 블록 번호
gas_limit 이 블록에 allow된 최대 gas
gas_used 이 블록에서 사용된 실제 가스
timestamp block time
extra_data arbitrary additional data as raw bytes
base_fee_per_gas base fee value
block_hash Hash of execution block
transactions_root 트랜잭션들의 root hash
withdrawal_root withdrawal 데이터의 root hash

Execution payload

parent_hash parent block의 hash

fee_recipient 트랜잭션 수수료를 받는 주소
state_root 이 블록으로 인한 state 변화를 적용한 global state의 root hash
receipts_root tx_receipt trie의 root hash
logs_bloom 이벤트로그를 포함하는 데이터 구조
prev_randao random validator selection에 사용된 value (RANDAO seed)
block_number 블록 번호
gas_limit 이 블록에 allow된 최대 gas
gas_used 이 블록에서 사용된 실제 가스
timestamp block time
extra_data arbitrary additional data as raw bytes
base_fee_per_gas base fee value
block_hash Hash of execution block
transactions 실행될 트랜잭션들의 리스트
withdrawals withdrawal 객체의 리스트
  • withdrawalsaddress withdraw한 객체의 주소
    amount withdraw한 ETH총량
    index withdrawal index value
    validatorIndex validator index value
  • 스테이킹된 이더를 인출하는것과 관련된 데이터 필드

Blocktime

블록타임(Blocktime)은 블록을 나누는 시간 단위. 이더리움에서는 12초 단위로 시간이 쪼개지며, 이를 slot(슬롯)이라는 시간 단위로 사용한다. 각 slot에서, 랜덤한 프로세스를 거쳐(RANDAO) single validator가 선출되어 블록을 propose한다. 모든 validator가 온라인 상태이고 문제없이 작동한다고 가정했을 때의 blocktime이 12초가 된다. 그러나, 종종 validator가 오프라인상태라면, 해당 검증자의 슬롯은 비워질 수 있다.

Blocksize

블록은 블록 사이즈에 의해 나눠지기도 한다. (Blocktime에서는 한 블록이 몇 초에 한 번 생성되는지를 이야기했다면, 이 말은 한 블록의 사이즈가 얼마나 되는냐는 말이다.) 각 블록의 일반적인 크기는 1500만 gas이며, 네트워크 상태에 따라 증가, 감소가 가능하여 최대 3000만 gas까지 늘어날 수 있다.

Blocksize가 늘어나는 매커니즘

블록의 크기는 한번에 확 증가하는 것이 아닌, 점진적으로 증가/감소하는 방식을 가지고 있다. 그 증가/감소 비율을 최대 $\frac{1}{1024}$의 비율만큼 늘어날 수 있는 것인데, 예를 들어 현재 블록의 크기가 1500만 gas라면, 다음 블록의 최대 크기는 $15,000,000 + (15,000,000/1024==14,648)=15,014,648$ 만큼의 크기를 가질 수 있게 된다는 말이며, 이러한 블록 크기 증가가 여러번 반복되어 최대 30,000,000 gas 크기까지 도달할 수 있다는 말이다.

결과적으로, validator는 합의를 통해 블록의 gas limit을 변경할 수 있으며, 블록의 모든 트랜잭션의 gas 소비량이 블록의 gas limit보다 작을 수 있도록 이를 조절하여야 한다. 블록의 사이즈가 임의대로 커질 수 있다면, 성능이 떨어지는 Full node는 공간 및 속도 요구사항을 충족시키지 못하여 네트워크의 속도를 따라잡을 수 없을 것이다. 블록이 클수록 다음 슬롯에 맞춰 처리하는 데 필요한 컴퓨팅 성능을 더 많이 요구하기 떄문에, 이는 적절히 조절되어야 할 것이다.

반응형

+ Recent posts