상세 컨텐츠

본문 제목

Uniswap V1 백서 및 코드

Blockchain/DeFi

by Yongari 2023. 3. 9. 22:02

본문

 

 

 

유니스왑(Uniswap)은 이더리움(Ethereum) 기반의 자동화된 토큰 교환 프로토콜입니다. 이는 사용의 용이성, 가스 효율성, 검열 저항성, 그리고 제로 임대료 추출을 중심으로 디자인되었습니다. 유니스왑은 거래자에게 유용하며, 보장된 체인 상 유동성을 요구하는 다른 스마트 계약의 구성 요소로서 특히 잘 작동합니다.

 

대부분의 거래소는 주문서를 유지하고 구매자와 판매자 간의 매칭을 용이하게 합니다. 유니스왑 스마트 계약은 다양한 토큰의 유동성 예비를 보유하며, 거래는 이러한 예비에 직접적으로 실행됩니다. 가격은 일정한 곱(x*y=k)의 시장 메이커 메커니즘을 사용하여 자동으로 설정되며, 전반적인 예비를 상대적으로 균형 상태로 유지합니다. 예비는 거래 수수료의 비례적인 점유율에 따라 시스템에 토큰을 제공하는 일련의 유동성 제공자 네트워크에서 풀링됩니다.

 

유니스왑(Uniswap)의 중요한 기능 중 하나는 ERC20 토큰마다 별도의 거래소 계약을 배포하는 팩토리/레지스트리(contract) 계약의 활용입니다. 이러한 거래소 계약은 ETH와 해당 ERC20의 예비를 각각 보유하고 있습니다. 이는 상대적인 공급을 기반으로 두 가지 사이의 거래를 허용합니다. 거래소 계약은 레지스트리를 통해 연결되어 ETH를 중개자로 사용하여 모든 토큰 간 직접적인 ERC20 to ERC20 거래가 가능합니다.

 

이 문서는 유니스왑(Uniswap)의 핵심 메커니즘과 기술적인 세부 정보를 개요합니다. 일부 코드는 가독성을 위해 단순화되었으며, 오버플로 확인 및 구매 최소액 등의 안전 기능은 생략되었습니다. 전체 소스 코드는 GitHub에서 확인할 수 있습니다.

 

Protocol Website:
uniswap.io

 

Home | Uniswap Protocol

Swap, earn, and build on the leading decentralized crypto trading protocol.

uniswap.org

 

Documentation:
docs.uniswap.io

 

Home | Uniswap Protocol

Swap, earn, and build on the leading decentralized crypto trading protocol.

uniswap.org

 

Code:
github.com/Uniswap

 

Uniswap Labs

Web3 infrastructure and products. Uniswap Labs has 88 repositories available. Follow their code on GitHub.

github.com

 

 

Formalized Model:
https://github.com/runtimeverification/verified-smart-contracts/blob/uniswap/uniswap/x-y-k.pdf

 

GitHub - runtimeverification/verified-smart-contracts: Smart contracts which are formally verified

Smart contracts which are formally verified. Contribute to runtimeverification/verified-smart-contracts development by creating an account on GitHub.

github.com

 

 

가스 벤치마크(Gas Benchmarks)

유니스왑(Uniswap)은 최소주의적인(미니멀리즘) 디자인으로 인해 매우 가스 효율적입니다. ETH에서 ERC20으로의 거래에서 Bancor보다 거의 10배 적은 가스를 사용합니다. 0x보다 ERC20 to ERC20 거래를 더 효율적으로 수행할 수 있으며, EtherDelta와 IDEX와 같은 체인상 주문서 거래와 비교할 때 상당한 가스 절감 효과가 있습니다

 

Exchange Uniswap EtherDelta Banco Radar
Relay(0x)
IDEX Airswap
ETH to ERC20 46,000 108,000 440,000 113,000* 143,000 90,000
ERC20 to ETH 60,000 93,000 403,000 113,000* 143,000 120,000*
ERC20 to ERC20 88,000 no 538,000 113,000 no no
*wrapped ETH            

ERC20 토큰을 직접 전송하는 비용은 36,000 가스다. -  유니스왑에서 ETH에서 ERC20으로의 거래 기준으로 다른 dex보다 20% 적은 가스를 사용한다.  (해석이 틀릴수도 있습니다. )

 

