[Olympus DAO를 분석해보자 ] — 2. Bonding 컨트랙트 코드 리뷰

Taeheon Lee
BerryFi
Published in
17 min readJan 22, 2022

BerryFi는 크립토 씬에서 화제가 되었던 프로젝트들을 코드 수준까지 분석하는 개발자/기획자들의 모임입니다. BerryFi의 첫 번째 시리즈로 올림푸스 다오에 대한 분석 아티클을 연재합니다. 두번째 글부터는 코드 분석을 통해서 올림푸스 다오 작동 원리에 대해서 상세하게 분석합니다. 코드 리뷰는 올림푸스 다오 깃헙 레포지토리 메인 브랜치에 22년 1월 13일에 업데이트된 f6cd363 커밋을 기준으로 하고 있으며, 이번 글에서는 올림푸스 다오의 본딩 메커니즘에 대해 설명합니다. 다음 글에서는 Treasury 및 Staking 코드를 설명하고, 마지막 글에서는 올림푸스 다오 코드를 전반적으로 살펴보면서 프로토콜 작동 방식에 대해 분석합니다. (간결한 글을 위해서 깃헙 코드 중 불필요한 부분은 지우고 설명할 내용 위주로 정리하여 코드를 업로드하였습니다.)

Author: 이태헌

Audited By: 표정우, 이동헌

이전 글에서 설명했듯이 올림푸스 다오 본딩 파트에서 사용자들은 DAI, FRAX 같은 스테이블 코인이나 OHM-WETH LP, OHM-DAI LP 같은 유동성 토큰을 지불하고 채권을 매입하고, 만기가 되면 위에서 채권 매입 시 지불한 토큰을 기준으로 할인된 가격으로 OHM 토큰을 지급받게 된다. 글 초반부는 본딩 컨트랙트의 사용자 요청 처리 매커니즘을 다루는 파트로, 위의 과정이 어떻게 이루어지는지 설명한다. 먼저 본딩 컨트랙트 스토리지 구조를 요약하고, 컨트랙트 위에서 채권 시장을 생성되는 과정과, 사용자들이 채권을 매입하고 행사하는 과정에 대해 설명할 것이다. 그 이후 글의 후반부는 본딩 컨트랙트의 할인율 결정 매커니즘을 다루는 파트로, ControlVariable 등 기본 파라미터들의 의미를 살펴보면서 채권 할인율 조절 알고리즘에 대해 분석할 예정이다. 이 글의 설명은 BondDepository.solNoteKeeper.sol 파일에 있는 코드들을 위주로 전개되며, 편의를 위해 채권을 매입하고 OHM을 얻으려는 사용자들을 Bonder라 하고, 이 사용자가 채권 매입을 위해 지불하려고 하는 토큰의 종류를 quote token이라고 부르겠다.

사용자 요청 처리 매커니즘

컨트랙트 스토리지

NoteKeeper.sol을 통해 정의되어있는 스토리지는 다음과 같다.

notes는 각 bonder의 주소를 통해 그 bonder가 매입한 채권들의 정보를 담아놓은 Note 배열에 접근할 수 있게 해주는 매핑이다. Note는 구조체로 정의되어 있으며 채권 행사시 bonder에게 돌려줘야하는 gOHM의 양, 생성 일, 시장 번호, 채권 행사여부 등이 기록된다 (코드링크). noteTransfers은 bonder 간 채권 거래가 일어났을때 누가 어떤 채권을 누구에게 전도하였는지를 기록하는 매핑 타입이다. (Bonder 간 채권 거래를 처리하는 로직은 NoteKeeper.sol 안의 pushNote 함수와 pullNote 함수에 정의되어 있는데, 참고로 올림푸스 다오 V2부터는 이 채권들을 NFT 형태로 만들자는 논의가 진행되고 있다: link1, link2.)

NoteKeeper.sol에 bonder들이 보유하고 있는 채권 정보에 대한 스토리지가 정의되어 있었다면 BondDepository.sol에는 채권 시장과 관련된 정보들에 대한 스토리지가 정의되어있다.

