[Balancer] 1. Vault와 batchSwap 컨트랙트 분석

Jungwoo Pyo
BerryFi
Published in
18 min readApr 7, 2022

Balancer는 EVM-compatible한 시스템에서 DEX를 통한 자동화된 포트폴리오 매니지먼트를 제공하는 디파이 프로토콜이다. Vault와 Pool을 통해 자산의 관리를 위한 컨트랙트와 스왑 시 필요한 수학적인 로직을 구분하였고, 기존의 AMM 모델과 다르게 자산의 비율을 다르게 설정할 수 있는 weighted pool 등을 지원하거나 세 종류 이상의 자산을 이용한 LP 구성이 가능한 점 등 여러 기능을 제공한다. 이번 글에서는 Balancer의 주요 프로덕트 중 하나인 Vault 시스템과 Balancer에서 수수료를 줄이기 위해 사용되는 batchSwap에 대해 컨트랙트 코드 분석과 함께 설명한다. 이후의 아티클에서는 Pool과 Smart Order Router, 스왑에 적용되는 CoW Protocol 등에 대해 자세히 다루어볼 예정이다.

source

Author: 표정우

Audited By: 이태헌, 이동헌, 김홍욱

Intro

Balancer는 DEX 상에서 자동화된 포트폴리오 매니지먼트를 수행하게 하기 위한 디파이 프로토콜로, Fernando Martinelli가 처음 제안한 프로젝트이다. Balancer의 pool은 기존의 AMM 모델과 다르게 세 종류 이상의 다양한 자산으로 이루어진 pool을 구성하고, 유동성 제공에 따른 swap fee와 같은 파라미터들을 조절할 수 있다. 또한, weighted pool을 사용하게 되면 LP pool을 구성하는 자산의 비중이 50/50이 아닌 80/20, 90/10 등 유동성 공급자가 원하는 비율의 설정이 가능하다는 특징을 가지고 있다.

분산환경에서 Balancer의 역할은 전통 금융시장에서의 자동화된 인덱스 펀드와 유사하다. 다만, 인덱스 펀드는 구매자들이 수수료를 지불하면서 브로커에 의해 포트폴리오 리밸런싱이 이루어지는데 반해 Balancer pool은 trader들의 스왑에 의해 자동적으로 포트폴리오 리밸런싱이 이루어지면서 오히려 스왑에 대한 수수료를 사용자들에게 혜택을 줄 수 있다.

Balancer Protocol의 핵심은 결국 디파이 환경에서의 자유로운 자산관리 시스템을 구축할 수 있다는 데 있다. 풀을 구성하는 자산의 종류부터 시작해서 자산의 구성 비율, swap fee에 대한 구조 등 DEX를 구축하는 데 필요한 대부분의 parameter를 LP가 커스터마이즈할 수 있기 때문에 Balancer를 활용한 DEX의 구축은 큰 무기가 될 것이다.

Balancer의 Product: The Vault

Balancer에서 제공하는 product들은 Vault, Weighted Pool, Smart Order Router 등 여러 가지가 있지만, 이번 글에서는 Balancer의 핵심 프로덕트 중 하나인 Vault 모듈과 Vault를 구성하고 있는 핵심적인 기능들을 살펴보고, 이를 통해 어떻게 트랜잭션 수수료를 낮추고 슬리피지 등의 요소를 줄였는지에 대해 코드 분석과 함께 설명하겠다.

Token management와 Pool logic을 구분

기존의 DEX들은 LP pool이 소유하고 있는 토큰이 pool contract에 직접 보관되어 있었다. 그래서 pool의 swap method를 실행하면 pool contract가 보유한 자산을 바꾸어주는 방식으로 스왑이 수행되었다.

하지만 Balancer의 Vault 구조에서는 token management와 pool logic을 구분하였다. Balancer가 소유하고 있는 모든 token은 Vault에서 일괄적으로 관리하고, pool contract는 vault contract와 분리되어 오직 swap, join, exit 등 AMM의 자산 스왑과 관련된 수학적인 계산만을 수행하는 역할을 담당한다. 그래서 pool contract는 기존의 DEX pool contract에 비해 구조가 많이 단순화되었다. 누군가가 trading system에 대한 새로운 아이디어가 있으면, 해당 토큰의 유동성이 작다고 걱정할 필요 없이 누구나 커스텀 풀을 만들어서 Balancer가 가지고 있는 유동성을 활용해 Liquidity Pool을 구성할 수 있다.