uniswap_factory.vy는 Uniswap 거래소의 팩토리 및 레지스트리 역할을 하는 스마트 계약입니다. 공개 함수인 createExchange()는 이미 거래소가 없는 모든 ERC20에 대해 Ethereum 사용자가 거래소 계약을 배포할 수 있도록 합니다.


[Python]

exchangeTemplate: public(address)
token_to_exchange: address[address]
exchange_to_token: address[address]
    
@public
def __init__(template: address):
    self.exchangeTemplate = template

#거래소를 배포하는 함수로 추정
@public
def createExchange(token: address) -> address:
    assert self.token_to_exchange[token] == ZERO_ADDRESS
    new_exchange: address = create_with_code_of(self.exchangeTemplate)
    self.token_to_exchange[token] = new_exchange
    self.exchange_to_token[new_exchange] = token
    return new_exchange

 

모든 토큰과 그에 대한 거래소의 기록이 팩토리에 저장됩니다. 토큰 또는 거래소 주소 중 하나로 getExchange() 및 getToken() 함수를 사용하여 다른 항목을 찾을 수 있습니다.

@public
@constant
def getExchange(token: address) -> address:
    return self.token_to_exchange[token]

@public
@constant
def getToken(exchange: address) -> address:
    return self.exchange_to_token[exchange]

팩토리는 거래소 계약을 시작할 때 토큰에 대해 강제하거나 검사하는 것이 아니라, 단일 토큰당 거래소 한 개의 제한을 강제합니다. 사용자 및 프론트엔드는 신뢰할 수 있는 토큰과 관련된 거래소와만 상호 작용해야 합니다.

 

 

ETH ⇄ ERC20 Trades

각 거래소 계약 (uniswap_exchange.vy)은 하나의 ERC20 토큰과 연관되며, ETH와 해당 토큰의 유동성 풀을 보유합니다. ETH와 ERC20 간의 환율은 계약 내에서 유동성 풀의 상대적 크기에 따라 결정됩니다. 이는 eth_pool * token_pool = 고정값 관계를 유지함으로써 수행됩니다. 이 고정값은 거래 중에 일정하게 유지되며, 시장에서 유동성이 추가되거나 제거될 때에만 변경됩니다.

ETH를 ERC20 토큰으로 변환하는 함수인 ethToTokenSwap()의 단순화된 버전은 아래와 같습니다:

 

eth_pool: uint256         
token_pool: uint256       
token: address(ERC20) 

@public
@payable
def ethToTokenSwap():
    fee: uint256 = msg.value / 500 
    invariant: uint256 = self.eth_pool * self.token_pool
    new_eth_pool: uint256 = self.eth_pool + msg.value
    new_token_pool: uint256 = invariant / (new_eth_pool - fee)
    tokens_out: uint256 = self.token_pool - new_token_pool
    self.eth_pool = new_eth_pool
    self.token_pool = new_token_pool
    self.token.transfer(msg.sender, tokens_out)

 

ETH가 eth_pool에 전송될 때 eth_pool이 증가합니다. eth_pool * token_pool = 고정값 관계를 유지하기 위해 token pool은 비례하는 양만큼 감소됩니다. token_pool이 감소하는 양은 구매한 토큰의 양과 같습니다. 이러한 비율 변화는 ETH를 ERC20으로 교환하는 환율을 변경시키며, 반대 방향의 거래를 유도합니다.

토큰을 ETH로 교환하는 것은 tokenToEthSwap() 함수를 사용하여 수행됩니다:

 

@public
def tokenToEthSwap(tokens_in: uint256):
    fee: uint256 = tokens_in / 500
    invariant: uint256 = self.eth_pool * self.token_pool
    new_token_pool: uint256 = self.token_pool + tokens_in
    new_eth_pool: uint256 = self.invariant / (new_token_pool - fee)
    eth_out: uint256 = self.eth_pool - new_eth_pool
    self.eth_pool = new_eth_pool
    self.token_pool = new_token_pool
    self.token.transferFrom(msg.sender, self, tokens_out)
    send(msg.sender, eth_out)

이는 token_pool을 증가시키고 eth_pool을 감소시켜 가격을 반대 방향으로 변경시킵니다. ETH에서 OMG으로 구매하는 예시를 아래에 보여드립니다.




