솔리디티

Uniswap(유니스왑) ERC20_core 솔리디티 코어 코드 리뷰

u0jin 2020. 12. 20. 08:21
//////////////// 핵심 코어 /////////////////

pragma solidity =0.5.16;
// 솔리디티 컴파일러의 버전

import './interfaces/IUniswapV2ERC20.sol';

import './libraries/SafeMath.sol';
// add , sub , mul 선언되어있음


// 컨트랙트 끼리 상속받을수 있음 
// 컨트랙 이름 정함 
contract UniswapV2ERC20 is IUniswapV2ERC20 {
    using SafeMath for uint;
    // unit :: 양수값만 받음  <-> int :: 음수도 포함  (8비트 ~ 256비트 숫자 지정해서 쓸수있음) :: unit 로 하면 변수에 담을수 있는 양수의 사이즈가 더 많이 가능하겠지 
    // unit == unit256
    // 상태 변수  :: 컨트랙 저장소에 영구히 저장되는 상태변수임
    // 상태변수는 클래스의 멤버변수라고 보면됨
     
    // 디폴트로 internal로 되어있음 :: 상태변수 

    string public constant name = 'Uniswap V2';
    string public constant symbol = 'UNI-V2';
    uint8 public constant decimals = 18;
    uint  public totalSupply;

// 매핑이란, 주소를 잔액과 연결하는 연관배열을 의미한다.
    mapping(address => uint) public balanceOf;

    // address 타입 :: 20바이트 값 (이더리움 주소만큼의 사이즈임 == 0x 접두사 빼면 ,40 글자임 ) , 이더리움 계정 주소임 , 두개 멤버 소유 balance , transfer 
// balance ::  주소의 밸런스 , eth를 얼마나 소유하고 있는지 조회 가능 
// transfer :: 해당 주소에 eth를 보낼수 있음 ex) x.transfer(10);  (x가 주소)
    mapping(address => mapping(address => uint)) public allowance;


// bytes 사이즈 지정가능 :: 1~32 까지 사이즈 지정가능
// 바이트로 문자열 저정할때는 문자열을 헥스로 변환해서 저장해야함  :: 라이브러리 사용하면 간단하게 변환가능 입니당 
// 솔리디티는 스트링에 최적화가 안되어있음 
// 32바이트 문자열에 최적화됨 . 32초과면 string 타입쓰셈
// 32안넘기면  바이트 ㄱㄱ
// 스트링은 가스비용이 바이트보다 더 요구됨 ㅇㅋ? ㅇㅇ
// 참고 byte == byte1


    bytes32 public DOMAIN_SEPARATOR;
    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
    bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
    mapping(address => uint) public nonces;

    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

    constructor() public {
         // 생성자를 만드는 과정  :: constructor 키워드 쓰고 public 붙임 >> 생성자 완성 

        uint chainId;
        assembly {
            chainId := chainid
        }
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
                keccak256(bytes(name)),
                keccak256(bytes('1')),
                chainId,
                address(this)
            )
        );
    }
// 함수의 구조 >> 자바스크립트 or 자바랑 비슷 

// fuction키워드 + 함수 이름 + (매개변수) +  [함수 타입] + [리턴타입]
// 함수 타입은 지정해주면 됨 
// 만약 값을 리턴하는 타입이면 어떤 타입을 리턴하는지 정해주면 된다

//함수 타입중 public 다됨(내부,외부, 상속 싹다 가능 ) >> 가시성을 명시안하면 ,즉  비워두면 public 으로 인식함 >>
//비우면 컴파일할때, warning이 뜨기때문에 채우는게 좋음 
// 상태변수에서 public 접근제어자를 선언하면 >> 컴파일러에서 자동적으로 상태변수의 getter 함수를 생성함 

    function _mint(address to, uint value) internal {
        totalSupply = totalSupply.add(value);
        balanceOf[to] = balanceOf[to].add(value);
        emit Transfer(address(0), to, value);
    }

    function _burn(address from, uint value) internal {
        balanceOf[from] = balanceOf[from].sub(value);
        totalSupply = totalSupply.sub(value);
        emit Transfer(from, address(0), value);
    }