그림 1. Balancer의 Token management와 Pool logic의 분리

BatchSwap: Gas fee optimization

Multi-hop trading은 여러 번의 token transfer를 통해 토큰 스왑을 수행하는 방법을 말한다. 동일한 메인넷 네트워크 상에서 사용자가 원하는 토큰과 보유한 토큰의 pair LP가 구성되어 있지 않거나 그 유동성이 부족하여 price slippage가 크게 발생할 가능성이 높은 경우에 빈번하게 사용된다.

Vault에서는 multi-hop trading에 BatchSwap이라는 방법을 사용한다. Vault는 단일 contract에 모든 토큰을 소유하고 있기 때문에, multi-hop trading을 수행하는 과정에서 타 DEX와 다르게 실제 transfer를 수행하지 않는다. 대신 스왑 수행 이후의 각 자산의 delta 값을 Vault contract의 _internalTokenBalance라는 스토리지 변수에 반영함으로써 token transfer의 횟수를 최소화한다. 좀 더 자세히 설명하면, ERC20 토큰의 전송은 해당 토큰의 컨트랙트 주소를 call한 후 transfer를 수행하기 때문에 가스비의 소모가 크다. 하지만 Vault contract 내부에 선언된 스토리지 변수(internal balance)를 조정하는 방식을 사용하면 contract call을 통한 토큰의 직접 transfer 방식에 비해 가스비 소모가 적다. 이는 기존의 방식에 비해 높은 gas efficiency를 보여준다.

그림 2. Multi-Hop Trading Method 비교(BatchSwap vs Multiple Single Swaps)

Security

모든 token을 단일 Vault contract에 보관하고 각 LP pool의 balance를 기록해놓는 Vault의 특성 상, vault contract는 pool contract와 독립적으로 동작하게 된다. 이러한 독립성을 유지하는 것은 크게 2가지 장점이 있다.

1. custom pool을 형성하여 fund draining을 시도하는 등의 악의적인 행위를 막을 수 있다.

DEX는 블록체인 상의 permissionless system 위에서 작동하는 어플리케이션이다. 기존의 DEX처럼 pool 별로 유동성 및 자금이 관리되면 악의적인 목적을 가진 누군가 페어 토큰의 시장 가격과 다른 비율로 custom pool을 구성할 수 있다.

예를 들어 BTC의 시장가가 100$, ETH가 50$이라면 초기 custom pool을 생성할 때 토큰의 수를 1:2 비율로 설정해야 한다(50/50 weighting 기준). 하지만 pool 생성자가 이 비율을 벗어난 형태로 구성할 경우 해당 pool을 통해 스왑을 사용하는 사용자는 시장가격과 다른 가격에 토큰을 스왑하는 것이 되어 큰 손해를 볼 수 있고, 반대로 custom pool 생성자는 큰 금전적인 이득을 취할 수 있다.

하지만 Balancer를 활용하면 BTC/ETH의 토큰의 수를 1:4 비율 등 시장가의 비율과 다르게 임의로 설정하는 것이 어렵다. 예로 기존의 DEX pool의 같은 경우 pool 내에서 자금을 관리하기 때문에 소액을 활용하여 BTC/ETH의 토큰의 양을 1:100의 초기 구성으로 설정하면 사용자가 실수로 해당 pool을 통한 스왑을 진행하는 등의 경우가 발생할 수 있다. Balancer는 AMM의 수식을 볼트 전체가 보유한 자산을 이용하여 계산하기 때문에 애초에 토큰의 수를 1:100 으로 구성할 수 없다. Vault에서 보유하고 있는 BTC와 ETH의 양이 충분할 경우, 시장가격이 이미 전부 반영되어 있기 때문에 토큰의 가격을 조작하는 것이 불가능하기 때문이다. 이는 pool 단위에서 자산을 관리하는 것이 아니라 Vault에서 자산을 모아서 관리하기 때문에 가능한 Balancer만의 장점이라고 볼 수 있다.