Example: ETH → OMG

10 ETH와 500 OMG(ERC20)가 유동성 공급자에 의해 스마트 계약에 입금됩니다. ETH_pool * OMG_pool = 불변으로 자동으로 설정됩니다.

ETH_pool = 10

OMG_pool = 500

invariant = 10 * 500 = 5000 (ETH * OMG)

OMG 구매자는 계약에 1 ETH를 보냅니다. 유동성 공급자를 위해 0.25%의 수수료가 공제되고 나머지 0.9975 ETH가 ETH_pool에 추가됩니다. 그 다음, 불변은 유동성 풀에 있는 ETH의 새로운 금액으로 나누어 OMG_pool의 새 크기를 결정합니다. 남은 OMG는 구매자에게 보내집니다.

 

Buyer sends: 1 ETH 
Fee = 1 ETH / 500 = 0.0025 ETH (수수료)
ETH_pool = 10 + 1 - 0.0025 = 10.9975 (ETH Pool에 1ETH 받고 - 수수료한 최종 ETH_pool)
OMG_pool = 5000/10.9975 = 454.65 (오미세고 풀은 불변량(5000)을  / 최종 ETH_pool로 나눈 개수 ) 
Buyer receieves: 500 - 454.65 = 45.35 OMG 

이제 수수료가 유동성 풀에 추가되어 시장에서 유동성을 제거할 때 수집되는 유동성 공급자에게 지불되는 보상으로 작용합니다. 가격 계산 후에 수수료가 추가되기 때문에 불변은 매 거래마다 약간 증가하여 유동성 공급자에게 수익이 발생합니다. 사실 불변이 실제로 나타내는 것은 이전 거래 종료 시점에서의 ETH_pool * OMG_pool입니다.

 

ETH_pool = 10.9975 + 0.0025 = 11
OMG_pool = 454.65
new invariant = 11 * 454.65 = 5,001.15

이 경우 구매자는 45.35 OMG/ETH의 환율을 받았습니다. 그러나 가격이 변동되었습니다. 같은 방향으로 거래를 하는 다른 구매자는 약간 나쁜 OMG/ETH 환율을 받게 됩니다. 그러나 반대 방향으로 거래하는 구매자는 약간 더 나은 ETH/OMG 환율을 받게 됩니다.

1 ETH in
44.5 OMG out
Rate = 45.35 OMG/ETH

1ETH가 들어오고 44.5 OMG가 나갔다.
환율 = 45.35 OMG/ 1 ETH 


 

 

 

대규모 구매는 유동성 풀의 총 규모에 비해 크기 때문에 가격 슬리피지가 발생할 수 있습니다. 활발한 시장에서는 아비트리지(Arbitrage)가 가격이 다른 거래소와 너무 멀리 떨어지지 않도록 보장합니다.

 

 

ERC20 ⇄ ERC20 Trades

ETH는 모든 ERC20 토큰의 공통 페어로 사용되기 때문에 직접적인 ERC20에서 ERC20으로 스왑하는 중개자로 사용될 수 있습니다. 예를 들어, 단일 트랜잭션 내에서 한 거래소에서 OMG를 ETH로 변환한 다음, 또 다른 거래소에서 ETH를 KNC로 변환하는 것이 가능합니다.

 

예를 들어 OMG에서 KNC로 변환하려면, 구매자는 OMG 거래소 계약에서 tokenToTokenSwap() 함수를 호출합니다.

 

contract Factory():
    def getExchange(token_addr: address) -> address: constant

contract Exchange():
    def ethToTokenTransfer(recipent: address) -> bool: modifying
    
factory: Factory
    
@public
def tokenToTokenSwap(token_addr: address, tokens_sold: uint256):
    exchange: address = self.factory.getExchange(token_addr)
    fee: uint256 = tokens_sold / 500
    invariant: uint256 = self.eth_pool * self.token_pool
    new_token_pool: uint256 = self.token_pool + tokens_sold
    new_eth_pool: uint256 = invariant / (new_token_pool - fee)
    eth_out: uint256 = self.eth_pool - new_eth_pool
    self.eth_pool = new_eth_pool
    self.token_pool = new_token_pool
    Exchange(exchange).ethToTokenTransfer(msg.sender, value=eth_out)