markets는 이 컨트랙트 위에 생성된 채권 시장들을 모두 저장하고 있는 Market 구조체의 배열로, 이 구조체는 각 채권 시장이 어떤 토큰을 quote token으로 (e.g. DAI, OHM-WETH LP) 받을지, 그 채권 시장의 보증 한도, 판매량 등을 저장하고 있다. terms는 위의 채권 시장의 할인율을 결정하는 controlVariable, 만기 방식 (fixed term vs fixed expiration) 등을 저장하고 있는 Terms 구조체의 배열이다. 이외에도 metadata, adjustments와 같이 스토리지에 할인율 조정을 위해 필요한 정보와 할인율 조정 내역을 기록하는 스토리지가 정의되어있다.

채권 시장 생성 (create)

채권 시장 생성 로직은 BondDepository.sol 안의 create 함수에 정의되어있고, 생성하려는 시장의 설정 값들은 인풋 파라미터로 받게 된다. 이 파라미터들은 채권 시장의 quote token 종류 (_quoteToken), 시장 한도, 초기 할인율을 결정할 OHM의 quote token 대비 가격 (_market) 등의 값을 담고 있다. create 함수에서는 인자로 들어온 값들을 사용하여 목표로 하는 시장 정보를 가공하고 이를 본딩 컨트랙트 스토리지에 기록함으로써 사용자들이 채권을 매입하고 행사할 수 있는 시장을 생성해주는 역할을 한다.

예를 들어, create 함수 내부 코드에서는 채권 시장이 생성되는 시간을 기준으로 시장 종료까지 남은 시간을 계산하고 (line 2), 채권 시장에서 목표로 하는 OHM 보증양의 초기값 (line 5), 한 번의 quote token 예치 (채권 구매) 기간동안 지불할 OHM 양의 최대값 (line 9), 채권 시장의 서킷 브레이커를 발동시킬 기준 부채양 (line 10), 초기 할인율을 결정할 controlVariable 값 (line 11) 등을 계산하게 된다.

그 다음 앞에서 계산된 값들을 가지고 컨트랙트 스토리지에 기록할 Market, Terms, Metadata 구조체를 만들고 관련 정보를 스토리지에 기록하게 된다. 위의 기록 방식을 보면 같은 인덱스 (id_) 값을 가지고 특정 채권 시장, 그리고 그 채권 시장의 특성에 접근하도록 저장된다. 채권 관련 값들은 채권 매입, 행사하는 과정에서 배열체인 markets[some_index], terms[some_index], metadata[some_index] 형태로 호출되어 활용된다.

채권 매입 (deposit)

Bonder의 채권 매입은 BondDepository.sol에 정의되어 있는 deposit 함수를 통해 이루어진다. deposit 함수는 bonder가 인자로 제시한 _id값을 통해 컨트랙트에 저장되어있는 시장 정보를 호출하면서 시작된다. 글의 후반부에서 설명할 _decay 함수를 통해 시간 흐름에 따라 채권 시장 총 부채량을 조정하고 (line 7), 조정된 부채량을 통해 업데이트된 controlVariable로 할인율을 결정할 price 값을 계산한다(line 8). 그 직후 이 price를 가지고 채권이 만기되었을 때 돌려줄 OHM 양을 계산한다(line 11). 이후 해당 채권 시장에 남아있는 부채 한도를 bonder가 돌려받을 OHM 양 혹은 그에 해당하는 quote token 수 만큼 감소시킴으로서 업데이트한다. 이외에도 채권 (Note)에 기록될 만기일, 이 채권 시장에서 지불된 총 토큰 양 (quote token 기준) 등을 계산하고 bonder가 구매한 채권 정보를 addNote라는 함수를 통해 본딩 컨트랙트 스토리지에 있는 notes 매핑에 기록한다 (코드 링크).

addNote 함수 안에서는 우선 BondDepository.sol 안에서 호출한 addNote 함수를 통해 bonder가 구매한 채권 정보를 컨트랙트 스토리지에 기록한다. deposit 함수에서는 채권 만기일에 돌려줄 OHM/sOHM 양을 addNote 함수에 인자로 전달했다면 이 함수에서는 이 값을 그에 해당하는 gOHM 양으로 변환해서 스토리지에 기록한다는 것이다.

