> For the complete documentation index, see [llms.txt](https://xogk39.gitbook.io/htaeha/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xogk39.gitbook.io/htaeha/tv/undefined/v2-1.md).

# V2 코드 분석

#### uniswap-v2-core

[GitHub - Uniswap/v2-core: 🎛 Core smart contracts of Uniswap V2](https://github.com/Uniswap/v2-core)

contrancts 폴더에 interfaces, libraries, test 폴더는 볼 필요 없다. 밑에 3가지 solidity 코드만 보면 되는데 UniswapV2ERC20.sol 파일은 UNI 토큰에 대한 것이다. 그래서 UniswapV2Factory.sol, UniswapV2Pair.sol 2가지 파일만 보면 된다.

## UniswapV2Factory.sol

새로운 pair를 거래하는 컨트랙트를 생성해내고 관리하는 게이트웨이.

### createPair

```solidity
function createPair(address tokenA, address tokenB) external returns (address pair) {
    require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
    require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
    bytes memory bytecode = type(UniswapV2Pair).creationCode;
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    assembly {
        pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
    }
    IUniswapV2Pair(pair).initialize(token0, token1);
    getPair[token0][token1] = pair;
    getPair[token1][token0] = pair; // populate mapping in the reverse direction
    allPairs.push(pair);
    emit PairCreated(token0, token1, pair, allPairs.length);
}
```

새로운 pair 컨트랙트를 만들어내는 함수.

tokenA와 tokenB의 주소를 argument로 받고 있다.

line 1

* 두 토큰의 주소가 같으면 안 된다.

line 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

```solidity
constructor() public {
    factory = msg.sender;
}
```

constructor에 factory 주소를 넣어준다.

msg.sender는 트랜잭션을 보낸 address를 나타낸다.

factory가 v2pair를 생성했기 때문에 msg.sender가 factory 주소를 받아온다.

### initialize

```solidity
// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
    require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
    token0 = _token0;
    token1 = _token1;
}
```

Facotry에서 호출했다.

token0와 token1에 각각의 argument를 대입해준다.

### \_update

```solidity
// update reserves and, on the first call per block, price accumulators
function _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 desired
    if (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;
    emit Sync(reserve0, reserve1);
}
```

컨트랙트는 자기가 얼마를 들고 있는지 계속해서 추적한다. 하지만 그 추적이 항상 맞지는 않다.

실제 잔고는 token0와 token1 컨트랙트에 있고 그 잔고에 대한 정보는 이 컨트랙트에 가야 볼 수 있다. 내가 지금 기록하고 있는 토큰의 수량보다 토큰 컨트랙트에 있는 수량이 더 많다면 내가 돈을 받았다는 것을 알 수 있다. (이 토큰 풀에 토큰이 들어왔다는 것을 알 수 있다. - 입금)

그 정보를 보고 업데이트 해주는 것이 \_update 함수이다.

line 2 - 8

* blockTimestamp, timeElapsed, price0CumulativeLast, price1CumulativeLast 변수들을 모종의 로직으로 바꾸어주고 있다.
* 이것이 Uniswap을 price oracle로 쓰기 위한 로직이다.
* 각각의 price들이 유지된 시간의 가중치만큼 더해서 평균을 낸 후 현재 가격을 결정한다.
* price0CumulativeLast, price1CumulativeLast가 public으로 선언되어 있기 때문에 Uniswap을 price oracle로 사용하고 싶다면 저 두 변수를 보면 된다.

### \_mintfee

```solidity
// if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
    address feeTo = IUniswapV2Factory(factory).feeTo();
    feeOn = feeTo != address(0);
    uint _kLast = kLast; // gas savings
    if (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);
            }
        }
    } else if (_kLast != 0) {
        kLast = 0;
    }
}
```

프로토콜 fee를 계산하는 함수이다.

아래 함수에서 ϕ를 1/6을 대입한 공식이다.

![](/files/ZIdx9EpnAUhDeTbVojNQ)

### mint

```solidity
// this low-level function should be called from a contract which performs important safety checks
function mint(address to) external lock returns (uint liquidity) {
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    uint 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 _mintFee
    if (_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-date
    emit Mint(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

```solidity
// this low-level function should be called from a contract which performs important safety checks
function burn(address to) external lock returns (uint amount0, uint amount1) {
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    address _token0 = token0;                                // gas savings
    address _token1 = token1;                                // gas savings
    uint 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 distribution
    require(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-date
    emit Burn(msg.sender, amount0, amount1, to);
}
```

line 1 - 6

* mint와 마찬가지로 잔고 정보를 가져온다.

line 10 - 11

* 이 사람이 지금 태우는 liquidity의 비율에 해당하는 각 토큰의 amount 만큼을 태우는 사람이 받게 된다.

line 14 - 15

* LP token을 전송해준다.

line 16 - 20

* 변경된 값들이 업데이트 된다.

### swap

```solidity
// this low-level function should be called from a contract which performs important safety checks
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
    require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
    (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
    require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

    uint balance0;
    uint balance1;
    { // scope for _token{0,1}, avoids stack too deep errors
    address _token0 = token0;
    address _token1 = token1;
    require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
    if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
    if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
    if (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 errors
    uint 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);
    emit Swap(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

```solidity
// force balances to match reserves
function skim(address to) external lock {
    address _token0 = token0; // gas savings
    address _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

```solidity
// force reserves to match balances
function sync() external lock {
    _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 로 교환한다.

수수료는 조금 더 내겠지만 빠르게 교환할 수 있다.

![](/files/vK8LbeBIur6eRd1msBZc)

## Reference

<https://www.youtube.com/watch?v=JfLB3DRwz0I&t=2s>

[GitHub - Uniswap/v2-core: 🎛 Core smart contracts of Uniswap V2](https://github.com/Uniswap/v2-core)

[탈중앙 금융에서 플래시 론이란 무엇인가요? | Binance Academy](https://academy.binance.com/ko/articles/what-are-flash-loans-in-defi)

[플래시 론 공격 (Flash Loan Attack)원리 및 방법 분석](https://medium.com/iotrustlab/%ED%94%8C%EB%9E%98%EC%8B%9C-%EB%A1%A0-%EA%B3%B5%EA%B2%A9-flash-loan-attack-%EC%9B%90%EB%A6%AC-%EB%B0%8F-%EB%B0%A9%EB%B2%95-%EB%B6%84%EC%84%9D-94e01fe1a9c8)