// private 은 컨트랙 내부에서만 호출 가능 
// inter 과 비슷하지만, 상속이 안되는겟이 다름 .. internat은 상속 가능 . private은 상속 불가임ㄴ

    function _approve(address owner, address spender, uint value) private {
        allowance[owner][spender] = value;
        emit Approval(owner, spender, value);
    }

    function _transfer(address from, address to, uint value) private {
        balanceOf[from] = balanceOf[from].sub(value);
        balanceOf[to] = balanceOf[to].add(value);
        emit Transfer(from, to, value);
    }


// 접근제어자 ::: external 의 경우 같은 contract 안에서는 부를수없으나, 다른 컨트랙에서는 호출가능
//  <-> internal 은 안에서 부를수 있음 :: 내부의 함수끼리 호출가능 
    function approve(address spender, uint value) external returns (bool) {
        _approve(msg.sender, spender, value);
        return true;
    }

    function transfer(address to, uint value) external returns (bool) {
        _transfer(msg.sender, to, value);
        return true;
    }

    function transferFrom(address from, address to, uint value) external returns (bool) {
        if (allowance[from][msg.sender] != uint(-1)) {
            allowance[from][msg.sender] = allowance[from][msg.sender].sub(value);
        }
        _transfer(from, to, value);
        return true;
    }

    function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
        require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
        bytes32 digest = keccak256(
            abi.encodePacked(
                '\x19\x01',
                DOMAIN_SEPARATOR,
                keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
            )
        );
        address recoveredAddress = ecrecover(digest, v, r, s);
        require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
        _approve(owner, spender, value);
    }
}

 

블록체인 코어 함수 분석을 위해 대략적인 문법정도는 주석으로 처리했습니다.

하나씩 좀더 해석해보면, 친구한테 설명하듯이 편하게 코드 읽는 순서대로 설명할게요

 

pragma solidity =0.5.16;

import './interfaces/IUniswapV2ERC20.sol';

import './libraries/SafeMath.sol';

솔리디티 컴파일러 버전은 0.5.16 이다.

IUniswapV2ERC20 인터페이스를 임포트

SafeMath 라이브러리를 임포트 >> add ,sub , mul 선언되어있다.

contract UniswapV2ERC20 is IUniswapV2ERC20 {
    using SafeMath for uint;

컨트랙트이름을 UniswapV2ERC20 으로 정함

이 컨트랙트는 IUniswapV2ERC20를 참조함

임포트했던, SafeMath 를 양수값만 받도록 설정하고, 사용함

contract 안에서 상태변수 SateMath를 선언한것과 같으며 상태변수는 블록체인안에서 영구히 저장되는 속성을 가짐

상태변수에 접근제어가 표시가 아무것도 없다면, internal로 디폴트 된다.

상태변수는 클래스의 멤버변수로 생각하면 됨

string public constant name = 'Uniswap V2';
string public constant symbol = 'UNI-V2';
uint8 public constant decimals = 18;
uint  public totalSupply;

name, symbol, decimals. totalsupply 선언해줌 

    mapping(address => uint) public balanceOf;
    mapping(address => mapping(address => uint)) public allowance;

매핑을 선언하는데, 매핑이란, 주소와 잔액을 연결하는 연결배관 역할을 해주는걸로 이해하면된다.

여기서는 mapping(address => uint) public balanceOf; 로 선언했고

주소형 키타입으로, unit을 값타입으로 매핑설정함 이걸 balanceOf 로 선언했음

이거 이제 아래 코드에서 이용할거임

주소형 타입은 , 이더리움 계정 주소를 말해, 얘는 두개의 멤버 balance , transfer 를 가짐

balance : eth를 얼마나 소유하고 있는지 조회가능

transfer : 해당 주소에 eth를 보낼수 있음

mapping(address => mapping(address => uint)) public allowance;

address 타입 : key 타입

mapping(address => uint) : value 타입

으로 allowance 선언함

    bytes32 public DOMAIN_SEPARATOR;
    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
    bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
    mapping(address => uint) public nonces;

 

DOMAIN_SEPARATOR ,PERMIT_TYPEHASH 를 바이트로 선언했어, 가스비용 생각해서 해준거임

mapping(address => uint) public nonces;

nonces 로 매핑선언해줌

    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

 그냥 이벤트 선언해준거야 

 

 

   
    constructor() public {

        uint chainId;
        assembly {
            chainId := chainid
        }
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
                keccak256(bytes(name)),
                keccak256(bytes('1')),
                chainId,
                address(this)
            )
        );
    }

 

 