그 다음 FrontEndRewarder.sol 안의 _giveRewards 함수를 통해 프론트엔드 오퍼레이터와 DAO에게 제공할 인센티브를 기록한다 (line 2). 해당 파일을 보면 알 수 있지만, FrontEndRewarder 컨트랙트에 기록된 인센티브 비율만큼의 OHM 보상을 컨트랙트에 기록해뒀다가 나중에 DAO나 프론트엔드 오퍼레이터에게 해당 양의 OHM을 전송해주는 형태로 보상이 주어진다. 그 이후 bonder 및 프론트엔드 오퍼레이터, DAO에게 지급할 만큼의 OHM을 새롭게 발행하고 (line 4), staking 컨트랙트를 통해 채권 만기 시 bonder에게 지불해야할 OHM들을 바로 스테이킹한다 (line 6). 여기서 사용자가 지급받을 OHM을 바로 스테이킹하도록 하는 것은 올림푸스 다오 v2에서 새롭게 도입된 매커니즘이다. (다음 글을 통해 treasury와 staking 컨트랙트에서 mint와 stake 로직에 대해 자세하게 다룰 것이다.)

다시 BondDepository.sol의 deposit 함수에서 사용자의 quote token이 채권 매입에 대한 비용으로 금고 보유자산으로 전송이 된다 (line 2). 여기까지가 deposit 함수가 처리하는 로직에 대한 설명이다. 이 과정이 모두 끝난 이후에는 글 후반부에 설명할 _tune 함수를 통해 controlVariable 등을 재조정하게 된다.

실제로 Etherscan을 통해 최근에 deposit 함수로 채권 매입을 요청한 트랜잭션을 보면, treasury를 통한 OHM 발행 (line 1), BondDepository가 이 OHM들을 바로 스테이킹 (line 2), 스테이킹에 대한 gOHM 발급 (line 3) 채권 구매에 사용한 DAI 토큰 (quote token) 만큼이 Treasury 컨트랙트에 금고 보유 자산으로 입금 (line 4) 되는 것을 확인할 수 있다.

채권 행사 (redeem)

Bonder가 채권을 행사하여 약속된 OHM을 지불받는 것은 NoteKeeper.sol 안의 redeem 함수에 정의되어 있다. 이 함수는 bonder가 행사하려는 채권들의 index들을 담은 배열을 인풋으로 받아서 처리하는데, 함수 내에서는 먼저 각 채권마다 채권을 행사할 수 있는 시기가 되었는지 그리고 bonder가 이미 그 채권을 행사한 것은 아닌지 등의 조건을 확인한다 (line 3: 코드 링크). 만약 행사하여 OHM을 수령할 수 있는 채권이면 현재 타임스탬프를 notes 매핑에 기록함으로써 해당 채권이 행사된 채권이라고 기록한다 (line 6). 그리고 bonder에게 지급할 OHM의 양을 누적해서 더한 다음에 (line 7), 지급할 총 양을 bonder의 요청에 따라 gOHM 혹은 sOHM의 형태로 지급한다 (line 12, 14).

할인율 결정 매커니즘

기본 파라미터 설명

글의 전반부에서는 사용자들의 요청을 본딩 컨트랙트가 어떻게 처리하는지에 대해서 주로 설명하였다. 여기서부터는 채권 할인율을 결정하는 여러 값들이 코드에서는 어떻게 정의되어 있는지, 그리고 그 값을 어떻게 이해하면 될 지에 대해서 설명하겠다. 그 이후 채권 시장이 작동하는 동안 이 값들이 어떻게 재조정되는지 살펴보겠다. 편의를 위해 시장에서의 quote token의 가격과 OHM의 가격 비율이 불변한다고 가정하겠다.

채권 매입 과정에서 bonder가 채권을 통해 지불받을 OHM 양은 밑의 식으로 계산되었다.

여기서 amount는 bonder가 지불한 quote token의 개수이고, price는 OHM 한 개의 가치를 quote token 기준으로 책정한 가격이다. payout 값이 커진다는 것은 같은 양의 quote token을 지불하고 더 많은 OHM을 받는다는 것을 의미하고 이는 해당 채권의 할인율이 높아졌다는 것과 같은 말이다. 그럼 위 식에서는 두 가지 포인트를 기억해두면 좋다.

  1. price가 높아지면 payout이 작아지고 이는 채권의 할인율이 낮아졌다는 것을 의미한다.
  2. price가 낮아지면 payout이 커지고 이는 채권의 할인율이 높아졌다는 것을 의미한다.

