ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Defi] Uniswap V2 Architecture 분석
    블록체인 2021. 2. 24. 04:40

     유니스왑 코드를 뜯어보다 보니, 다른 소프트웨어도 다 그렇겠지만 아키텍처를 한번 살펴보는게 더 도움이 되겠다 싶어서 잠시 코드 분석은 접어두고 아키텍처 분석을 해보기로 했다.

     일단 docs도 보고 하겠지만 우선 Github에 어떤 Repository들이 있는지 간단히 살펴봐야겠다. (github.com/Uniswap)
    스마트 컨트랙트와 관련된 레포지토리는 크게 다음과 같다.

     

    Uniswap/uniswap-lib

    📖 Solidity libraries that are shared across Uniswap contracts - Uniswap/uniswap-lib

    github.com

     

    Uniswap/uniswap-v2-core

    🎛 Core smart contracts of Uniswap V2. Contribute to Uniswap/uniswap-v2-core development by creating an account on GitHub.

    github.com

     

    Uniswap/uniswap-v2-periphery

    🎚 Peripheral smart contracts for interacting with Uniswap V2 - Uniswap/uniswap-v2-periphery

    github.com

     

    Uniswap/governance

    🏛 Governance contracts for the Uniswap protocol. Contribute to Uniswap/governance development by creating an account on GitHub.

    github.com

     

    Uniswap/advanced-weth

    A smart contract that wraps WETH that adds functionality for transparently dealing in WETH - Uniswap/advanced-weth

    github.com

     

    이 정도가 있는 것 같다. 몇개는 Archieve 레포지토리 같기도 한데, 자세한 것은 고민해보기로 하고, 각 레포지토리가 어떤 역할을 하는지 대충 알아보자. 주로 uniswap.org/docs/v2/protocol-overview/smart-contracts/ 를 많이 참고했다. (governance와 advanced-weth는 여기서는 살펴보지 않았다.)

     

    Uniswap

    Automated liquidity protocol on Ethereum

    uniswap.org

     

    1. Uniswap-lib

    이름만 봐도 어떤 역할인지 알 것 같고, Abstraction에도 유니스왑 컨트랙트에서 사용하는 다양한 공유 라이브러리다. 특히 safety와 gas efficiency, 즉 보안과 가스 절약에 집중한다고 하니, 라이브러리 레포지토리 코드 분석할 때는 어떻게 이것들을 구현했는지 집중해서 들여다 봐야겠다.

     

    2. Uniswap-core

     마찬가지로 유니스왑의 코어 부분을 구현한 레포지토리이다. README에는 큰 내용은 없지만 docs에 이런저런 내용이 있어서 간단하게 요약해봐야겠다. 이 전 포스팅에서 core쪽 컨트랙트들을 분석했지만, 크게 봐서 Core는 factorypairs 두 싱글턴 컨트랙트로 구성된다. 

     Factory는 한 토큰 페어 당 유일한 하나의 컨트랙트를 만드는 역할을 한다. Factory쪽 문서를 보면 컨트랙트의 함수를 Read-only FunctionsState-Changing Functions로 나누는데, 이더리움의 스테이트를 변경하는 것은 굉장히 중요한 일이므로 이렇게 나누는게 딱 보기에도 좋아보이긴 하다.
     어쨋든 Read-only 함수들을 보면 토큰 pair를 가져오는 함수들이 대부분이고, 주소를 return하는걸 보니 각 토큰 pair도 유일한 컨트랙트로 구성된다는 것을 짐작할 수 있다.
     State-changed 함수는 새로운 토큰 페어를 생성하는 함수가 있다. 이 때 이더리움 네트워크에 토큰 주소라던지 새로 만든 컨트랙트 주소를 기록해야 하므로 State-Changed 함수로 분류한다. (물론 새로운 컨트랙트를 만드는 작업도 포함)

     Pairs는 AMM(Automated Market Maker)기능을 제공하고, 토큰 pool의 잔고를 추적한다. 그리고 오라클 가격을 형성할 수 있는 데이터들도 제공한다고 한다. 어제 pairs 분석할 때 이쪽 코드가 있었나..하는 기억이 있는데 다시 봐야지..

     

    3. Uniswap-periphery

     Periphery는 "주변부" 라는 뜻인데, 말 그대로 유니스왑의 코어 기능이 아닌 주변부 기능(?)을 구현한 레포지토리이다. Core 코드 분석하면서 Periphery쪽 코드도 같이 봐야 좀 이해가 잘 되는거 같아 살짝 봤었는데 대부분 Core 함수를 호출하는 역할을 많이 담당하는 것 같았다.

     문서의 설명을 보면, core 컨트랙트와 domain별 상호 작용을 지원하기 위한 컨트랙트들의 모임이라고 정의하고 있다. 뭔가 어렵지만 핵심은 core 컨트랙트들과 상호작용, 즉 새로운 pool을 만들고, pool을 가져오고, 토큰을 발행하고 소각하는 기본 동작들을 호출하는 기능을 담당한다. 

     Periphery도 내부적으로는 LibraryRouter로 구성된다. 
    - Library는 다 Periphery에서 편리하게 데이터를 가져오고(fetch), 가격을 구성하기 위한 기능들을 제공한다.
    - Router는 유니스왑 front-end에서 유니스왑의 모든 기본적인 기능을 사용하기 위해 사용되는 컨트랙트이다. 기본적인 기능이라 하면, 트레이딩 기능과 유동성(liquidity) 관리 기능을 말한다. 특히 Router는 멀티 페어 트레이딩, erc20 - erc20간의 트레이딩 기능을 제공한다. 또한 유동성 제거를 위한 기능도 제공한다.

     또한 Router의 경우, 내가 컨트랙트로 유니스왑과 상호작용하는 로직을 작성하고 싶을 때, 이 Router를 통하도록 작성한다. 예제는 soliditydeveloper.com/uniswap2서 확인할 수 있다.

     

    Design Decisions

     핵심은 이 부분이다. 유니스왑 개발팀이 컨트랙트를 구성할 때, 어떤 의도를 갖고 컨트랙트를 구성했는지 살펴보는게 핵심이다. 이 부분과 더불어서 V1과 무엇이 달라졌는지도 궁금한데, 하나씩 살펴봐야지. 

    - 토큰 전송

     일반적으로 토큰이 필요한 컨트랙트는, 토큰 컨트랙트에 approval을 먼저 하고(즉, 내 토큰의 전송 권한을 주는 것), 그리고 transferFrom 함수를 호출해야 한다고 한다. 그리고 이 과정에서 interactor가 필요하고. 이 approval과 transferFrom은 모두 erc20에 정의된 그 함수를 말하는 것 같다. 그런데 유니스왑 V2에서는 토큰을 전송받기 위해 이렇게 하지 않는다고 한다.
     여기서는 각 pair 컨트랙트가 모든 상호작용의 마지막 단계에서 그들의 토큰 밸런스를 체크한다고 한다. 그리고 다음 상호작용의 시작 부분에서, interactor에 의해 전송된 토큰의 양을 비교하기 위해 현재 잔고와 저장된 값을 비교한다고 한다. 간단하게 말하면, 토큰이 필요한 함수가 실행되기 전에 토큰을 보내야 한다는 것이다. 이것만 봤을때는 무슨 말인지 잘 이해가 안돼서 백서랑 같이 보면서 좀 더 이해해보기로 했다.

    When evaluating the security of this core contract, the most important question is whether it protects liquidity providers from having their assets stolen or locked. Any feature that is meant to support or protect traders—other than the basic functionality of allowing one asset in the pool to be swapped for another—can be handled in a “router” contract.

    - Uniswap white paper, 3.2 Contract re-architecture

     백서를 보니, 보안을 위해 유동성 공급자(liquidity provider)들이 공급한 자산이 잠기거나(locked), 도둑맞지 않게(stolen) 하는 것이 가장 중요한 기능으로 본 것 같다. 이 기능을 Router를 통해 제공하는데, 위에서 얘기한 "자산을 먼저 전송하고 함수를 실행하는 것"이 어떤 의미인지 생각해봐야겠다.

     In fact, even part of the swap functionality can be pulled out into the router contract. As mentioned above, Uniswap v2 stores the last recorded balance of each asset (in order to prevent a particular manipulative exploit of the oracle mechanism). The new architecture takes advantage of this to further simplify the Uniswap v1 contract.
     In Uniswap v2, the seller sends the asset to the core contract before calling the swap function. Then, the contract measures how much of the asset it has received, by comparing the last recorded balance to its current balance. This means the core contract is agnostic to the way in which the trader transfers the asset. Instead of transferFrom, it could be a meta transaction, or any other future mechanism for authorizing the transfer of ERC-20s.

    - Uniswap white paper, 3.2 Contract re-architecture

     이 문단을 보면 유니스왑 V2는 각 자산(asset)의 마지막 잔고를 저장(stores the last recorded balance)한다고 한다. 이유는 오라클 가격 조작을 막기 위해서라는데 잘 이해는 안된다ㅠ_ㅠ. agnostic이라는 단어가 나오는데 사전상으로는 불가지론..? 그런데 컴퓨터 용어로써는 특정 플랫폼에 종속적이지 않다는 뜻이라고 한다. 그러니까 유니스왑에서 swap을 할 떄, 컨트랙트로 자산을 입금하고 다른 자산을 받는 일이 atomic 한 것이 아니라, 1. 입금하고, 2. 변화된 자산의 양을 비교하여 교환할 양을 계산하는 두 개의 작업으로 나누어서 실행한다. 이를 통해서 어떤 자산을 입금하던 간에 하나의 함수로 swap을 할 수 있도록 구현했다는 뜻인것 같다.

     이게 잘 감이 안와서 유니스왑 V1과 비교해보기로 했다. V1 같은 경우는 pool이 직접 토큰을 보관했다는 것 같다. 코드를 보고 비교해보면, V1의 경우는 ERC20 표준을 그대로 컨트랙트로 사용하고 있고, V2의 경우는 permit, _mint, _burn등의 여러 유틸리티 함수들이 붙어있다. 일종의 wrapper같은 느낌? 좀 더 찾아보니 ERC777 토큰, 또는 non-standard ERC-20을 지원할 수 있다고한다.

     또한 백서에는 core 모듈의 로직을 최소화 함으로써, 컨트랙트를 더 upgradable하게 작성했다고 설명하고 있다. 실제로 V1 코드를 보면 컨트랙트가 두개밖에 없는데, uniswap_exchange와 uniswap_factory가 그것이다. factory는 새로운 exchange를 만드는 역할을 하고, exchange에 유동성 공급 관련 로직과 swap 로직이 모두 작성돼있다. 또한 exchange가 erc20까지 구현하고 있다. 반면에 V2에서는 swap, mint, burn관련 로직을 UniswapV2Pair 컨트랙트에 구현하고, erc20은 UniswapV2ERC20에서 따로 구현한다. 또한 유동성 공급 / 해제 관련 로직은 Router쪽에서 구현하여 모듈을 분리했다.

    Uniswap V1을 간단히 그림으로 표현해봤는데, 이렇게 하나의 컨트랙트에서 교환, erc20, 유동성 관련 함수 콜이 모두 일어난다.

     

    V2의 구조를 간단하게 그림으로 표현해봤는데, 보통 Router와 의사소통하고 유동성 및 swap 작업은 Core에 있는 여러 컨트랙트와 소통한다.

     

     일반적으로 OOP나 소프트웨어 구조에 대해 배울 때(사실 전문가는 아님^^;), Single Responsibility나 모듈화에 대해 많이 강조하곤 하는데, 하나의 모듈 또는 객체가 하나의 책임만을 지게 코드를 짜라고들 한다. 다시 말하면 관심사끼리 잘 분리하라는 뜻인데, 이게 보는 사람마다 관심사가 다르고 해서 좀 애매하긴 하지만 Core의 동작을 swap, mint, burn 등의 행위로 나누고 Router에서는 Core의 동작을 이용하여 비즈니스 로직을 작성하는 부분은 V1보다는 확실히 개선됐다고 할 수 있을것 같다. 

     그리고 Core에 있는 대부분에 함수들에 safety체크를 하는 다른 컨트랙트에서만 호출하라는 주석이 달려있는데, Router에서 어떤 safety 체크를 하는지도 궁금해져서 한번 살펴보기로 했다. 우선 Router에 있는 많은 함수들이 ensure(deadline)이라는 modifer를 사용하고 있다. 어떤 데드라인으로 들어오는 파라미터가 블록의 타임스탬프 이상이어야만 함수가 실행된다. 그 외에도 다양한 require문을 통해 조건을 따지는데, swapETHForExactTokens와 같은 함수에서는 두 토큰 중 하나가 Wrapped ETH여야 하는 지 등을 체크하고, 모든 swap 함수에서는 amount값의 최소, 최대값을 체크한다. 어떤 조건을 체크하는지는 periphery를 더 깊게 분석할 때 이야기 해봐야겠다.

     요약하자면 핵심은 컨트랙트를 분리함으로써 모듈성을 확보하고, Router라는 상위 컨트랙트에서 유동성 공급, 스왑등의 다양한 작업을 Core를 통해 구현하도록 한 것이다. 만약 Router에서 구현하는 작업이 아닌 다른 동작을 구현하고 싶더라도, 컨트랙트를 새로 만들어서 Core의 기능들을 사용할 수 있을것이다. 스마트 컨트랙트는 업데이트가 되지 않기 때문에, 만약 Core에 많은 기능이 구현되어있다면 컨트랙트를 새로 배포해야 하는 번거로움이 있을것이다. 거기다 Uniswap처럼 많은 사람들이 사용하는 컨트랙트라면, 이미 사용하는 사람들을 migrate하는 작업또한 필요할 것인데, 생각만해도 막막한 작업이 될 것 같다. 
     또한 Router와 같은 상위 컨트랙트에서 다양한 조건을 체크하여 V1에서 발생했다는 문제들(아직 자세히는 모르지만 ERC777과 관련된 reentrancy attack이 발생했다고 한다.)을 해결하는것 같다. 아직 V1에서 발생한 문제들에 대해 자세히 조사해보지는 못해서 어떻게 해결했는지는 차차 알아봐야겠다.

     

     확실히 블로그를 하니까 뭔가 글을 써야겠다는 압박감도 생기고, 공부도하게 되는 것 같다. 다만 정말 나도 하나도 모르는 상태에서, 또는 얼핏 아는 상태에서 컨트랙트를 들여다보고 검색해보면서 글을 쓰는것이라 잘못된 정보가 있을 가능성이 매우매우매우매우 높다 ㅠ_ㅠ 혹시 글에서 잘못된 정보가 있거나, 보충하면 좋을 내용이 있다면 댓글 부탁드립니다!

    댓글

Designed by Tistory.