/* constructor : 생성자는 클래스 내의 객체를 만들고 초기화하는데, 도움이 되는 특수 함수이다.

각 클래스는 하나의 생성자로 제한된다.

생성자라고 부르는 것으로써 최초 계약 배포 시점에 한해 1회만 수행되는 함수이다

생성자 함수의 이름은 반드시 계약 이름과 동일해야 함 */

 

초기배포시 최초 1회 실행되는 그 함수, 이게 좀 중요하다면 중요할수있지

생성자를 만들어준다. -> 얘는 초기에 배포할때 한번 돌려지는거야

생성자는 클래내의 객체를 만들고, 초기화 하는데 도움이 되는 특수 함수이다.

얘는 상태변수 데이터를 초기화해주는 함수이다.

chainId 를 unit 로 선언해준다.

어셈블리를 사용해서 가스비용 절약하네

:= 연산자를 이용해서 초기화를 시켜준거임

위에서 선언한 DOMAIN_SEPARATOR 를 이제 keccak256 해시를 계산해서 저장하게 해

컴파일하면 나오느 abi 의 정보를 다시 encode 하고, 해시로 감싼다음에 해당되는걸 가져와서 초기화를 해주는거같아

address (this) :: 현재의 컨트랙트 address 로 명시적 변환이 가능해

/* 스마트컨트랙트 코드는 컴파일을 거치면 abi + bin (바이트코드)가 나와

이걸 사용해서 contract 객체를 만들어주고, 배포에 사용될 지갑 주소가 포함도니 트랙잭션을 생성해, deploy 해줘 */

 

아래는 함수를 쭉 정의 해줬는데,

 

    function _mint(address to, uint value) internal {
        totalSupply = totalSupply.add(value);
        balanceOf[to] = balanceOf[to].add(value);
        emit Transfer(address(0), to, value);
    }

    function _burn(address from, uint value) internal {
        balanceOf[from] = balanceOf[from].sub(value);
        totalSupply = totalSupply.sub(value);
        emit Transfer(from, address(0), value);
    }

 

_mint 함수는 internal 로 선언되어있는데 얘는 내부 호출이 가능해, 상속받은 컨트랙도 호출가능

mint 이 친구는 사실 음 용어적으로 보면 블록체인에서 발행을 뜻해

여기서는 BME 라는용어가 나오는데,

burn and mint equilibrium 라고 해서 소각이랑 발행을 말해,

소각은 토큰이 사용되면 소각이 되자나 그래서 소각인건데

토큰이 많이 사용되면 많이 소각된다고 볼수있지? = 소각이 많이 되면 토큰의 가치가 상승해

근데, 끝없이 소각만 되면 안되니까 발행이 생긴거야 그래서 소각과 발행으로 토큰의 유통량을 조절할수 있어

=== 즉 뭐냐? 이걸로 토큰의 가치에 영향을 미칠수있다는거지

그니까 다시 보면

지금 여기에 선언된 함수는 토큰의 가치에 영향을 미칠수 있는 부분인거야