여기서 token_addr은 KNC 토큰의 주소이며, tokens_sold는 판매되는 OMG의 양입니다. 이 함수는 먼저 공장에서 KNC 거래소 주소를 가져와 검색합니다. 그다음, 거래소는 입력된 OMG를 ETH로 변환합니다. 그러나 구매한 ETH를 구매자에게 반환하는 대신, 이 함수는 대신 KNC 거래소의 payable 함수인 ethToTokenTransfer()를 호출합니다.

@public
@payable
def ethToTokenTransfer(recipent: address):
    fee: uint256 = msg.value / 500
    invariant: uint256 = self.eth_pool * self.token_pool
    new_eth_pool: uint256 = self.eth_pool + msg.value
    new_token_pool: uint256 = invariant / (new_eth_pool - fee)
    tokens_out: uint256 = self.token_pool - new_token_pool
    self.eth_pool = new_eth_pool
    self.token_pool = new_token_pool
    self.invariant = new_eth_pool * new_token_pool
    self.token.transfer(recipent, tokens_out)

 

ethToTokenTransfer() 함수는 ETH와 구매자 주소를 받아들이고, 호출이 등록된 교환소에서 이루어졌는지 확인한 다음 ETH를 KNC로 변환하고 KNC를 원래 구매자에게 전달합니다. ethToTokenTransfer() 함수는 ethToTokenSwap() 함수와 동일하게 작동하지만 추가 입력 매개 변수 recipient: address가 있습니다. 이것은 msg.sender(이 경우 OMG 거래소) 대신 원래 구매자에게 구매된 토큰을 전달하는 데 사용됩니다.

 

Swaps vs Transfers(교환 vs 이체)

ethToTokenSwap(), tokenToEthSwap() 및 tokenToTokenSwap() 함수는 구매자의 주소에 구매된 토큰을 반환합니다.

ethToTokenTransfer(), tokenToEthTransfer() 및 tokenToTokenTransfer() 함수를 사용하면 구매자는 거래를하고 즉시 구매된 토큰을 수령자 주소로 전송할 수 있습니다.

 

 

 

Providing Liquidity(유동성 공급)


유동성 추가


유동성을 추가하려면, 해당 ERC20 토큰의 관련 거래소 계약에 ETH 및 ERC20 토큰과 동일한 가치를 예치해야합니다.

풀에 처음 유동성을 제공하는 공급자는 ETH와 ERC20 토큰의 동등한 가치를 예치하여 초기 환율을 설정합니다. 이 비율이 잘못되면, 자본유치 거래자들은 초기 유동성 공급자의 비용으로 가격을 균형으로 이끌어냅니다.

이후 모든 유동성 공급자는 예치 시점의 환율을 사용하여 ETH 및 ERC20를 예치합니다. 환율이 좋지 않으면 가격을 교정할 수 있는 이윤을 얻는 자본유치 기회가 있습니다.

 

 


유동성 토큰

 

유동성 토큰은 각 유동성 공급자가 공급한 총 예약액의 상대 비율을 추적하기 위해 발행됩니다. 이들은 매우 분할 가능하며, 언제든지 소각하여 시장의 유동성 비율에 비례하는 비율을 제공자에게 반환할 수 있습니다.

유동성 공급자는 예약금을 입금하고 새로운 유동성 토큰을 발행하기 위해 addLiquidity() 함수를 호출합니다.

 
@public
@payable
def addLiquidity():
    token_amount: uint256 = msg.value * token_pool / eth_pool 
    liquidity_minted: uint256 = msg.value * total_liquidity / eth_pool
        
        
    eth_added: uint256 = msg.value
    shares_minted: uint256 = (eth_added * self.total_shares) / self.eth_pool
    tokens_added: uint256 = (shares_minted * self.token_pool) / self.total_shares)
    self.shares[msg.sender] = self.shares[msg.sender] + shares_minted
    self.total_shares = self.total_shares + shares_minted
    self.eth_pool = self.eth_pool + eth_added
    self.token_pool = self.token_pool + tokens_added
    self.token.transferFrom(msg.sender, self, tokens_added)

 

