상세 컨텐츠

본문 제목

UniswapV2-core - UniswapV2Pair.sol 코드 분석

Blockchain/DeFi

by Yongari 2023. 3. 26. 21:45

본문

UniswapV2Pair.sol 개요

이 컨트랙트는 유니스왑에서 토큰을 교환하는 실제 Pool을 교환한다. 이것은 유니스왑의 핵심기능이다. 

 

 

UniswapV2Pair.sol 코드의 핵심함수들

함수명 설명
initialize
이 함수는 UniswapV2Pair 컨트랙트를 초기화합니다. 두 개의 토큰 주소를 받아들여 페어의 초기 잔액을 설정하고, 각 토큰의 정보를 저장합니다. 이 함수는 오직 한 번만 호출될 수 있으며, 오직 팩토리 컨트랙트에서만 호출됩니다.
_update
이 함수는 토큰을 입금하거나 출금할 때마다 호출됩니다.
_mintFee
유니스왑 2.0에서 트레이더는 시장을 이용하기 위해 0.30%의 수수료를 지불합니다.
이 수수료의 대부분(거래의 0.25%)은 항상 유동성 공급자에게 전달됩니다.
나머지 0.05%는 유동성 공급자에게 지급되거나 유니스왑이 프로토콜 수수료로 지정한 주소로 지급되며, 이는 유니스왑의 개발 노력에 대한 대가로 지급됩니다.
mint
이 함수는 토큰을 Pair에 추가하는 데 사용됩니다. 특정 토큰과 특정 비율을 입력받아 토큰을 Pair 컨트랙트에 추가하여 Pair 잔액을 업데이트합니다.
burn 이 함수는 Pair에서 토큰을 제거하는 데 사용됩니다. 특정 토큰과 특정 비율을 가져와 Pair 컨트랙트에서 토큰을 제거하여 Pair 잔액을 업데이트합니다.
swap 이 기능은 Pair 콘트랙트 내에서 토큰을 교환하는 데 사용됩니다. 두 개의 토큰과 환율을 입력으로 받아 교환을 수행하여 토큰과 Pair 콘트랙트의 잔액을 업데이트합니다.
skim
이 기능은 컨트랙트에서 생성된 수수료를 징수하는 데 사용됩니다. Pair의 잔액과 수수료 잔액을 업데이트합니다.
sync 이 함수는 외부 시스템에서 Pair 상태를 업데이트하는 데 사용됩니다. Pair의 잔액을 외부 시스템과 동기화하고 수수료를 징수합니다.

 

 

 

UniswapV2Pair.sol 코드 분석

pragma solidity =0.5.16;
//이 콘트랙트는 토큰을 교환하는 실제 풀을 구현합니다. 유니스왑의 핵심 기능입니다.

/*이는 컨트랙트가 구현하는 인터페이스(IUniswapV2Pair 및 UniswapV2ERC20)이거나
 이를 구현하는 컨트랙트를 호출하기 때문에 컨트랙트가 알아야 하는 모든 인터페이스입니다. 
 */
import './interfaces/IUniswapV2Pair.sol';
import './UniswapV2ERC20.sol';
import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';