2. pool의 유동성이 분리된 pool을 통해 운영하는 것보다 커서 price slippage로부터 강건하다.

pool 생성자가 100개의 A token을 보유하고 있고, A/B, A/C 두 개의 페어 풀에 유동성 공급을 할 경우 기존의 DEX에서는 보유한 A를 페어 풀에 적절한 비율(ex. A/B에 A 토큰 50개, A/C에 A토큰 50개)로 나누어 공급하였다. 이러한 방법은 보유한 A token에 비해 유동성의 규모가 축소되는 결과를 낳는다.

Balancer에서 pool은 실제 토큰을 보유하지 않는 virtual pool이기 때문에 A/B, A/C 두 개의 풀에 모두 100개의 A 토큰에 해당하는 유동성을 공급할 수 있다.

Flashloan

Vault에서는 플래시론을 지원한다. 플래시론이란, 단일 트랜잭션을 통해 무위험 차익을 얻고자 하는 차익거래자가 자본이 없는 상태에서도 무담보로 특정 자산을 대출받은 후, LP의 비율이 달라져 있는 DEX에서의 스왑을 통해 차익을 얻은후 바로 상환하는 방법이다. 플래시론을 직접 실행하는 차익거래자는 해당 행위를 통해 수익을 얻을 수 있고, 디파이 프로토콜은 플래시론을 지원함으로써 LP를 구성하는 자산들의 불균형을 해소하여 보다 정확하고 균일한 가격을 제공할 수 있다는 장점이 있다.

컨트랙트 분석을 통한 batchSwap 메커니즘 파악

batchSwap과 관련된 컨트랙트 코드 분석을 통해 자세한 batchswap의 메커니즘을 확인해보았는데,특히 Vault의 internal balance를 활용하여 여러 번의 스왑에 대한 gas fee를 잘 절약하는지에 대해 중점적으로 살펴보았다.

Swapkind에 따라 Given-in, Given-out을 구분

batchSwap은 여러 건의 연속적인 스왑이 포함되며, 이를 multi-hop swap이라고 부른다. 스왑의 종류는 열거형 변수인 Swapkind kind값에 따라 given-in swap과 given-out swap 두 가지로 구분된다.

  • GIVEN_IN: 사용자가 pool로 보내려고 하는 A 토큰의 양을 고정(주어진 값)하고, 이에 따라 사용자가 받게 될 B 토큰의 양이 계산(계산된 값)된다.
  • GIVEN_OUT: 사용자가 pool로부터 받게 되는 B 토큰의 양을 고정(주어진 값)하고, 이에 따라 pool이 보내게 될 A 토큰의 양이 계산(계산된 값)된다.

주어진 값은 이전 스왑의 결과로 얻게 되는 토큰의 양과 같이 이미 결정된 값을 의미한다. 계산된 값은 pool contract의 수식에 의하여 계산된 값으로, vault의 internal balance에 적용하기 위한 값이다.

multi-hop swap에서 In, out에 해당하는 토큰의 양이 주어진 값인지 계산된 값인지를 구분하는게 중요한 이유는 스왑 경로에 여러 종류의 intermediate token들이 포함될 수 있기 때문이다. Targeted token이 아닐 경우 token transfer를 수행해야 할 필연적인 이유는 없고, 무의미하게 token transfer를 수행하면 많은 가스비가 발생하게 된다. 따라서 intermediate token과 관련된 스왑의 경우는 수량에 대한 계산을 통해 Vault의 internal balance만을 조정하고 실제 token transfer는 수행하지 않는다. 해당 부분에 대한 설명은 아래에서 BatchSwapStep 구조체를 설명할 때 더 구체적으로 설명하겠다.

batchSwap을 이해하기 위한 자료형 및 구조체

batchswap을 이해하기 위한 상태 및 구조체는 다음과 같다.