발행된 유동성 토큰의 수는 함수에 전송된 ETH의 양에 의해 결정됩니다. 그것은 방정식을 사용하여 계산할 수 있습니다 :

 

 

ETH를 적립금에 예치하려면 ERC20 토큰에 상응하는 가치도 예치해야 한다. 이 값은 다음 방정식으로 계산됩니다:

Removing Liquidity(유동성 제거)

 

공급자는 언제든지 유동성 토큰을 소각해서(burn) ETH 및 ERC20 토큰의 비례적 몫을 풀에서 인출할 수 있습니다. 

 

 

ETH와 ERC20 토큰은 현재의 환율(보유 비율)에 따라 인출됩니다. 즉, 원래 투자 비율과는 다를 수 있습니다. 이는 시장 변동과 아비트라지(차익거래)로 인해 일부 가치가 손실될 수 있다는 것을 의미합니다.

거래 중에 취급 수수료는 새로운 유동성 토큰을 만들지 않고 총 유동성 풀에 추가됩니다. 이로 인해 ethWithdrawn 및 tokensWithdrawn은 유동성이 처음 추가된 이후 수집된 모든 수수료의 비례적인 점유율을 포함합니다.




유동성 토큰

유니스왑 유동성 토큰은 ETH-ERC20 페어에 대한 유동성 공급자의 기여를 나타냅니다. 유동성 토큰 자체가 ERC20 토큰이며 EIP-20의 완전한 구현을 포함합니다.

이를 통해 유동성 공급자는 유동성 토큰을 판매하거나 풀에서 유동성을 제거하지 않고 계정 간에 이전할 수 있습니다. 유동성 토큰은 단일 ETH⇄ERC20 교환에만 해당됩니다. 이 프로젝트에 대한 단일 통합 ERC20 토큰은 없습니다.

 

 