// 이 콘트랙트는 유동성 토큰에 ERC-20 기능을 제공하는 유니스왑V2ERC20을 계승합니다. 
contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
    //SafeMath 라이브러리는 오버플로우와 언더플로우를 방지하는 데 사용됩니다. 
    //그렇지 않으면 -1이어야 하는 값이 2^256-1이 되는 상황이 발생할 수 있기 때문에 이 점이 중요합니다. 
    using SafeMath  for uint;
    // 풀 컨트랙트에서 많은 계산에는 분수가 필요합니다. 그러나 분수는 EVM에서 지원되지 않습니다. 
    // 유니스왑이 찾은 해결책은 정수 부분은 112비트, 분수는 112비트로 구성된 224비트 값을 사용하는 것입니다. 
    // 따라서 1.0은 2^112, 1.5는 2^112 + 2^111 등으로 표현됩니다.
    using UQ112x112 for uint224;


    // 0으로 나뉘는 경우를 피하기 위해 항상 존재하는(그러나 0 계정이 소유하는) 최소 유동성 토큰 수가 있습니다.
    //  이 숫자는 최소 유동성 토큰 수인 1000입니다. 
    uint public constant MINIMUM_LIQUIDITY = 10**3;

    //ERC-20 전송 함수를 위한 ABI 선택기입니다. 두 토큰 계정에 있는 ERC-20 토큰을 전송하는 데 사용됩니다. 
    bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

    //이것이 이 풀을 생성한 팩토리 컨트랙트입니다. 
    //모든 풀은 두 개의 ERC-20 토큰 간의 교환이며, 팩토리는 이 모든 풀을 연결하는 중심점입니다. 
    address public factory;

    //이 풀에서 교환할 수  있는 두 가지 유형의 ERC-20 토큰에 대한 컨트랙트 주소가 있습니다.
    address public token0;
    address public token1;

    //풀이 각 토큰 유형에 대해 보유하고 있는 준비금입니다. 두 가지가 동일한 가치를 나타내므로 각 토큰0의 가치는 토큰1/예비금0 토큰1의 가치가 있다고 가정합니다. 
    uint112 private reserve0;           // uses single storage slot, accessible via getReserves
    uint112 private reserve1;           // uses single storage slot, accessible via getReserves

    //환율이 발생한 마지막 블록의 타임스탬프로, 시간 경과에 따른 환율을 추적하는 데 사용됩니다.
    uint32  private blockTimestampLast; // uses single storage slot, accessible via getReserves
    // 이더리움 콘트랙트에서 가장 큰 가스 비용 중 하나는 콘트랙트의 한 통화에서 다음 통화까지 지속되는 스토리지입니다. 
    // 각 저장 셀의 길이는 256비트입니다. 따라서 하나의 저장소 값에 세 가지 변수가 모두 포함될 수 있도록(112+112+32=256)
    // 예비0, 예비1, 블록타임스탬프라스트의 세 가지 변수가 할당됩니다. 


//이 변수는 각 토큰의 누적 비용을 보유합니다(각각 다른 토큰에 대해). 일정 기간 동안의 평균 환율을 계산하는 데 사용할 수 있습니다.
    uint public price0CumulativeLast;
    uint public price1CumulativeLast;

// 페어 거래소가 토큰 0과 토큰 1 사이의 환율을 결정하는 방식은 거래 중에 두 준비금의 배수를 일정하게 유지하는 것입니다.
// 마지막은 이 값입니다. 유동성 공급자가 토큰을 입금하거나 출금할 때 변경되며, 0.3%의 시장 수수료로 인해 약간 증가합니다.
// 다음은 간단한 예시입니다. 단순성을 위해 소수점 이하 세 자리만 표시했으며, 0.3%의 거래 수수료를 무시했기 때문에 숫자가 정확하지 않을 수 있습니다.

    uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event

// 재진입 악용(새 탭에서 열기)을 기반으로 하는 보안 취약점 클래스가 있습니다. 
// 유니스왑은 임의의 ERC-20 토큰을 전송해야 하는데, 
// 이는 토큰을 호출하는 유니스왑 시장을 악용할 수 있는 ERC-20 컨트랙트를 호출하는 것을 의미합니다. 
// 컨트랙트의 일부로 잠금 해제된 변수를 사용하면 함수가 실행되는 동안(동일한 트랜잭션 내에서) 함수가 호출되는 것을 방지할 수 있습니다.
    uint private unlocked = 1;

//이 함수는 수정자(새 탭에서 열기)로, 일반 함수를 감싸서 어떤 식으로든 동작을 변경하는 함수입니다.
//잠금 해제가 1이면 0으로 설정합니다. 이미 0이면 호출을 되돌리고 실패로 설정합니다.
    modifier lock() {
        require(unlocked == 1, 'UniswapV2: LOCKED');
        unlocked = 0;
        _;
        unlocked = 1;
    }

//이 함수는 호출자에게 거래소의 현재 상태를 제공합니다. 솔리디티 함수는 여러 값을 반환할 수 있습니다(새 탭에서 열기).
    function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
        _reserve0 = reserve0;
        _reserve1 = reserve1;
        _blockTimestampLast = blockTimestampLast;
    }


//이 내부 함수는 거래소에서 다른 사람에게 일정량의 ERC20 토큰을 전송합니다. SELECTOR는 우리가 호출하는 함수가 transfer(address,uint)임을 지정합니다(위의 정의 참조).
//ERC-20 전송 호출이 실패를 보고하는 방법에는 두 가지가 있습니다:

// 1.되돌리기. 외부 컨트랙트에 대한 호출이 되돌리면 부울 반환값은 거짓입니다.
// 2. 정상적으로 종료하지만 실패를 보고합니다. 이 경우 반환값 버퍼의 길이가 0이 아니며, 부울 값으로 디코딩될 때 거짓이 됩니다.
// 이 두 조건 중 하나라도 발생하면 되돌립니다.


    function _safeTransfer(address token, address to, uint value) private {
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));

        //토큰 함수에 대한 인터페이스를 가져올 필요가 없도록 ABI 함수(새 탭에서 열기) 중 하나를 사용하여 호출을 "수동으로" 생성합니다.
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
    }
// 이 두 이벤트는 유동성 공급자가 유동성을 입금(Mint)하거나 출금(Burn)할 때 발생합니다. 
// 두 경우 모두 입금 또는 출금된 토큰0과 토큰1의 금액과 당사를 호출한 계정(발신자)의 신원이 이벤트의 일부가 됩니다. 
// 출금의 경우, 이벤트에는 토큰을 받은 대상(받는 사람)도 포함되며, 이는 발신자와 동일하지 않을 수 있습니다.
    event Mint(address indexed sender, uint amount0, uint amount1);
    event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
//이 이벤트는 트레이더가 한 토큰을 다른 토큰으로 교환할 때 발생합니다. 다시 말하지만, 발신자와 수신자가 동일하지 않을 수 있습니다. 각 토큰은 거래소로 보내거나 거래소로부터 받을 수 있습니다.
    event Swap(
        address indexed sender,
        uint amount0In,
        uint amount1In,
        uint amount0Out,
        uint amount1Out,
        address indexed to
    );

//마지막으로, 동기화는 이유에 관계없이 토큰을 추가하거나 출금할 때마다 최신 준비금 정보(따라서 환율)를 제공하기 위해 전송됩니다.
    event Sync(uint112 reserve0, uint112 reserve1);

//이 함수는 새 페어 교환을 설정할 때 한 번만 호출해야 합니다.
    constructor() public {
        factory = msg.sender;
    }

//생성자는 쌍을 생성한 팩토리의 주소를 추적할 수 있도록 합니다. 이 정보는 초기화 및 공장 수수료(있는 경우)에 필요합니다.
    // called once by the factory at time of deployment
    //이 기능을 사용하면 공장(그리고 공장만)이 이 쌍이 교환할 두 개의 ERC-20 토큰을 지정할 수 있습니다.
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
        token0 = _token0;
        token1 = _token1;
    }

    // update reserves and, on the first call per block, price accumulators
    //이 함수는 토큰을 입금하거나 출금할 때마다 호출됩니다.
    function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {


//balance0 또는 balance1(uint256)이 uint112(-1)(=2^112-1)보다 큰 경우(따라서 오버플로우가 발생하여 uint112로 변환할 때 0으로 다시 래핑됨) 
//오버플로우를 방지하기 위해 _update를 계속하지 않습니다.
//10^18 단위로 세분할 수 있는 일반 토큰의 경우, 이는 각 교환이 각 토큰의 약 5.1*10^15로 제한된다는 것을 의미합니다. 지금까지는 문제가 없었습니다.
        require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');

//경과 시간이 0이 아니라면 해당 블록의 첫 번째 거래소 트랜잭션이라는 뜻입니다. 이 경우 비용 누산기를 업데이트해야 합니다.        
        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
            //경과 시간이 0이 아니라면 해당 블록의 첫 번째 거래소 트랜잭션이라는 뜻입니다. 이 경우 비용 누산기를 업데이트해야 합니다.
            price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
            price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
        }
//각 비용 누산기는 최신 비용(다른 토큰의 보유량/이 토큰의 보유량)에 경과 시간(초)을 곱한 값으로 업데이트됩니다. 
//평균 가격을 구하려면 누적 가격을 두 시점으로 읽고 그 사이의 시간 차이로 나눕니다. 
//타임스탬프 5,030과 5,150 사이의 토큰 0의 평균 가격을 계산하고 싶다고 가정해 보겠습니다. 
//가격0 누적 값의 차이는 143.702-29.07=114.632입니다. 이는 2분(120초) 동안의 평균입니다. 따라서 평균 가격은 114.632/120 = 0.955입니다.
//마지막으로 전역 변수를 업데이트하고 동기화 이벤트를 발생시킵니다.
이 가격 계산은 이전 예약금 크기를 알아야 하는 이유입니다.
        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
        blockTimestampLast = blockTimestamp;
        emit Sync(reserve0, reserve1);
    }