payout 식의 price는 다음과 같이 계산된다.

앞의 create 함수에서 살펴봤듯이 시장이 만들어진 직후의 초기 상태에서의 initial controlVariable 값은 price 값이 목표로 하는 initial price와 같은 값이 되도록 설정되어 있다. 이 값은 다음의 식으로 나타낼 수 있다.

채권이 팔리기 시작한 이후에는 채권 판매에 따라 total_debt 값이 증가하고, 그에 따라 OHM이 새롭게 발행된 것이 OHM supply 값에도 영향을 주기 때문에 계속해서 price와 할인율이 변화하게 된다. 시장이 만들어지고 나서 첫 채권 매입이 발생된 다음 매입에서의 next price 값은 다음과 같이 계산될 것이다.

이 식에서 등호 이후의 식의 첫번째, 두번째 값의 곱은 채권 시장 생성 시에 설정되었던 값들이고 세번째 값은 시장에서의 첫번째 채권 매입이 발생하고 난 이후의 증가된 debt 값과 증가된 OHM 유통량의 비를 나타낸다. 위 식을 정리해서 다시 써보면 다음과 같아진다.

위의 식은 두번째 분수 값과 세번째 분수 값을 debt값과 OHM supply 값을 기준으로 이해하면 쉽게 이해할 수 있다.

  1. debt 영향: 세번째 분수 값에서 채권이 판매됨에 따라 초기 상태의 total debt 값에 비해서 현재의 total debt 값은 증가하였을 것이다. 그리고 이 증가한 비율만큼 초기 price에서 현재 price가 증가하도록한다고 해석할 수 있다. 이는 채권 구매 수요가 높을 수록 할인율이 떨어지는 것과 같은 상황이다.
  2. OHM supply 영향: 두번째 분수 값에서는 채권 판매에 따라 NoteKeeper.sol의 addNote에서 OHM 유통량이 증가하게 되는데 시장 생성 당시의 OHM supply 대비 현재의 유통량 만큼이 debt 증가에 의해 비싸진 price를 어느정도는 상쇄해줄 것이다.

하지만 debt는 한 시장에서 발생될 OHM의 양이고, OHM supply는 전체 OHM 공급량이라는 것을 고려했을 때 debt 값의 크기는 OHM supply 값보다 훨씬 작을 것이다. 그렇기 때문에 debt 값의 변화 비율에 비해 OHM supply의 변화 비율은 더 작을 것이며, OHM supply 증가에 의해서 상쇄되는 영향은 그리 크지 않을 것이다. 따라서 controlVariable 재조정 없이는 price가 계속해서 상승하고 할인율은 낮아질 수 밖에 없다.

위 식에서는 설명의 편의성을 위해 시장 초기화 이후의 price 변화에 대해 설명하였지만, 시장이 활성화된 이후에도 bonder들의 채권 매수에 따른 직전 price 대비 그 다음 price 변화는 controlVariable 값의 재조정이 있지 않는 이상 같은 방향으로 작용한다.

채권 할인율 조정 1 (_tune 함수)

채권 시장이 작동하는 도중에 controlVariableprice가 재조정되는 매커니즘은 BondDepository.sol 안의 _tune 함수 내에 정의되어있다. 전체적인 함수의 구조는 다음의 링크에서 확인할 수 있는데, 코드를 읽은 사람은 눈치챘겠지만 전반부의 채권 시장 생성 파트의 create 함수와 구성이 거의 비슷하다. 코드 분석을 통해 _tune 함수가 하는 일을 이해하면 왜 그런지 명확하게 느낄 수 있을 것이다.

할인율 관련 파라미터 재조정은 우선 tuneInterval이 지났을 때만 실행되도록 설정되어 있다 (line 2). 그 다음 채권 시장 종료까지 남은 시간 (line 5)과 채권 매입 직후에 계산되는 price를 계산한다 (line 6). 그 다음 create 함수에서와 같은 논리로 maxPayout 값을 재설정한다 (line 9).

그림 1. _tune 함수 내 targetDebt 값의 재설정

