[OlympusDAO를 분석해보자] — 3. Treasury 및 Staking 컨트랙트 코드 리뷰

Jungwoo Pyo
BerryFi
Published in
13 min readFeb 2, 2022

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

Author: 표정우

Audited By: 이태헌, 이동헌

[OlympusDAO 시리즈]

  1. Introduction
  2. Bonding 컨트랙트 코드 리뷰
  3. Treasury 및 Staking 컨트랙트 코드 리뷰
  4. 주목할 만한 기능들

Treasury 동작 메커니즘

Treasury의 역할

Treasury는 OlympusDAO에서 OHM의 가치를 backing하기 위한 여러 reserve token과 LP들이 보관되어 있는 컨트랙트이다. Bond의 구매를 통해 reserve token LP를 지불하면 지불된 token들은 해당 컨트랙트에 저장되며, 이렇게 모인 token의 가치는 발행되는 OHM의 가격을 지지하는(backing) reserved asset의 역할을 한다.

reserve token이 예치될 때마다 그 가치들은 totalReserves 라는 상태변수에 합산된다. 이를 현재 유통 중인 OHM의 수로 나누게 되면 OHM의 이론적인 backing price(가격하한선)을 구할 수 있다. 또한 excessReserves()를 통하여 OHM의 공급대비 얼마나 많은 reserved asset이 쌓였는지를 파악할 수 있어서 treasury에 쌓인 reserved asset에 비해 유통되고 있는 OHM의 발행량이 많을 때, 이에 비해 과도한 OHM의 발행을 요청하는 경우 excessReserves() 에 의해 OHM의 추가 발행을 막음으로써 OHM의 가격 하락을 막는다.

Treasury 컨트랙트에서 사용되는 자료구조 및 상태 변수

Treasury.sol에서 정의된 자료구조는 다음과 같다.

enum STATUS는 reserve의 예치자, 소비자, reserve token 등의 역할을 정의해 놓은 type이다. struct Queue는 매니징할 토큰에 대한 정보를 저장하는 자료구조로 treasury에 존재하는 각종 토큰에 접근할 수 있는 권한을 관리하는 부분에서 사용하게 된다.

Treasury에서 사용되는 주요 상태 변수는 다음과 같다.

먼저 permissionQueueQueue의 array로, permissions 변수에 주소의 권한을 얻기 위해 queueTimelock() 함수에서 채워지는 상태 변수이다. 이후, execute()에 의해 permissionQueue에 채워진 Queue가 실행되며 permissions 등의 권한을 저장하는 상태변수에 특정 주소의 권한이 주어진다.

registry는 reserve token과 liquidity token을 보유하고 있는 account list이며, 추후에 auditReserves()를 통해 deposit이나 withdraw 등의 함수에 의해 계산된 totalReserves의 값이 맞는지 확인하는 용도로 사용된다.

permissionsSTATUS의 특정 타입에 대해 특정 주소의 접근권한 여부가 저장되어 있는 상태 변수이다. Treasury.sol에서 정의된 대부분의 함수들이 함수 실행 전 해당 변수를 확인하여 sender의 권한을 확인한다.

bondCalculator는 liquidity token의 contract 주소와 이를 계산하는 calculator의 주소의 매핑을 저장하는 변수이다. calculator의 경우 나중에 자세히 설명하겠지만, Uniswap과 같은 DEX에서 해당 토큰의 시장 가격을 불러오거나 LP token의 수를 파악할 때 사용하게 된다.

Treasury 컨트랙트의 함수 기능 설명

기본적으로 treasury contract의 함수는 permissions[STATUS]에 주소가 list 되어있는지에 따라 실행할 수 있는 함수가 다르니 이를 미리 파악하는 것이 좋다.

1) deposit

deposit 함수는 bond에서도 똑같이 정의되어 있는데, bond의 deposit은 실제 사용자가 채권을 구매할 때 사용하는 데 반해 treasury의 deposit은 reserve allocator와 상호작용하게 된다.(f6cd363 커밋 기준으로 allocator 관련 코드가 구현되어 있지 않지만, 다이어그램에 allocator의 역할이 명시되어 있어서 본 글에서는 allocator에 대한 언급만 하고 contract 관련 설명은 생략한다.) reserve allocator는 treasury fund를 Aave, Curve와 같은 외부의 디파이 전략을 사용하기 위해 배포된 컨트랙트를 말한다. treasury의 deposit은 treasury로부터 fund를 인출한 후, 이를 다시 돌려놓을 때 사용하는 컨트랙트이다. fund를 인출할 때는 후에 설명할 manage() 를 이용한다.

  • _amount만큼의 _token을 treasury에 예치한다.
  • 예치한 토큰의 총 가치는 value 이며, value-_profit 에 해당하는 OHM을 sender에게 발행한다. 처음 Treasury에 token을 채워넣을 때에는 OHM이 발행되지 않게 하기 위하여 _profittokenValue와 동일한 값을 전달한다. 그 이후에는 0을 전달하여 value에 해당하는 OHM을 발행하게 된다.
  • reserve depositor 혹은 liquidity depositor permission 소유자만이 실행할 수 있다.