IVault.sol
  1. SwapKind:연속적인 스왑에 대한 특성이 어떠한지를 나타내는 것으로, GIVEN_IN과 GIVEN_OUT 두가지로 나뉜다.
  2. BatchSwapStep: 스왑에 대한 정보를 포함하며, BatchSwapStep에 포함된 필드는 다음과 같다.

poolId: pool의 ID

AssetInIndex: assets에서 tokenIn ERC20의 contract address가 포함된 인덱스

AssetOutIndex: assets에서 tokenOut ERC20의 contract address가 포함된 인덱스

amount

kind 값에 따라 amount가 의미하는 바가 다르다. kind가 GIVEN_IN인 경우, amount는 tokenIn의 주어진 값이며, 스왑 과정에서 tokenOut의 계산된 양을 구하게 된다. GIVEN_OUT일 경우는 그 반대로 amount = (tokenOut의 주어진 값)이 되며, 스왑 과정에서 tokenIn의 계산된 양을 구한다.

caller가 swaps.length가 2 이상인 multi-hop swap을 실행할 경우, swaps[0]을 제외한 나머지 element의 amount 값은 전부 0으로 할당시키게 된다.(GIVEN_IN swap의 경우)

batchSwap 과정에서(정확히는 _swapWithPools()에서) amount에 0이 할당된 경우의 step은 이전 BatchSwapStep의 calculated amount로 재할당된 후 balance 계산이 이루어진다.

userData: 해당 필드는 Vault에서는 무시되나, 추후에 Pool의 onSwap hook을 수행하는데 전달되어 swap의 기능을 확장하는데 사용된다.

3. FundManagement: Swap fund를 vault internal balance를 통해 집행할지 token transfer로 집행할지에 대한 내용이 담겨 있다.

address sender, recipient: 보내는 주소와 받는 주소

bool fromInternalBalance: asset을 보낼 때 자산을 sender의 Vault internal balance에서 보낼 것인지 아니면 직접 token transfer를 통해 보낼 것인지 선택하는 필드이다. True일 경우 vault에서 스왑을 집행하게 되며 가스비가 적게 든다는 장점이 있다. 혹시 보내는 토큰의 양이 sender의 vault internal balance보다 많을 경우 sender의 지갑으로부터 차액에 해당하는 토큰이 추가로 스왑된다.

bool toInternalBalance: fromInternalBalance와 반대로, asset을 받을 때 recipient가 Vault internal balance로 받을지 직접 token transfer로 받을지를 선택한다. True일 경우 Vault로 송금받는다.

batchSwap의 argument

IVault.sol
  • SwapKind kind: 연속적인 스왑에 대한 특성이 어떠한지를 나타내는 것으로, GIVEN_IN과 GIVEN_OUT 두가지로 나뉜다.
  • BatchSwapStep[] memory swaps: 연속적인 스왑 각각에 대한 정보를 array 형태로 제공한다.
  • Asset[] memory assets: batchSwap에 사용되는 모든 asset의 ERC20 contract address가 담긴다.
  • FundManagement memory funds: Swap fund를 vault internal balance를 통해 집행할지 token transfer로 집행할지에 대한 내용이 담겨 있는데, 모든 swap에 통용되는 변수이기 때문에 array 형태로 제공하지 않는다.
  • int256[] memory limits: 스왑이 허용되는 토큰의 최대 양이다. 급격하게 변하는 시장과 여러 MEV 공격으로부터 사용자를 보호하기 위해 스왑에 포함된 모든 토큰에 대하여 limits 라는 변수를 할당하여 이동 가능한 토큰의 수의 최댓값을 설정해 놓았다.
  • uint256 deadline: 지정한 시간이 지나면 swap을 revert한다.

batchSwap 함수의 메커니즘

Swaps.sol

batchSwap의 흐름은 크게 세 단계로 구분하여 살펴볼 수 있다.

  1. _swapWithPools()를 통해 스왑에 포함되는 모든 자산의 변화량을 assetDeltas에 저장한다.
  2. 자산별로 delta값이 주어진 limits을 초과하지 않는지 체크한다.
  3. delta > 0이면 funds.sender로부터 delta 만큼 해당 자산을 전송받고, delta < 0이면 funds.recipient에게 (-delta)만큼의 자산을 보낸다.