Fee Structure

  • ETH to ERC20 trades
    • 0.3% fee paid in ETH (ETH에서 ERC20으로 거래시 ETH 0.3% 거래 수수료 지급)
  • ERC20 to ETH trades
    • 0.3% fee paid in ERC20 tokens  (ERC20에서 ETH으로 거래시 ETH 0.3% 거래 수수료 지급)
  • ERC20 to ERC20 trades
    • 0.3% fee paid in ERC20 tokens for ERC20 to ETH swap on input exchange
      (입력 거래소에서 ERC20에서 ETH로 스왑 시 0.3%의 수수료가 ERC20 토큰으로 지불됩니다.
    • 0.3% fee paid in ETH for ETH to ERC20 swap on output exchange
      (출력 거래소에서 ETH에서 ERC20으로 스왑 시 0.3%의 수수료가 ETH로 지불됩니다.)
    • Effectively 0.5991% fee on input ERC20
      (입력 ERC20에 대한 수수료는 실제로 0.5991%가 됩니다.)

1. ERC20 > ETH 거래시에는 ERC20 토큰의 0.3% 거래 수수료 지급
2. ETH > ERC20 거래시에는 ETH 토큰의 0.3% 거래 수수료 지급
3. ERC20 > ETH > ERC20 으로 들어가는 총 수수료는 0.5991%로 이해했다. 

ETH와 ERC20 토큰 간 스왑 시 0.3% 수수료가 있습니다. 이 수수료는 유동성 공급자가 유동성 예약에 기여한 비율에 따라 분배됩니다. ERC20에서 ERC20으로 거래할 경우 ERC20에서 ETH로 스왑과 ETH에서 ERC20으로 스왑이 모두 포함되므로 수수료가 두 거래 모두에서 부과됩니다. 플랫폼 수수료는 없습니다.

스왑 수수료는 즉시 유동성 예약에 입금됩니다. 추가적인 쉐어 토큰 없이 총 예약금이 증가하므로, 이는 모든 쉐어 토큰의 가치를 동일하게 증가시키는 기능을 합니다. 이것은 쉐어를 소각하여 수집할 수 있는 유동성 공급자에 대한 지급으로 작용합니다.

수수료가 유동성 풀에 추가되기 때문에, 무결성은 매 거래 종료 후 증가합니다. 하나의 거래 내에서, 무결성은 이전 거래 종료 시점에서 eth_pool * token_pool을 나타냅니다.

 

 

 

Custom Pools

ERC20 to Exchange

추가 기능인 tokenToExchangeSwap() 및 tokenToExchangeTransfer()는 유니스왑(Uniswap)의 유연성을 높입니다. 이러한 기능은 ERC20 토큰을 ETH로 변환하고 사용자 입력 주소에서 ethToTokenTransfer()를 시도합니다. 이를 통해 ERC20 토큰을 사용한 커스텀 유니스왑 거래를 같은 공장에서 나오지 않은 거래소에 대해 수행할 수 있습니다. 다만 해당 거래소가 적절한 인터페이스를 구현하고 있어야 합니다. 커스텀 거래소는 서로 다른 곡선, 매니저, 개인 유동성 풀, FOMO 기반 펀지 스키마 또는 상상할 수 있는 모든 것들을 가질 수 있습니다.

 

 

Opt-in Upgrades

검열 저항성 및 분산화된 스마트 계약의 업그레이드는 어렵습니다. 유니스왑(Uniswap) 1.0이 완벽하길 바랍니다. 하지만 아마 완벽하지 않을 것입니다. 개선된 유니스왑 2.0 디자인이 만들어진다면 새로운 팩토리 계약을 배포할 수 있습니다. 유동성 제공자들은 새로운 시스템으로 이동하거나 이전 시스템에 머무를 수 있습니다.

tokenToExchange 함수들은 서로 다른 공장에서 출시된 거래소와 거래할 수 있도록 합니다. 이는 하위 호환성(backwards compatibility)에 사용될 수 있습니다. 버전 내에서는 tokenToToken 및 tokenToExchange 함수를 모두 사용하여 ERC20 to ERC20 거래가 가능하지만, 버전 간에는 tokenToExchange 함수만 작동합니다. 모든 업그레이드는 옵트인(opt-in) 및 하위 호환성을 가지고 이루어집니다.

 

Frontrunning (선행매매)

Uniswap의 거래는 다른 사용자들이 먼저 거래(선행매매)하는 것을 막지 못합니다. 이는 어느 정도 제한되어 있으며, 사용자가 설정한 최소/최대값과 거래 마감 시간에 따라 제한됩니다. 이는 사용자가 설정한 최소/최대 값과 트랜잭션 마감일에 의해 제한됩니다.

즉, Uniswap에서 거래를 시도할 때, 다른 사용자들이 먼저 해당 거래를 처리할 수 있으며 이는 거래 가격을 올리거나 내리는 등의 영향을 미칠 수 있습니다. 하지만 사용자가 거래 시간과 최소/최대값을 제한해두면 이러한 선행 매매에 대한 영향을 어느 정도 제한할 수 있습니다.

선행매매란 투자매매업자나 투자중개업자가 금융투자상품의 가격에 중대한 영향을 미칠 수 있는 매수 또는 매도 주문을 받거나 받게 될 가능성이 큰 경우 고객의 주문을 체결하기 전에 자기의 계산으로 매수 또는 매도하거나 제3자에게 매수 또는 매도를 권유하는 행위를 말합니다.

경제
주식 시장에서 거래의 흐름을 미리 알고 주식을 팔거나 사들여 이익을 취하는 일.

 

 

 

 

장점:

1. 탈중앙화

2. 검열 저항성

3. 무허가 

4. eth pool과 erc20 pool의 비율을 일정량 유지하는 것을 통한 시장 공급을 조절하는 기능 

 

 

단점:

1. 슬리피지 : 매수/매도호가 스프레드로 인해 사용자가 원하는 가격 및 실제 체결 가격 간의 괴리 
2. ETH와 ERC20 토큰의 교환만 지원하고 ERC20과 ERC20을 거래할 때는 ERC20 > ETH > ERC20 으로 교환해야해서 수수료가 많이 발생하며 거래 처리가 복잡한 단점이 있다. 

 

 

참고:
https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig#Introduction

 

🦄 Uniswap Whitepaper - HackMD

   owned this note    owned this note       Published Linked with GitHub # 🦄 Uniswap Whitepaper ⚠️⚠️⚠️ **WORK IN PROGRESS** ⚠️⚠️⚠️ > [color=#F578FF] [TOC] # Introduction Uniswap is a protocol for automated token excha

hackmd.io

 

관련글 더보기