토큰의 market price를 알기 위해서는 해당 토큰이 상장된 거래소에서 가격 정보를 불러오거나, 토큰이 거래되고 있는 DEX(ex. Uniswap, sushiswap, Pancakeswap 등)에서 해당 토큰-stable coin의 페어 풀로부터 알아내는 두 가지 방법이 있다. 그러나 첫 번째 방법 같은 경우는 외부의 잘못된 정보를 블록체인 내부로 가져오면서 발생하는 블록체인 오라클 문제가 발생할 수 있고, 실제로 상장이 안 되어 있을 경우 market price를 매길 방법이 없다. 따라서 OlympusDAO에서는 후자의 방법을 사용하며, tokenValue()에 의해 market price를 불러오게 된다.

tokenValue()가 market price를 불러오는 방법에 대해 자세히 설명하자면, reserve token의 market price는 UniswapV2로부터 LP 풀의 정보를 불러와 LP 풀에 예치된 토큰의 비율을 파악하는 방식으로 이루어진다.

Token-DAI LP pair를 이루고 있는 Token의 수를 x, DAI의 수를 y라고 한다면, 보유하고 있는 토큰의 양(amount)의 가치(value)는 다음과 같이 산정된다.

따라서, Bond를 수행할 때 위 메커니즘에 의해 LP pair를 구성하고 있는 reserve token의 가치를 파악할 수 있고 reserve asset에 즉각 반영된다.

2) manage & withdraw

두 함수는 treasury에 예치되어 있는 token을 인출하는 함수이다.

withdraw

  • reserve spender permission 소유자가 실행 가능하다.
  • 보유하고 있는 OHM을 소각한 후, 동일한 가치에 해당하는 token을 _amount만큼 빼 온다.

manage

  • liquidity manager나 reserve manage에 대한 permission이 있어야 실행 가능하다.
  • OHM을 보유할 필요 없이, 원하는 만큼의 token을 인출한다. 단, 그 value가 treasury의 reserve를 초과하는 경우에는 revert된다.

3) mint

  • recipient에게 _amount 만큼의 OHM을 발행해주는 함수로, reward manager permission 소유자만이 이를 실행할 수 있다
  • excessReserves()를 통해 OHM의 유통량이 과도하거나 treasury에 저장된 reserve에 비해 많은 OHM의 발행을 요구하는 경우 mint를 revert함으로써 채권을 판매할 때나 staking의 이자를 배분할 때 OHM이 너무 많이 풀리지 않게 OHM의 공급을 조절한다.
  • 해당 함수는 bonding contract에서 사용자들이 reserve token이 포함된 LP를 지불하여 할인된 OHM을 구매하는 채권 구매 프로세스에서 호출된다.
  • 이후에 Staking 쪽에서 distributor들이 sOHM에 대한 이자를 분배할 때에도 활용하는 함수이다

Staking의 동작 메커니즘

Staking의 역할

스테이킹은 OHM의 가격을 지지하게 해주는 중요한 기능이다. Bond를 통해 OHM을 market price보다 작은 가격으로(높은 할인율로) 구매하게 된다면, OHM의 가격은 지속적으로 떨어질 것이고 이를 예방하기 위한 방법은 OHM staking을 통해 고이율의 이자를 지급함으로써 staker들에게 OHM을 보유할 유인을 제공하는 것이다.

OHM과 sOHM, gOHM

Staking contract 코드를 설명하기 전에 OHM과 sOHM, gOHM의 차이를 알아야 한다. 사용자가 OHM을 소유하고 있으면, 다른 주소에 전송하는 등 일반 토큰과 같이 사용할 수 있다. 하지만 이자를 수령하기 위해 가지고 있는 OHM을 Staking contract에 맡기게 되면 sOHM의 형태로 고정되어 이를 자유롭게 사용하지 못한다. 물론 Unstake를 수행하면 락업기간 없이 다시 OHM을 돌려받을 수는 있지만, 이자를 수령하지 못한다.

gOHM은 일종의 wrapped sOHM으로, OlympusDAO V2에서 소개되는 개념이다. gOHM은 OHM의 스테이킹 이자를 받으면서 자유롭게 거래가 가능하다. 다만 리베이스 시간(이자를 지급하는 term)마다 개수가 증가하는 것은 아니고, gOHM의 가격이 증가하는 형태로 이자를 지급하게 된다. gOHM의 가격은 다음과 같은 식으로 표현된다.

$gOHM_{price} = OHM_{price} * CurrentIndex$

$CurrentIndex$는 epoch이 지날 때 마다 incremental하게 증가하는 값으로, 시간이 지날수록 OHM의 market price가 감소하지 않는다는 전제 하에 지속적으로 증가하는 구조를 가지고 있다. 참고로 OlympusDAO의 공식 문서에서는 gOHM을 “sOHM이 들어있는 가방” 이라는 표현을 들어가며 설명하니 참고해도 좋다.