다시말하면 뭐다? 중요한 함수다 이거임 ㅇㅇ

자 다시 함수로 가서 mint 함수는 일단 parm 으로 주소형 to , unit 값을 받아오고

internal로 선언했어

좀더 자세히 보면, to 는 토큰을 받을주소 를 말하고, value 는 받을 토큰의 양 을 말하는거야

이걸 ineternal로 선언했다는건 이 컨트랙으로만 호출이 가능하다는거임

다시 코드를 보면

그안에서 totalsupply 는 contract 정의할때, 초기에 선언한 변수 그대로 가져와서 값을 넣어주면 그걸 다시 더해서 재정의하도록 만들어줬어

balanceOf도 위에서 선언된 친구 데려와서 쓴건데 :: 모든 잔액을 가진 배열

얘는 수신자 잔액에 값 넣어주면 더해서 재정의한거야 함수 이름에 맞게 ''발행''한거지?

그리고 emit transfer 보이지? 이걸로 코인 보낸거임

emit 이라는 키워드는 선언한 이벤트에 데이터를 넣는역할을 해줘

 

event Transfer(address indexed from, address indexed to, uint value); 라고 위에서 정의해줬지?

정의된 transfer이벤트에 맞게 emit Transfer(address(0), to, value); 이렇게 값을 넣어준거야

 

다음 함수인 burn을 보면

burn 은 mint 와 반대되는 개념이야 소각한다는 뜻이니까

민트와 반대로, 빼주면 돼 간단하죠?

    function _approve(address owner, address spender, uint value) private {
        allowance[owner][spender] = value;  // 송금액 
        emit Approval(owner, spender, value);
    }

_approve 보면 ,

소유자 주소, 지출에 권한이 있는 주소 = 소비자 주소 , 지출할 최대금액을 parm으로 가져갔어

private으로 선언한걸 보니 중요한가봐요

컨트랙 내부에서만 호출가능하게 해놨네요

상속도 안되요

음 ,,진짜 중요한가봐요

value를 송금액으로 저장해준거고,

Approval 이벤트를 내보내주는 역할을해

 

    function _transfer(address from, address to, uint value) private {
        balanceOf[from] = balanceOf[from].sub(value);
        balanceOf[to] = balanceOf[to].add(value);
        emit Transfer(from, to, value);
    }

 

_transfer 함수를 보면

value 토큰이 from계정에서 to 계정으로 이동할때 발생

balanceOf를 제정의 해줘 from 계정에서 보내는거니까 from 에선 빼주고,

to계정에서는 받아가는거니까 더해주는걸로 재정의 해주는 거야

그리고 Transfer 이벤트를 불러와

 

    function approve(address spender, uint value) external returns (bool) {
        _approve(msg.sender, spender, value);
        return true;
    }

 

 

다음으로 approve 함수를 보자

parm 으로 된 친구들 부터 보면 , spender 있지 얘는 지출권한이있는주소를 말해, 그리고 value는 지출 할수 있는 최대 금액을 말해주고있어

앞에서 만들어둔 _approve 가지고 와서 다른주소에서 토큰을 이체할수있도록 하는것같아

/* 솔리디티에는 모든 함수에서 이용가능한 특정 전역변수가 있음

msg.sender 는 그 중하나이다.

현재 함수를 호출한 사람 (혹은 스마트컨트랙트)의 주소 = msg.sender

msg.sender 를 활용하면 이더리움 블록페인의 보안성을 높일수 있다.

msg.sender 는 개인티 역할을 하기때문에 누군가 다른사람의 데이터를 변경하려면 해당 이더리움 주소와 관련된 개인키를 훔치지 않는 이상 해킹을 당하지 않게 한다. */

 

    function transfer(address to, uint value) external returns (bool) {
        _transfer(msg.sender, to, value);
        return true;
    }

 