1. _swapWithPools()를 통해 스왑에 포함되는 모든 자산의 변화량을 assetDeltas에 저장한다.

Swaps.sol

_swapWithPools()의 역할은 BatchSwapStep[] swaps 값을 이용하여 swaps.length 번의 연속적인 스왑을 수행하는 것이다. swaps.length가 1인 경우는 타 AMM에서 사용하는 단일 스왑과 다른 점이 없다. 하지만 length가 2 이상인 경우는 multi-hop swap으로, 스왑의 target asset과 무관한 intermediate token이 batchSwap 과정에 포함되게 된다.

해당 함수 내부에서는 swaps[i].amount의 값을 확인하는데, 이것이 0으로 할당되어 있으면 swaps[i]을 multihop swap과정에서 intermediate token이 포함된 거래로 간주한다. 그래서 swaps[i-1]에서의 계산된 값을 swaps[i].amount로 할당하게 된다. 이유는 위에서 설명하였듯이, 스왑의 종류가 GIVEN_IN swap이라고 가정하였을 때 바로 앞 단계 스왑에서의 tokenOut amount는 계산된 값이고, 스왑은 sequential하게 실행되므로 이것이 곧 현재 단계 스왑에서의 tokenIn amount가 되기 때문이다.

2. 자산별로 delta값이 주어진 limits을 초과하지 않는지 체크한다.

Vault의 자산을 보호하기 위한 조치로, 간단한 _require() 함수를 통해 처리한다.

3. delta > 0이면 funds.sender로부터 delta 만큼 해당 자산을 전송받고, delta < 0이면 funds.recipient에게 (-delta)만큼의 자산을 보낸다.

assetDeltas는 모든 assets에 대해 계산한 delta의 array이다. 이를 iterate하면서 다음을 확인한다:

  • delta 값이 0 초과일 때: funds.sender가 delta에 해당하는 ERC20 혹은 WETH를 보낸다
  • delta 값이 0 미만일 때: funds.recipient가 -delta에 해당하는 ERC20 혹은 WETH를 받는다

delta가 0보다 클 경우, _receiveAsset() 내부 함수를 이용해 funds.sender에서 vault로 자산이 이동하고, delta가 0보다 작을 경우에는 _sendAsset()을 이용해 vault에서 funds.recipient로 자금이 이동한다. _receiveAsset()의 내부를 한번 살펴보자.

AssetTransfersHandler.sol

_receiveAsset()에서는 해당 스왑이 internalBalance를 이용하는지를 체크하고, sender가 delta 이상의 자산을 vault에 보유하고 있으면 이를 internal balance에서 차감한다. 만일 delta 값이 sender가 vault에 보유한 internal balance보다 크다면 나머지를 ERC20의 transfer 기능을 활용하여 직접 전송하게 된다.

_sendAsset() 역시 비슷한 메커니즘으로 작동한다.

마치며

본 아티클을 통해 알아본 Balancer의 특징을 요약하면 다음과 같다.

  • Balancer는 자산 관리를 위한 LP Pool을 구성하는 자산의 종류 및 그 비율을 자유롭게 조절하고, liquidity mining을 통한 수수료 및 스왑에 대한 수수료도 관리자에 입맞에 맞게 설정 가능하기 때문에 DeFi 시스템 구축을 위해 효과적이고 유연한 building block을 제공한다.
  • Vault contract에서 모든 자산이 관리되며, pool contract는 AMM을 위한 수식을 적용하기 위한 것이지 실제 자산이 들어있지 않는 virtual pool이다.
  • batchSwap을 활용하면 multi-hop swap을 수행할 때 vault의 internal balance를 조정하는 방법을 통해 token transfer를 줄임으로써 수수료를 최소화할 수 있다.

이번 아티클에서는 Balancer에 대한 전반적인 설명과 주요 product에 대한 소개, 그 중에서도 Vault에 대해 집중적으로 다루어 보았다. 다음 아티클에서는 Pool에 대해서 자세히 다루어보도록 하겠다.

--

--

Jungwoo Pyo
BerryFi

Crypto Researcher & Software Engineer | Common Computer | Decipher