출처: https://docs.olympusdao.finance/main/contracts/tokens#gohm

아래의 Staking contract을 설명하는 부분에서는 sOHM과 gOHM을 다루는 함수가 있을 때 _rebasing 이라는 argument를 전달하는데, 스테이킹 이용자가 sOHM 혹은 gOHM을 받기를 원하는지에 따라 _rebasing 값이 True 혹은 False로 달라지게 된다.

Staking 컨트랙트에서 사용되는 자료구조 및 상태 변수

OlympusDAO에서는 epoch 단위로 스테이킹에 대한 이자를 지급하게 된다. struct Epoch은 말 그대로 스테이킹 epoch에 대한 정보를 담을 수 있는 구조이다. Epoch 안의 distribute는 해당 epoch에서 staker들에게 나눠줄 이자의 양을 의미한다. 현재 Epoch의 정보는 epoch 상태 변수 안에 담긴다.

struct Claim 은 사용자가 OHM을 staking 할 때 각 사용자들의 staking 정보를 담는 구조체이다. Staking contract에서는 해당 구조를 address를 key로 하는 매핑 변수인 warmupInfo에 담는다.

Staking 컨트랙트의 함수 및 기능 설명

1) Stake

  • sender가 스테이킹하려는 OHM을 Staking contract에 전송한다.
  • _claim이 False이면 warmupPeriod에 관계없이 스테이킹한 OHM의 담보에 해당하는 sOHM이나 gOHM을 받게 된다.
  • _claim이 True일 경우 lock-up이 있어서 expiry가 지난 경우에야 sOHM이나 gOHM을 받게 된다.

2) Claim, Forfeit & Unstake

Claim

  • OHM을 스테이킹함으로써 sOHM 혹은 gOHM에 대한 이자가 쌓이는데, 스테이킹한 원금과 이자를 수령하는 함수이다.
  • warmupInfo[_to].gons에 원금+이자가 기록되어 있는데, 해당 sOHM(혹은 gOHM)을 _to 주소에게 전송한 후 warmupInfo[_to]를 삭제한다.

Forfeit

  • 스테이킹을 종료하고 맡긴 OHM을 수령하는 함수이다.
  • claim() 과의 차이점은 claim()은 sOHM 혹은 gOHM의 형태로 수령하고, forfeit()은 OHM의 형태로 수령한다.

Unstake

  • 보유한 sOHM 혹은 gOHM을 반환 혹은 소각하고 이에 상응하는 OHM을 수령하는 함수이다.

3) Rebase

Rebase는 현재 epoch이 종료되었을 때(block.timestampepoch.end를 초과하였을 때) staker들에게 제공할 sOHM의 이자율을 포함한 여러 값들을 갱신하는 함수이다.

  • epoch.distribute는 이자율에 따라 늘어난 sOHM의 양으로, 현재 epoch에서 이자로써 staker들에게 제공될 sOHM을 뜻한다.
  • epoch.distribute의 값은 sOHM.rebase()profit_ 인자에 해당하는 값으로 전달된다.
  • sOHM.rebase()에서는 rebaseAmount를 구하고 이를 totalSupply에 합하게 되는데, 그 값은 인자로 전달받은 epoch.distributetotalSupply/circulatingSupply를 곱한 값이다.
  • _gonsPerFragment는 전체 공급량 기준으로 sOHM 1개의 비율에 해당하며, 결국 전달받은 epoch.distribute의 값에 비례해서 sOHM이 발행량이 증가하게 되고 그만큼 개개인의 이자수익이 증가하게 된다.
  • 다음 epoch의 이자율에 해당하는 양만큼 treasury가 staking contract에 mint를 수행하고, bounty가 있을 경우 역시 추가로 treasury가 mint를 수행한다.
  • 다음 epoch의 distribute는 (staking된 OHM의 수 — sOHM의 유통공급량 — bounty) 에 해당하는 양만큼 발행을 하게 되며, 이 값이 음수일 경우 distribute의 값은 0이 된다.

4) 기타 함수

_send

  • sOHM 혹은 gOHM을 staker에게 보내는 internal function이다.
  • _rebasing 값이 True일 경우 sOHM, False일 경우 gOHM의 양을 return한다.

wrap & unwrap

  • wrap은 sOHM을 gOHM으로 변환하는 함수이며, unwrap은 그 반대이다.

마치며

이번 글에서는 올림푸스다오의 Treasury와 Staking의 메커니즘을 코드 레벨로 파악해 보았다. 이로써 올림푸스다오의 주요 기능을 어느 정도 파악하였으니, 마지막 글에서는 올림포스다오의 메커니즘에 대한 전반적인 요약 및 Q&A를 다루면서 올림푸스다오 시리즈를 마무리하도록 하겠다.

--

--

Jungwoo Pyo
BerryFi

Crypto Researcher & Software Engineer | Common Computer | Decipher