transfer 함수는 _transfer 함수를 불러내는게 전부야

다만, 여기에 msg.sender 가 들어가고 이친구는 현재 함수를 호출한 사람 주소를 말해

그니까 from 을 현재 함수 로출한 사람의 주소로 두는거지

parm값만 정해준것뿐이야

 

 

    function transferFrom(address from, address to, uint value) external returns (bool) {
        if (allowance[from][msg.sender] != uint(-1)) {
            allowance[from][msg.sender] = allowance[from][msg.sender].sub(value);
        }
        _transfer(from, to, value);
        return true;
    }

transferFrom 을 보면 ,

value 값을 from 이 to에게 전달을 해

if문 해석이 너무 어려움

uint 는 양수만 표현가능 여기서 unit(-1) 이 정확히 무슨 값인지는 알 수 없지만, 아마 에러가 아닌 상황에서

아무튼 같지않으면, allowance에서 value값을 뺸걸로 재정의 해주고 송금한다는 의미로 받아들였어

느낌으로는 최소금액? 돈이 보낼수 있는 정도가 있어야 보낼수있도록 송금할수 있도록 if 문으로 설정해 준거같아

 

    function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
        require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
        bytes32 digest = keccak256(
            abi.encodePacked(
                '\x19\x01',
                DOMAIN_SEPARATOR,
                keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
            )
        );
        address recoveredAddress = ecrecover(digest, v, r, s);
        require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
        _approve(owner, spender, value);
    }
}

permit 함수는 

require 로 조건을 달아줌

deadline >= block.timestamp, 'UniswapV2: EXPIRED' 라는 조건이 충족되지않으면 발생한다.

deadline 이 유닉스 시대 이후의 현재 블록 타임 스탬프 보다 크거나 같다.

'UniswapV2: EXPIRED' = 메인넷에 브로드캐스트 하는데 너무 오래걸리면 나오는 결과임

스왑을 실행하는데 20분 넘게 걸리는 경우 코어 컨트랙은 허용하지않게된다.

현재 시간이 deadline 을 초과할 경우 'UniswapV2: EXPIRED' 를 출력해준다

 

 

바이트32로 가스비용 절감해서 써줬고, digest 는

keccak256 해시를 계산해서 저장하게 해

ncode 방식보다 간편하게 인코딩을 하기위해 abi.encodepacked() 함수를 사용했다.

이 함수는 32바이트보다 작은 문자는 그냥 해당 문자열을 바이트로 출력하고, 32바이트에 패딩하지도 않는다.

특징은, keccak256 을 사용하여 hashing 할때, 복수개의 인자를 전달하면, 이것을 내부적으로 abi.encodepacked() 함수로 인코딩하여 해싱한다는 것이다.

 

주소형recoveredAddress 은

ecrecover 라는 내장함수를 써서 정의해주었다. 서명에서 주소를 복구 할때 사용한다.

ecrecover(messageHash, v,r,s)

 

서명된 메세지를 v,r,s 로 분할한 다음 원래 해시된 메시지와 함께 ecrecover 를 수행하면 되는것이다.

ecrecover 를 사용하여, 함수의 서명자를 검색하는것과 같다.

require 로 필요조건을 정해주고

유효하지 않은 서명은 빈 주소를 생성한다.

이것으로 마지막 확인을 진행한후,

 

_approve 를 불러온다.

 

 

 

  • `block.timestamp`( `uint`) : 유닉스 시대 이후의 현재 블록 타임 스탬프 (초)

  •  

    `require(bool condition)`: 조건이 충족되지 않으면 발생합니다. 입력 또는 외부 구성 요소의 오류에 사용됩니다.

     

 


마치며

코어 코드를 작성할때, 거의 모든 블록체인 코어는 함수를 돌려쓴다.

아주 살짝씩만 바꿔서 돌려쓰기 때문에

한번만 제대로 익히면 가성비가 좋다고 생각한다.