탈중앙화 거래소
탈중앙화 거래소(Decentralized Exchanges, 이하 DEX)는 자산을 중앙화된 거래소에 맡기지 않고, 사용자가 자기 지갑에 있는 암호화폐를 직접 거래할 수 있도록 해주는 거래소입니다.
듣기엔 간단해 보이지만 DEX 구현은 그리 간단하진 않습니다. 수학과 경제 지식이 있어야 DEX를 구현할 수 있기 때문입니다.
유니스왑
탈중앙화 거래소는 유니스왑(Uniswap)이 주도하고 있습니다. 유니스왑은 이더리움 생태계에서 가장 거래가 많은 DEX일 뿐만 아니라 사용량이 많은 DApp(탈중앙화 애플리케이션)이기도 합니다.
유니스왑의 창시자 헤이든 아담스(Hayden Adams)는 비탈릭 부테린이 2017년에 자신의 블로그에 게시한 On Path Independence를 본 후, 영감을 받아 비탈릭의 아이디어를 유니스왑에 구현해 보기로 다짐합니다. 1년이 넘는 시간 동안 개발에 몰두한 헤이든은 마침내 2018년 11월에 유니스왑 초기 버전 발표, 출시하였습니다. 유니스왑의 역사에 대한 자세한 내용은 창립자가 작성한 블로그 포스팅에서 볼 수 있습니다.
이 글에선 유니스왑 초기 버전을 작동하게 해주는 수식과 함께 탈중앙화 거래소가 무엇인지 대해 알아보겠습니다.
문제정의
“기존 거래소 로직을 그대로 가져다 스마트 컨트랙트로 구현하면 탈중앙화 거래소를 만들 수 있는 거 아니야?”라고 생각할 수 있습니다. 가능은 하지만 기존 로직만으론 충분하지 않습니다.
중앙 거래소는 일반적으로 오더북(order book) 기반으로 동작합니다. 오더북은 이런식으로 동작합니다. 만약 수지가 ‘토큰 A’ 100개를 ‘토큰 B’ 50개에 판매하겠다는 주문을 내면 해당 내역이 오더북에 추가됩니다. 어느 순간 서준이가 나타나 ‘토큰 A’ 100개를 ‘토큰 B’ 50개에 사고싶다고 주문하면, 오더북 내에 있는 수지의 주문이 서준이의 주문과 짝이 맞기 때문에(매칭) 거래가 체결됩니다.
오더북은 매수 또는 매도 ‘주문’을 저장하는 ‘데이터베이스’ 입니다.
오더북 기반 거래를 시도했던 프로젝트가 이더리움에도 있었습니다. 대표적인 예는 0x 프로젝트였는데, 주문 등의 정보를 온체인에 저장하고 매칭 알고리즘을 실행하는 데 너무 많은 가스가 소요되어 사람들의 이목을 그다지 끌지 못했습니다.
오더북 없이도 사용자가 두 토큰을 교환할 수 있는 새로운 접근 방식이 필요했습니다.
유니스왑의 진화
2023년 10월 기준으로 공식적으로 발표된 최신 유니스왑 버전(version, 이하 v)은 v4입니다. v3은 2021년 5월에 메인넷에 출시 되었고, v4 백서는 공개 되었지만 아직 개발 및 검증 중입니다.
첫 번째 버전인 유니스왑 v1은 2018년 11월에 출시되었으며 ETH와 ERC-20 토큰 간 스왑을 가능하게 해주었습니다. 토큰 A와 토큰 B 스왑, 즉 토큰 간 스왑도 가능하게 해줬는데, 이 경우엔 두 토큰 중 하나를 먼저 ETH로 전환한 다음 이 ETH를 다른 토큰을 사는 데 쓰는 식으로 동작(chained swap, 연쇄 스왑)했습니다.
유니스왑 v2는 2020년 3월에 출시되었는데 v1에 비해 크게 개선되어 두 ERC-20 토큰 간의 직접 스왑은 물론, 직접 스왑이 불가능한 경우 연쇄 스왑을 통해 두 토큰 스왑이 가능해졌습니다.
2021년 5월에 출시된 유니스왑 v3에선 자본 효율성이 크게 개선되었습니다. 기존엔 유동성 공급자가 풀(pool)에 넣은 자금이 해당 풀의 가격 범위 전체에서 사용되었는데, v3에선 유동성 제공 시 특정 가격 범위를 설정할 수 있게 되어 유동성 공급자들은 더 적은 자본으로도 높은 보상을 얻을 수 있게 되었습니다.
유니스왑 v4에선 풀에 커스텀 훅을 추가할 수 있고, 단일 유니스왑 컨트랙트로 DEX의 모든 풀을 관리할 수 있게 해줘 가스비를 절감하고 다양한 풀을 맞춤으로 만들 수 있도록 해줄 예정입니다.
이 글에선 DeFi의 기초를 다룬다는 측면에서 유니스왑 v1의 설계에 대해 자세히 설명하겠습니다.
마켓 메이커
유니스왑은 자동화된 마켓 메이커(Automated Market Maker, 이하 AMM)를 구현한 제품입니다. AMM이 정확히 무엇인지 알아봅시다.
AMM을 알아보기 전에 먼저 마켓 메이커가 무엇인지 알아봅시다. 마켓 메이커(시장 조성자)는 거래 시장에 유동성(자산)을 공급하는 주체입니다. 오더북이 아닌 시스템에서는 유동성이 있어야 거래가 가능합니다. BTC를 매도하여 ETH를 구매하려고 하는 경우를 상상해 봅시다. 시장에 BTC로 구매할 수 있는 ETH가 있어야 이 거래가 가능합니다. 그런데 시장엔 유동성이 높은 거래 쌍(예: BTH/ETH 쌍)도 있지만 그렇지 않은 거래 쌍(예: 스캠 또는 새로 생성된 토큰으로 구성된 거래쌍)도 많습니다. 마켓 메이커는 이렇게 유동성이 적은 거래 쌍을 대상으로 자산을 공급하는 역할을 합니다.
DEX 역시 전통 거래소와 마찬가지로 충분한 유동성을 보유해야 합니다. 유동성을 확보하는 방법으론 DEX 개발자(또는 투자자)가 자신의 자금을 투입하여 마켓 메이커가 되는 방법이 있습니다. 그런데 이 방법은 가능한 모든 거래 쌍에 충분한 유동성을 제공해야 하기 때문에 막대한 자금이 들어 비현실적입니다. 또한 개발자/투자자가 시장의 모든 권한을 보유하게 되므로 ‘탈중앙’이라는 철학에도 위배됩니다.
이런 단점을 극복하기 위해 유니스왑은 누구나 마켓 메이커가 되도록 하는 기능을 고안해 냅니다. 누구나(유동성 공급자) 특정 거래 쌍에 자금을 예치할 수 있도록 해주고, 그 대가로 토큰 스왑시 DEX 사용자로부터 받은 거래수수료를 통해 유동성 공급자들이 이득을 얻을 수 있도록 해줘 유동성 문제를 해결하게 됩니다.
AMM은 암호화폐 시장, 특히 탈중앙화 거래소(DEX)에서 발견되는 혁신적인 개념으로, 전통적인 주문형 거래 방식을 사용하는 대신, 프로그램화된 방식으로 자산의 가격을 계산하고 유동성을 제공합니다. AMM 시스템은 미리 정의된 수학적 공식(가격 결정 알고리즘)에 따라 가격을 자동으로 조정합니다. 이 알고리즘은 거래가 발생할 때마다 자동으로 실행되어, 실시간으로 가격을 업데이트합니다.
DEX 기능 요구사항
AMM을 통해 유동성을 공급받는 DEX를 구현하려면 다음과 같은 조건을 만족해야 합니다.
- 누구나 유동성을 추가하여 유동성 공급자(Liquidity Provider, 이하 LP)가 될 수 있게 한다.
- LP는 원할 때 언제든지 유동성을 제거하고 풀에 예치한 암호화폐를 되찾을 수 있다.
- DEX 사용자는 유동성이 충분하다는 가정하에 풀에 있는 자산간 스왑을 한다.
- 사용자에게는 소액의 거래 수수료가 부과되며, 수취된 수수료는 LP에게 배분된다. 이를 통해 LP는 유동성 공급에 대한 수익을 얻는다.
x * y = k
유니스왑 v1의 핵심엔 x * y = k
라는 방정식(상수 곱 상품 모델)이 있습니다.
ETH와 특정 토큰(TOKEN)이 쌍인 트레이딩을 한다고 가정했을 때, 위 방정식에서 변수는 각각 아래를 의미합니다.
x
: 트레이딩 풀에 있는 ETH 잔액y
: 트레이딩 풀에 있는 TOKEN 잔액k
: 상수
공식에 따르면 k
는 x
, y
에 상관없이 일정합니다. 토큰 간 스왑이 발생할 때마다 ETH 또는 TOKEN 중 한쪽 잔액은 증가하고 다른 한쪽은 감소합니다.
공식을 조금 수정해 스왑이 이뤄지는 것을 표현해 봅시다. ETH를 TOKEN으로 스왑한다 가정하면, 풀 내 ETH 잔액이 증가하고 TOKEN 잔액이 감소합니다. 이를 표현해 봅시다.
(x + Δx) * (y — Δy) = k
이 식에서 각 변수는 다음을 의미합니다.
x
: 트레이딩 풀에 있는 기존 ETH 잔액y
: 트레이딩 풀에 있는 기존 TOKEN 잔액Δx
: 사용자가 TOKEN을 구매하기 위해 풀에 추가하는 ETHΔy
: 사용자가 풀에서 인출받게 되는 TOKEN
그런데 (x + Δx) * (y — Δy) = k
를 조금 변형하면 다음과 같은 식을 만들 수 있습니다.
Δy = (y * Δx) / (x + Δx)
이 수학식을 솔리디티로 작성해봅시다.
function calculateOutputAmount(
uint inputAmount,
uint inputReserve,
uint outputReserve) private pure returns (uint) {
uint outputAmount =
(outputReserve * inputAmount) / (inputReserve + inputAmount);
return outputAmount;
}
예시를 통해 코드를 자세히 이해해 봅시다. 트레이딩 풀(스마트 컨트랙트)에 100 ETH와 200 TOKEN이 있다고 가정합시다. 이때 1 ETH를 TOKEN 몇 개로 스왑할 수 있는지 계산해 보겠습니다.
각 변수 값은 다음과 같습니다.
- inputAmount
= 1ETH
- inputReserve
= 100ETH
- outputReserve
= 200TOKEN
계산결과 outputAmount
는 1.98019802 TOKEN이 됩니다.
이번엔 2 TOKEN을 ETH로 스왑할 때를 가정하고 계산해 봅시다.
- inputAmount
= 2TOKEN
- inputReserve
= 200TOKEN
- outputReserve
= 100ETH
계산 결과 2 TOKEN은 0.9901ETH로 스왑 가능합니다.
컨트랙트엔 두 토큰이 1:2 비율로 예치되어 있기 때문에 언뜻 생각하면 1 ETH를 TOKEN으로 스왑할 땐 2 TOKEN이, 2 TOKEN을 ETH로 스왑할 땐 1 ETH를 받을 수 있다고 예상할 수 있는데 실제 결과는 예상보단 적습니다. 왜 그런 걸까요?
x * y = k
방정식을 그래프로 그리면 다음과 같은 쌍곡선이 나옵니다.
쌍곡선은 x = 0 또는 y = 0에서 교차하지 않으므로, 토큰을 교환한다고 해서 예치량(준비금)이 0이 될 수는 없습니다. x * y=k 방정식을 따르는 DEX에선 이런 식으로 준비금이 0이 될 수는 없습니다.
슬리피지
토큰 스왑시 풀에 있는 준비금 비율대로 토큰을 받지 못하기 때문에 사용자는 슬리피지(slippage, 주문을 실행할 때 예상 가격과 실제 거래 가격 사이의 차이)로 인한 손해를 보게 됩니다.
슬리피지는 주문이 실행될 때 예상 가격과 실제 거래 가격 사이의 차이를 말합니다. DEX는 상수 곱 상품 모델을 사용하기 때문에 DEX에선 거의 모든 거래에서 어느 정도 슬리피지가 발생합니다. 이외에도 슬리피지는 대규모 주문이나 유동성이 부족한 시장에서 발생하며, 주문의 크기가 해당 시장의 현재 유동성을 초과할 때 가장 자주 발생합니다.
규모가 큰 거래는 풀의 토큰 잔액에 상대적으로 더 큰 영향을 미치므로 슬리피지도 늘어납니다.
위 예시를 다시 생각해 봅시다(풀에 100 ETH와 200 TOKEN이 있음). 200 ETH를 사용해 풀을 모두 고갈시킬 수 있는지 시도해봅시다.
- inputAmount
= 200 ETH
- inputReserve
= 100 ETH
- outputReserve
= 200 TOKEN
위 값을 코드에 넣으면 다음과 같은 결과가 도출됩니다.
- outputAmount
= 133.333 TOKEN
준비금 비율(ETH : TOKEN = 1 : 2)만 봤을 땐 스왑 결과로 400 TOKEN을 받을 수 있을 거라 기대했겠지만, 슬리피지 때문에 그 절반도 안 되는 133.333 TOKEN만 받을 수 있습니다.
이런 현상 때문에 혹자는 x * y = k를 따르는 AMM은 결함이 있다고 생각할 수 있지만 실제론 그렇지 않습니다. AMM 메커니즘은 풀이 완전히 고갈되는것을 예방할 수 있다는 장점이 있습니다. 또한 수요가 공급을 초과할 때 비용이 증가하는 수요-공급의 법칙에도 어긋나지 않습니다.
초기 토큰 가격 결정
새 암호화폐가 만들어지면, 해당 토큰 관련 거래 쌍이 없기 때문에 DEX 내 유동성이 전무하게 됩니다. 따라서 가격을 계산할 방법이 없습니다. 이때는 풀에 유동성을 가장 먼저 추가하는 사람이 가격을 설정하게 됩니다(참고로 유동성을 추가하려면 거래 쌍에 해당하는 두 토큰을 모두 풀에 추가해야 합니다. 한쪽에만 유동성을 추가할 수는 없습니다).
초기 풀 생성자가 유동성을 공급하면 x * y = k에서 x와 y값이 설정됩니다. 이때부터 스왑시 가격 계산이 가능해집니다.
그럼 지금부턴 유동성 공급 관련 함수를 한번 간단히 구현해 봅시다.
function addLiquidity(uint256 tokenAmount) public payable {
IERC20 token = IERC20(tokenAddress);
token.transferFrom(msg.sender, address(this), tokenAmount);
}
이 함수를 호출하면 어떤 토큰이든 풀에 추가할 수 있습니다. 하지만 비율 관련 로직이 없습니다. 함수를 좀 더 개선해 봅시다.
초기 유동성 공급자 이후에 다른 사람이 와서 유동성을 공급한다고 가정해 봅시다. 이 사람이 기존 비율과 다른 비율로 유동성을 공급하면 토큰 가격에 큰 영향을 미칠 수 있습니다. 이런 가격 조작을 방지하는 코드를 추가해 보겠습니다.
function addLiquidity(uint tokenAmount) public payable {
// assuming a hypothetical function
// that returns the balance of the
// token in the contract
if (getReserve() == 0) {
IERC20 token = IERC20(tokenAddress);
token.transferFrom(msg.sender, address(this), tokenAmount);
} else {
uint ethReserve = address(this).balance — msg.value;
uint tokenReserve = getReserve();
uint proportionalTokenAmount =
(msg.value * tokenReserve) / ethReserve;
require(tokenAmount >= proportionalTokenAmount, “incorrect ratio of tokens provided”);
IERC20 token = IERC20(tokenAddress);
token.transferFrom(msg.sender, address(this), proportionalTokenAmount);
}
}
풀에 새롭게 유동성을 추가하려는 사람을 풀에 이미 설정된 비율과 동일한 비율로 유동성을 추가하게 강제하는 코드가 추가되었습니다. 여기에 더하여 풀이 완전히 비워졌을 때 임의의 비율을 허용하는 예외 처리를 추가하였습니다.
LP 토큰
지금까지 유동성을 추가하는 방법과 스왑 가격 계산 방법에 대해 알아보았습니다. 하지만 유동성 공급자가 풀에서 유동성을 인출할 때 발생하는 일에 대해선 다루지 않았습니다. 지금부턴 유동성 인출시 중요한 역할을 하는 LP 토큰에 대해 알아보겠습니다.
유동성 공급자가 유동성을 공급하지 않으면 DEX 사용자는 스왑을 통해 원하는 토큰을 얻을 수 없습니다. 이렇듯 DEX에서 유동성 공급자의 역할은 아주 중요합니다. 따라서 DEX를 설계할 땐 유동성 공급자에게 유동성을 공급할 이유, 즉 경제적 보상을 제공해 줘야 합니다. 유동성 공급시 얻을 수 있는 것이 없다면 그 누구도 제 3자가 만든 컨트랙트에 토큰을 예치하지 않을것은 자명하기 때문입니다.
유동성 공급자가 유동성을 계속 공급할 수 있도록 유인할 수 있는 방법으론 토큰 스왑이 일어날 때마다 사용자로부터 소액의 수수료를 징수하고, 이 수수료를 유동성 공급자에게 배분하는 방식이 있습니다. 이때 유동성 공급자가 배분받는 수수료는 얼마나 많은 유동성을 제공했는지에 따라 달라집니다. 전체 유동성 중 50% 제공했으면 수수료의 50%를 받는식으로 말이죠.
이를 실현하려면 LP토큰이란 개념이 필요합니다. LP토큰은 주식처럼 동작하는데 아래와 같은 요구사항을 만족시킵니다.
- 유동성을 공급하면 그 댓가로 LP토큰을 받습니다.
- 지급받는 LP토큰의 양은 풀에 공급한 유동성 지분에 비례합니다.
- 보유하고 있는 LP토큰에 비례하여 수수료를 배분받습니다.
- 유동성 공급을 해제하는 경우, 유동성 공급자는 LP토큰을 거래소에 반환하고, 풀에 예치했던 기존 토큰 쌍과 그 동안 벌어들인 수수료를 회수합니다.
그리고 여기에 더하여 다음과 같은 추가 요구사항도 필요합니다.
- 다른 사람이 추가로 유동성을 공급하거나 해제 해 풀의 총 가치가 변할 때 기존 공급자의 LP 토큰 상대적 지분(퍼센트로 표시된 지분)은 변할 수 있지만 지분이 대표하는 절대적 자산 가치는 기존에 투자한 금액을 기반으로 유지되어야 합니다.
- LP 토큰을 발행하고 위 요구사항들을 만족시키는데 드는 가스비는 최소화 되어야 합니다.
그럼 지금까지 다룬내용을 바탕으로 새로운 유동성 풀(liquid pool)이 생성되고 유동성이 추가될 때 LP 토큰을 어떻게 해야 모든 요구사항들을 만족시킬 수 있을지 생각해봅시다.
먼저 풀이 처음 생성되는 경우를 생각해 봅시다. 풀이 생성되면 해당 풀의 LP 토큰은 최초 유동성 공급자에게 처음 발행되어야 하는데, 발행할 수 있는 LP 토큰 숫자를 미리 정해놓고 풀 최초 생성자에게 LP 토큰 전부를 발행하는 방식을 채택했다고 가정해 보겠습니다. 이 방식은 문제가 있습니다. 두 번째 이후 사람이 유동성을 추가할 때마다 지분을 다시 계산해야 하는데, 이때 드는 가스 비용이 많이 들어서 마지막 조건을 만족시키지 못하기 때문입니다.
아주 많은 수(고정값)의 LP 토큰을 발행한 후, 최초 풀 생성자에게 발행할 수 있는 LP 토큰 중 일부만 주고, 유동성 공급자가 추가 될 때마다 남은 LP 토큰을 주는 방식도 생각해 볼 수 있는데, 이 방식 역시 유동성 공급자가 계속 추가되면 언젠간 지분 비율을 다시 조정해야하기 때문에 지분 재 계산에 많은 가스비를 소모해야 하는 상황이 발생합니다. 역시 적절치 않습니다.
유니스왑 창시자인 헤이든은 ‘가스비 최적화’라는 제한 하에, 위에 언급한 요구사항을 반영하기 위해 풀에 유동성이 추가될 때마다 LP 토큰을 무한정 발행하는 방식을 고안해 냅니다. 이렇게 하면 유동성 공급과 해제를 제한 없이 할 수 있고, 가스비를 많이 소모하지 않으면서도 기존 유동성 공급자의 절대적 자산 가치는 유지할 수 있기 때문입니다.
스는 비탈릭 부테린의 AMM 관련 아이디어에서 영감을 받아 Uniswap을 개발했으며, 그 과정에서 무한 발행할 수 있는 ‘LP 토큰’ 개념을 현대의 암호화폐 거래 플랫폼에 도입했습니다. 이러한 혁신은 암호화폐 거래소 방식에 혁명을 일으켜, 수많은 사용자가 더 낮은 거래 비용과 높은 유동성의 이점을 누릴 수 있게 되었고, 이후 다양한 DeFi 프로젝트들도 이 아이디어를 발전시켜 자신들의 플랫폼에 구현하였습니다.
그렇다면 유동성 공급자에게 발행(민팅) 할 LP 토큰 양은 어떻게 계산할 수 있을까요?
유니스왑 v1에선 이더리움 준비금에 비례하여 LP 토큰을 발행합니다. 실제 공식을 살펴봅시다.
amountMinted = (totalAmount * ethDeposited) / ethReserve
amountMinted
: 유동성 공급자 발급받게 될 LP 토큰 양totalAmount
: 현재까지 발행된 LP 토큰의 총량ethDeposited
: 유동성 공급자가 추가로 예치하는 이더리움 양ethReserve
: 풀에 현재 보유하고 있는 이더리움의 준비량
지금까지 배운 내용을 토대로 addLiquidity 함수를 개선해 봅시다.
function addLiquidity(uint tokenAmount) public payable {
if (getReserve() == 0) {
…
uint liquidity = address(this).balance;
_mint(msg.sender, liquidity);
} else {
…
uint liquidity = (totalSupply() * msg.value) / ethReserve;
_mint(msg.sender, liquidity);
}
}
새롭게 추가된 코드 덕분에 이제 유동성 공급자들은 LP 토큰을 지급(민팅)받을 수 있게 되었습니다. 유동성 인출 시 얼마큼의 토큰을 반환받아야 하는지는 LP 토큰 보유량에 따라 달라지므로 유동성 인출에 대한 상황도 대비가 가능해졌습니다.
지금까진 함수 이름(addLiquidity) 유동성을 공급하는 경우 어떤 일이 일어나는지를 알아보았습니다.
참고로 유동성 해제(LP 토큰 반환) 시 얼마나 많은 ETH를 반환받을 수 있는지 계산하는 공식은 다음과 같습니다.
underlyingTokensReturned = userLPtokens / totalAmount * ethReserve
userLPtokens
: 유동성 공급자가 보유중이던 LP 토큰 양totalAmount
: 현재 풀에 있는 LP 토큰 총량ethReserve
: 풀에 예치된 이더리움의 총량
LP 토큰은 예치한 이더리움의 양에 비례하므로 이 방식을 사용하면 유동성 풀에 예치한 토큰 양 정보 없이도 유동성 반환 시 돌려받을 토큰의 양을 계산할 수 있습니다.
스왑 수수료
이제 DEX 사용자가 지불하게 되는 수수료에 대해 생각해 봅시다. 다음과 같은 질문이 떠오를 겁니다.
- 수수료를 ETH로 징수할 것인가 토큰으로 할 것인가
- 보상을 ETH로 지급할 것인가 토큰으로 지급할 것인가
- 수수료는 얼마큼 징수할 것인가
- 유동성 공급자에게 어떻게 수수료를 분배할 것인가
어려워 보이는 문제일 수 있는데, 앞서 다룬 내용을 가지고 충분히 답변할 수 있습니다.
DEX 사용자는 스왑시 ETH, 토큰 쌍을 컨트랙트에 전송하게 됩니다. 이때 사용자에게 수수료를 따로 요청하는 대신, 보내는 토큰에서 일정 금액(수수료 명목)을 차감합니다.
차감한 수수료는 준비금에 추가합니다. 이렇게 하면 시간이 지남에 따라 준비금의 총량이 점차 증가합니다.
수수료는 해당 풀을 구성하는 토큰 단위로 수집합니다. 이렇게 하면 유동성 제공자들은 LP 토큰 비율에 따라 균형 있게 ETH와 토큰을 얻게 됩니다.
유니스왑은 스왑시 0.03%의 수수료를 징수하는데, 쉬운 설명을 위해 여기선 1%를 받는다고 가정해 보겠습니다. 생각보다 간단하게 수수료 관련 코드를 추가할 수 있습니다.
outputAmount = (outputReserve * inputAmount) / (inputReserve + inputAmount)
아래에 outputAmountWithFees = 0.99 * outputAmount
를 추가하면 되죠.
그런데 솔리디티는 부동소수점 연산을 지원하지 않습니다. 따라서 0.99를 곱하는 대신 outputAmountWithFees = (outputAmount * 99) / 100
이런식으로 코드를 작성하면 됩니다.
작성자: 서강대학교 게임교육원 이보라 교수
참고: Learn Web3 DAO, A deep dive into Automated Market Maker Decentralized Exchanges (Uniswap v1)