//유니스왑 2.0에서 트레이더는 시장을 이용하기 위해 0.30%의 수수료를 지불합니다.
// 이 수수료의 대부분(거래의 0.25%)은 항상 유동성 공급자에게 전달됩니다. 
//나머지 0.05%는 유동성 공급자에게 지급되거나 유니스왑이 프로토콜 수수료로 지정한 주소로 지급되며, 이는 유니스왑의 개발 노력에 대한 대가로 지급됩니다.
    // 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) {
        //공장의 수수료 목적지를 확인합니다. 0이면 프로토콜 수수료가 없으므로 해당 수수료를 계산할 필요가 없습니다.
        address feeTo = IUniswapV2Factory(factory).feeTo();
        feeOn = feeTo != address(0);
        //kLast 상태 변수는 스토리지에 있으므로 컨트랙트에 대한 여러 호출 사이에 값을 갖게 됩니다. 
        //스토리지에 대한 액세스는 컨트랙트에 대한 함수 호출이 종료될 때 해제되는 휘발성 메모리에 대한 액세스보다 훨씬 더 비싸기 때문에 가스를 절약하기 위해 내부 변수를 사용합니다.
        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) {

//이 복잡한 수수료 계산은 5페이지의 백서(새 탭에서 열기)에 설명되어 있습니다. 
//kLast가 계산된 시점과 현재 사이에 유동성이 추가되거나 제거되지 않았으므로(유동성이 실제로 변경되기 전에 유동성이 추가되거나 제거될 때마다 이 계산을 실행하기 때문에)
// 준비금0 * 준비금1의 변화는 거래 수수료에서 발생해야 합니다(수수료가 없으면 준비금0 * 준비금1이 일정하게 유지됩니다).
                    uint numerator = totalSupply.mul(rootK.sub(rootKLast));
                    uint denominator = rootK.mul(5).add(rootKLast);
                    uint liquidity = numerator / denominator;

//UniswapV2ERC20._mint 함수를 사용하여 실제로 추가 유동성 토큰을 생성하고 이를 수수료 토큰에 할당합니다.
                    if (liquidity > 0) _mint(feeTo, liquidity);
                }
            }
//수수료가 없는 경우(아직 없는 경우) kLast를 0으로 설정합니다. 
//이 콘트랙트를 작성할 당시에는 가스 환불 기능(새 탭에서 열기)이 있어 콘트랙트가 필요하지 않은 스토리지를 제로화하여 이더리움 스테이트의 전체 크기를 줄이도록 권장했습니다. 
//이 코드는 가능한 경우 해당 환불을 가져옵니다.
        } else if (_kLast != 0) {
            kLast = 0;
        }
    }


//외부에서 액세스 가능한 함수
//모든 트랜잭션이나 콘트랙트가 이러한 함수를 호출할 수 있지만, 
//이는 주변 콘트랙트에서 호출하도록 설계되었습니다. 직접 호출하면 페어 교환을 속일 수는 없지만 실수로 가치를 잃을 수 있습니다.
    // 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);//
//징수할 프로토콜 수수료(있는 경우)를 계산하고 그에 따라 유동성 토큰을 발행합니다. mintFee의 매개변수는 이전 준비금 값이므로 수수료로 인한 풀 변경만을 기준으로 수수료를 정확하게 계산합니다.
        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
//첫 입금인 경우, MINIMUM_LIQUIDITY 토큰을 생성하고 이를 주소 0으로 전송하여 잠급니다. 
//이 토큰은 절대로 상환할 수 없으므로 풀이 완전히 비워지지 않습니다(일부 지역에서 0으로 나누지 않아도 됩니다). 최소 유동성의 값은 1,000이며, 
//대부분의 ERC-20이 토큰의 10^-18'의 단위로 세분화되어 있다는 점을 고려할 때 이더리움이 웨이로 나뉘는 것처럼 단일 토큰의 값에 10^-15가 됩니다. 높은 비용이 아닙니다.        
//첫 입금 시에는 두 토큰의 상대적 가치를 알 수 없으므로, 입금이 두 토큰에 동일한 가치를 제공한다고 가정하고 금액을 곱한 후 제곱근을 구합니다.
//차익거래로 인한 가치 손실을 피하기 위해 동일한 가치를 제공하는 것이 예금자에게 이익이 되기 때문에 이를 신뢰할 수 있습니다. 
//두 토큰의 가치가 동일하지만 예금자가 토큰1을 토큰0보다 4배 더 많이 예치했다고 가정해 봅시다.
// 트레이더는 페어 거래소가 토큰0의 가치가 더 높다고 생각한다는 사실을 이용해 토큰1의 가치를 끌어낼 수 있습니다.