targetDebt 또한 다시 설정되고 (line 10) 그 다음에 controlVariablenewControlVariable로 다시 계산되는데 이 부분이 중요하다 (line 11). targetDebt 값은 capacity 값이 지금부터 시장 종료 시점까지 선형적으로 줄어든다고 가정하였을때, 시장 생성 시점에서는 capacity 혹은 targetDebt 값이 얼마였을지 재계산한 것이다 (그림1). 만약 채권 매입이 활발하게 이루어져서 현재의 capacity 값이 초기 설정 상태서의 capacity 값이 선형적으로 줄어들었을 때보다 작다면 targetDebt 값도 그만큼 작아지고 newControlVariable 값은 반대로 증가할 것이다. 이는 price를 상승시켜 채권 할인율은 떨어지게해서 채권의 판매 속도를 늦추는 역할을 하게 된다. (capacity 값이 비교적 큰 경우에도 반대의 논리로 재조정 매커니즘을 설명할 수 있다.)

그 이후 재조정된 newControlVariable 값이 기존 controlVariable 값보다 클 때에는 이 값을 바로 업데이트하고 (line 15) 그렇지 않을 경우에는 adjustments 스토리지에 변화해야하는 값을 기록해서 이 값이 서서히 반영되도록 한다 (line 18).

_tune 함수는 현재의 capacity 값을 가지고 그 시점에서 시장을 초기화 시키는 작업을 한다고 생각할 수 있다. 그래서 create 함수와 많은 부분에서 유사한 구조를 가지고 있는 것이다. _tune 함수는 이를 통해 채권이 시장 종료 시점까지 꾸준히 일정 속도로 팔려나가도록 bonder들의 매입되는 속도를 조절해주는 역할을 한다.

채권 할인율 조정 2 (_decay 함수)

올림푸스 다오에서는 _tune 함수 이외에도 BondDepository.sol 안의 _decay 함수를 통해 할인율을 재조정한다. _decay 함수는 다음과 같다.

우선 totalDebt 의 값을 시간 흐름에 따라 감소시킨다 (line 2). 시장 생성부터 totalDebt가 선형적으로 감소하여 시장 종료 시점에서는 자연적으로 0이 되도록 totalDebt 값이 줄어든다. 그 다음 위의 _tune 함수를 통해 설정된 반영해야할 controlVariable 조정 내역이 있으면 이를 시간이 흐른 것에 비례해서 서서히 반영한다 (line 9).

만약 채권 시장에서 채권 매입이 하나도 일어나지 않았다고 해보자. 그럼 capacity 값은 시장 생성 시점 그대로인데 totalDebt의 값만 시간 흐름에 따라 자연 감소하였을 것이다. 이는 price 값을 감소시키고 할인율을 더 높여서 bonder들이 채권을 구매하도록 하는 요인을 강화시키게 된다. 만약 이 상황에서 _tune 함수까지 시행되었다면 targetDebt가 커지고 newControlVariable은 더 작아져버려서 할인율 상승 폭을 더 크게 만들것이다.

마치며

이번 글에서는 코드 분석을 통해 올림푸스 다오에서 제시하는 프로토콜이 블록체인 위에서 어떻게 실행되는지 파악하였다. 다음 글에서 Treasury와 Staking 컨트랙트를 분석하고 나면 올림푸스 다오가 어떤 구조로 이루어져 있는지 좀 더 명확하게 파악할 수 있을 것이라 기대한다.

글 후반부에서 채권의 할인율 관련하여 controlVariable 설정에 대한 rational과 업데이트 방식에 대해서 살펴보았다. 이 글에서는 OHM의 가격이 고정되어있는 상황을 가정하고 사용자의 채권 구매 상황에 대한 할인율 변화에 대해 설명하였다. 하지만 실제로는 OHM 가격의 변화폭은 매우 크며 이에 따라 할인율이 -35%를 찍는 상황도 발생했었다. 그렇기 때문에 OHM과 quote token 가격 변동에 따른 할인율 변화 및 조정 방식에 대해서도 이해하여야 한다. 올림푸스 다오 코드 설명이 끝난 이후에는 이 내용에 대해 좀 더 자세하게 설명함과 동시에 수치적인 시뮬레이션을 통해, 어떤 함수를 써서 controlVariable과 debt decay를 조절하는 것이 채권의 할인율을 조정하는데 있어 좀 더 효율적인지 분석할 것이다.

--

--