contrancts 폴더에 interfaces, libraries, test 폴더는 볼 필요 없다. 밑에 3가지 solidity 코드만 보면 되는데 UniswapV2ERC20.sol 파일은 UNI 토큰에 대한 것이다. 그래서 UniswapV2Factory.sol, UniswapV2Pair.sol 2가지 파일만 보면 된다.
두 토큰 주소를 비교해서 A가 크면 B-A pair를 만들고 B가 크면 A-B pair를 만든다.
A-B pair와 B-A pair는 같은 것인데 이것을 토큰 주소의 크기로 비교하여 정의한 것이다.
line 3
token0의 주소가 0인지 검사한다.
주소가 0인 컨트랙트는 없다.
token1은 token0보다 크기 때문에 token1은 검사를 하지 않아도 된다.
line 4
두 토큰 페어에 해당하는 컨트랙트가 이미 있는지 검사한다.
factory가 토큰 페어를 관리하는 자료구조를 담고 있는 변수 이름이 getPair이다.
만약 존재한다면 revert시킨다. 이미 존재하는 토큰 페어를 또 만들면 안 되기 때문이다.
line 5
UniswapV2Pair의 tokenPair 컨트랙트 코드를 바이트코드로 가져온다.
line 6
salt를 하나 만든다.
token0, token1의 abi를 keccak256이라고 하는 hash 함수를 돌려서 만들어낸다.
line 7-9
create2함수를 가지고 pair 컨트랙트를 만든다.
어셈블리어로 선언이 되어 있다.
line 10
token0과 token1을 초기화 시켜준다.
이제부터 이 pair는 token0와 token1을 거래할 수 있는 풀이 된다.
line 11-12
getPair는 token exchange contract의 주소를 담고 있는 자료구조이다.
token0, token1에 해당하는 요소에 pair를 넣어준다.
안정성을 위해 모든 조합에 대해 다 넣어주는 것 같다.
line 13
allPairs에 새로 생성한 pair를 푸시해준다.
line 14
새로 pair를 만들었다는 로그를 남겨준다.
V2에서 새로 생긴 것이 fee 중에 0.05%를 가져오는 것이다. 그것을 위해서 feeTo 라고 하는 address 변수를 넣어놨다. V2 백서에서 봤던 공식에 의해서 이 변수에게 토큰 컨트랙트에서 거래되는 수수료의 일부가 가게 된다.
UniswapV2Pair.sol
실제로 두 토큰을 거래하는 풀이자 거래소.
위의 UniswapV2Factory.sol 에서 생성한 V2Pair를 알아보자.
constructor
constructor() public { factory = msg.sender;}
constructor에 factory 주소를 넣어준다.
msg.sender는 트랜잭션을 보낸 address를 나타낸다.
factory가 v2pair를 생성했기 때문에 msg.sender가 factory 주소를 받아온다.
initialize
// called once by the factory at time of deploymentfunctioninitialize(address_token0,address_token1) external {require(msg.sender == factory,'UniswapV2: FORBIDDEN'); // sufficient check token0 = _token0; token1 = _token1;}
Facotry에서 호출했다.
token0와 token1에 각각의 argument를 대입해준다.
_update
// update reserves and, on the first call per block, price accumulatorsfunction_update(uint balance0,uint balance1,uint112_reserve0,uint112_reserve1) private {require(balance0 <=uint112(-1) && balance1 <=uint112(-1),'UniswapV2: OVERFLOW');uint32 blockTimestamp =uint32(block.timestamp % 2**32);uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desiredif (timeElapsed >0&& _reserve0 !=0&& _reserve1 !=0) {// * never overflows, and + overflow is desired price0CumulativeLast +=uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast +=uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 =uint112(balance0); reserve1 =uint112(balance1); blockTimestampLast = blockTimestamp;emitSync(reserve0, reserve1);}
컨트랙트는 자기가 얼마를 들고 있는지 계속해서 추적한다. 하지만 그 추적이 항상 맞지는 않다.
실제 잔고는 token0와 token1 컨트랙트에 있고 그 잔고에 대한 정보는 이 컨트랙트에 가야 볼 수 있다. 내가 지금 기록하고 있는 토큰의 수량보다 토큰 컨트랙트에 있는 수량이 더 많다면 내가 돈을 받았다는 것을 알 수 있다. (이 토큰 풀에 토큰이 들어왔다는 것을 알 수 있다. - 입금)
그 정보를 보고 업데이트 해주는 것이 _update 함수이다.
line 2 - 8
blockTimestamp, timeElapsed, price0CumulativeLast, price1CumulativeLast 변수들을 모종의 로직으로 바꾸어주고 있다.
이것이 Uniswap을 price oracle로 쓰기 위한 로직이다.
각각의 price들이 유지된 시간의 가중치만큼 더해서 평균을 낸 후 현재 가격을 결정한다.
price0CumulativeLast, price1CumulativeLast가 public으로 선언되어 있기 때문에 Uniswap을 price oracle로 사용하고 싶다면 저 두 변수를 보면 된다.
_mintfee
// if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)function_mintFee(uint112_reserve0,uint112_reserve1) privatereturns (bool feeOn) {address feeTo =IUniswapV2Factory(factory).feeTo(); feeOn = feeTo !=address(0);uint _kLast = kLast; // gas savingsif (feeOn) {if (_kLast !=0) {uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));uint rootKLast = Math.sqrt(_kLast);if (rootK > rootKLast) {uint numerator = totalSupply.mul(rootK.sub(rootKLast));uint denominator = rootK.mul(5).add(rootKLast);uint liquidity = numerator / denominator;if (liquidity >0) _mint(feeTo, liquidity); } } } elseif (_kLast !=0) { kLast =0; }}
프로토콜 fee를 계산하는 함수이다.
아래 함수에서 ϕ를 1/6을 대입한 공식이다.
mint
// this low-level function should be called from a contract which performs important safety checksfunctionmint(address to) externallockreturns (uint liquidity) { (uint112 _reserve0,uint112 _reserve1,) =getReserves(); // gas savingsuint balance0 =IERC20(token0).balanceOf(address(this));uint balance1 =IERC20(token1).balanceOf(address(this));uint amount0 = balance0.sub(_reserve0);uint amount1 = balance1.sub(_reserve1);bool feeOn =_mintFee(_reserve0, _reserve1);uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFeeif (_totalSupply ==0) { liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); }require(liquidity >0,'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');_mint(to, liquidity);_update(balance0, balance1, _reserve0, _reserve1);if (feeOn) kLast =uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-dateemitMint(msg.sender, amount0, amount1);}
LP 토큰을 만들어내는 함수이다.
pair contract에서 기록하고 있는 자산의 양과 해당 token contract에서 기록하고 있는 자산의 양이 다를 수 있다.
만약 토큰 컨트랙트에 있는 내 자산의 양이 더 많으면 mint시켜줘야 한다. 반대로 더 적다면 burn을 시켜줘야 한다.
line 1
내 pair contract에서 기록하고 있는 잔고
line 2 - 3
token0, token1 컨트랙트에서 잔고를 받아옴.
line 4 - 5
받아온 잔고에서 기록하고 있는 잔고를 빼서 내가 받은 양(amount)를 계산한다.
line 7
수수료를 계산한다.
line 8 - 14
totalSupply가 0이라는 것은 처음 이 컨트랙트에 pair를 넣는 사람이라는 것이다. 이때에는 LP share의 최소 단위 즉 1LP 토큰의 천배에 해당하는 돈이 burn된다. address 0 에게 생성하지만 address 0 는 존재하지 않기 때문에 이 돈은 그냥 묶이게 된다.(burn) 이전 백서에서 봤던 공식에 따라 liquidity가 정해진다.
totalSupply가 0이 아니면 인플레이션을 시키게 된다. 이전 백서에서 봤던 공식에 따라 정해진다.
line 16
지금 돈을 넣은 사람(to 주소)에게 liquidity(LP token)을 준다.
line 18 - 19
값들을 업데이트 해준다.
line 20
민트했다고 로그를 남긴다.
burn
// this low-level function should be called from a contract which performs important safety checksfunctionburn(address to) externallockreturns (uint amount0,uint amount1) { (uint112 _reserve0,uint112 _reserve1,) =getReserves(); // gas savingsaddress _token0 = token0; // gas savingsaddress _token1 = token1; // gas savingsuint balance0 =IERC20(_token0).balanceOf(address(this));uint balance1 =IERC20(_token1).balanceOf(address(this));uint liquidity = balanceOf[address(this)];bool feeOn =_mintFee(_reserve0, _reserve1);uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distributionrequire(amount0 >0&& amount1 >0,'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');_burn(address(this), liquidity);_safeTransfer(_token0, to, amount0);_safeTransfer(_token1, to, amount1); balance0 =IERC20(_token0).balanceOf(address(this)); balance1 =IERC20(_token1).balanceOf(address(this));_update(balance0, balance1, _reserve0, _reserve1);if (feeOn) kLast =uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-dateemitBurn(msg.sender, amount0, amount1, to);}
line 1 - 6
mint와 마찬가지로 잔고 정보를 가져온다.
line 10 - 11
이 사람이 지금 태우는 liquidity의 비율에 해당하는 각 토큰의 amount 만큼을 태우는 사람이 받게 된다.
line 14 - 15
LP token을 전송해준다.
line 16 - 20
변경된 값들이 업데이트 된다.
swap
// this low-level function should be called from a contract which performs important safety checksfunctionswap(uint amount0Out,uint amount1Out,address to,bytescalldata data) externallock {require(amount0Out >0|| amount1Out >0,'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); (uint112 _reserve0,uint112 _reserve1,) =getReserves(); // gas savingsrequire(amount0Out < _reserve0 && amount1Out < _reserve1,'UniswapV2: INSUFFICIENT_LIQUIDITY');uint balance0;uint balance1; { // scope for _token{0,1}, avoids stack too deep errorsaddress _token0 = token0;address _token1 = token1;require(to != _token0 && to != _token1,'UniswapV2: INVALID_TO');if (amount0Out >0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokensif (amount1Out >0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokensif (data.length >0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 =IERC20(_token0).balanceOf(address(this)); balance1 =IERC20(_token1).balanceOf(address(this)); }uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) :0;uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) :0;require(amount0In >0|| amount1In >0,'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); { // scope for reserve{0,1}Adjusted, avoids stack too deep errorsuint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));require(balance0Adjusted.mul(balance1Adjusted) >=uint(_reserve0).mul(_reserve1).mul(1000**2),'UniswapV2: K'); }_update(balance0, balance1, _reserve0, _reserve1);emitSwap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);}
두 토큰을 교환하는 함수이다.
보통 교환을 생각하면 지불하는 토큰과 받는 토큰이 있으면 된다고 생각하는데 이 함수는 넣는 돈(in)은 없고 out만 2개가 있다.
flash loan이라고 해서 암호화폐 대출 서비스인 Aave나 Compound에서 토큰을 먼저 빌린 다음에 그 토큰을 swap하고 swap한 토큰을 다른 곳에 집어 넣어 가지고 차액을 얻은 다음 다시 Aave나 Compound에 반납하는 그런 식의 작업을 한 트랜잭션 내에서 atomic하게 수행하는 경우가 있다.
Uniswap에서도 flash loan을 쓸 수 있도록 구현해놓은 것이다.
in은 이 스왑함수를 실행시키기 전에 pair 컨트랙트의 token 주소로 먼저 교환하고자 하는 토큰을 집어넣어야 한다. 그 후에 이 swap 함수를 부른다.
백서상으로 보면 in에 해당하는 argument가 없으니까 돈을 누가 얼마나 보냈는지를 알기 힘들어서 프라이버시를 지킬 수 있다고 한다.
line 1 - 10
값들을 받아오고 검사한다.
line 11 - 12
토큰0, 1을 amount0, 1만큼 보낸다.
line 13
뭔가 데이터가 있다면 UniswapV2Call이라는 함수를 이용해서 콜 해준다.
Uniswap을 실행한 후에 다른 무언가를 실행하게 만들 수가 있다. 그럴 때 저 데이터에 값을 넣어서 이 함수로 부를 수가 있다.
예를 들면 Aave나 Compound에 대출을 상환하는 코드가 될 수 있다.
line 14 - 15
이 사람이 돈을 넣었는지 확인하기 위해 balance를 받아온다.
line 17 - 18
in을 계산한다.
새로운 balance보다 원래 가지고 있던 reserve에서 내가 다른데 보낸 돈(amountOut)을 뺀 것이 작으면 balance를 조정해준다.
그 반대이면 amount가 잘못된 것이다. 보낼 수 있는 양보다 더 크게 된 것이다. 그런 경우 0을 대입한다.
line 19
amountIn이 둘 중 하나라도 0이면 ‘입금액이 부족함’이라고 해서 취소된다.
line 21 - 22
balance를 조정한다.
line 23
0.3% 수수료를 낼 수 있는지 확인한다.
skim
// force balances to match reservesfunctionskim(address to) externallock {address _token0 = token0; // gas savingsaddress _token1 = token1; // gas savings_safeTransfer(_token0, to,IERC20(_token0).balanceOf(address(this)).sub(reserve0));_safeTransfer(_token1, to,IERC20(_token1).balanceOf(address(this)).sub(reserve1));}
만약 입금된 토큰이 이 pair 컨트랙트에서 담을 수 있는 토큰의 양을 넘었을 때 남는 여유분을 회수할 수 있게 만들어주는 함수이다.
회수를 하지 않으면 K값이 max로 고정되어 있으니까 로직이 망가진다.
sync
// force reserves to match balancesfunctionsync() externallock {_update(IERC20(token0).balanceOf(address(this)),IERC20(token1).balanceOf(address(this)), reserve0, reserve1);}
skim과 반대로 돈이 너무 적은데 토큰간에 imbalance가 생겼을 때 그것을 다시 맞춰주기 위해서 부르는 함수이다.
Routing
Uniswap은 pair로 토큰 풀을 만들고 만약에 교환하고 싶은 토큰이 있으면 그 풀이 있어야 된다. 그래서 Uniswap에는 그 풀의 양이 굉장히 많을 것이다. 그 모든 풀을 만드는 것은 굉장히 비효율적이다.
페어가 만약 존재하지 않으면 직접적인 pair가 없다고 하더라도 그 pair를 만들어 낼 수 있는 루트를 찾는다. 그것을 routing이라고 한다.
예를 들어 SUSHI와 YFI 토큰을 교환하고 싶은데 SUSHI-YFI pair 컨트랙트가 없다면 다른것을 사용한다. SUSHI-WETH pair와 WETH-YFI pair를 이용하여 atomic하게 SUSHI → WETH → YFI 로 교환한다.