//보시다시피, 트레이더는 풀의 가치 하락으로 인해 8개의 토큰을 추가로 획득했으며, 이는 해당 토큰을 소유한 예금자에게 손해를 끼쳤습니다.
        } else {

            liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
        }
// 이후 입금할 때마다 두 자산 간의 환율을 이미 알고 있으며, 유동성 공급자가 두 자산 모두에서 동일한 가치를 제공할 것으로 기대합니다. 
//그렇지 않은 경우, 저희는 더 적은 가치를 제공한 것에 대한 벌칙으로 유동성 토큰을 지급합니다.
// 초기 예치금이든 후속 예치금이든, 저희가 제공하는 유동성 토큰의 수는 준비금0*준비금1의 변화의 제곱근과 같으며 유동성 토큰의 가치는 변하지 않습니다
//(두 유형의 가치가 같지 않은 예치금을 받지 않는 한, 이 경우 '벌금'이 분배됩니다). 
//다음은 동일한 가치를 가진 두 개의 토큰을 예치하는 또 다른 예시로, 3개의 좋은 예치와 1개의 나쁜 예치(한 가지 토큰 유형만 예치하므로 유동성 토큰이 생성되지 않음)가 있습니다.


//UniswapV2ERC20._mint 함수를 사용하여 실제로 추가 유동성 토큰을 생성하고 올바른 계정으로 전달합니다.
        require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
        _mint(to, liquidity);

//상태 변수(reserve0, reserve1, 필요한 경우 kLast)를 업데이트하고 적절한 이벤트를 발생시킵니다.
        _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);
    }


// 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);
    }
    //나머지 burn 기능은 위의 mint 기능의 미러 이미지입니다.

    // this low-level function should be called from a contract which performs important safety checks
    //이 함수는 또한 periphery 컨트랙트에서 호출되어야 합니다.
    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;

        //로컬 변수는 메모리에 저장할 수도 있고, 너무 많지 않은 경우 스택에 직접 저장할 수도 있습니다. 
        //스택을 사용하도록 개수를 제한할 수 있다면 가스 사용량을 줄일 수 있습니다. 자세한 내용은 옐로 페이퍼, 공식 이더리움 사양(새 탭에서 열기), 26페이지, 방정식 298을 참조하세요.
        { // 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));
        }


    // 이는 스왑으로 인해 손실이 발생하지 않도록 하기 위한 건전성 검사입니다. 스왑으로 인해 리저브0*리저브1이 감소하는 상황은 없습니다. 
    // 또한 이 단계에서 0.3%의 수수료가 스왑으로 전송되는지 확인합니다. 
    // K값을 확인하기 전에 두 잔액에 1000을 곱한 후 3을 곱한 금액을 뺍니다. 즉, 현재 준비금 K값과 비교하기 전에 잔액에서 0.3%(3/1000 = 0.003 = 0.3%)가 공제된다는 뜻입니다.
        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');
        }



    //적립금0과 적립금1, 필요한 경우 가격 누산기와 타임스탬프를 업데이트하고 이벤트를 발생시킵니다.
        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);

    }

//실제 잔액이 페어 거래소가 생각하는 준비금과 일치하지 않을 수 있습니다. 
//계약자의 동의 없이 토큰을 인출할 수 있는 방법은 없지만, 예치금은 다른 문제입니다. 계정은 민트나 스왑을 호출하지 않고도 거래소에 토큰을 전송할 수 있습니다.

// 이 경우 두 가지 해결책이 있습니다:


// skim, 추가 금액을 인출합니다. 누가 토큰을 입금했는지 알 수 없으므로 모든 계정에서 스킴을 호출할 수 있다는 점에 유의하세요. 
// 이 정보는 이벤트로 전송되지만 블록체인에서는 이벤트에 액세스할 수 없습니다.
    // 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));
    }

// sync, 준비금을 현재 잔액으로 업데이트합니다.
    // force reserves to match balances
    function sync() external lock {
        _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
    }
}

 

 

출처:

https://ethereum.org/en/developers/tutorials/uniswap-v2-annotated-code/#uniswapv2pair 

 

Home | ethereum.org

Ethereum is a global, decentralized platform for money and new kinds of applications. On Ethereum, you can write code that controls money, and build applications accessible anywhere in the world.

ethereum.org

 

관련글 더보기