간단한 CSMM 스왑 만들기

유지민·2022년 12월 28일
0

solidity

목록 보기
4/4

오픈 제플린의 ERC-20을 이용하여 간단한 스왑을 만들었다.
토큰의 교환비가 결정되도록 하는 알고리즘인 많은 AMM(Auto Market Maker)중에 가장 간단한 CSMM(Constant Sum Market Maker)으로 만들었다.
CSMM은 다음과 같은 공식을 가진다.

x+y=k

항상 합이 K로 일정하게 하는 알고리즘이다.

  • x, y는 유동성 풀에 있는 각각 토큰의 개수.
  • x, y의 양이 변해도 k의 양은 고정
  • A토큰을 넣으면 B토큰으로 동일한 양을 받는다.
  • 한쪽만 일방적으로 넣거나 할경우 한쪽이 0이될수도 있다.
    CSMM의 기본 특징은 five1star님 블로그에서 참고했다. 특징이 정리가 잘되어있다.

ERC-20표준 코드를 사용할꺼기 때문에
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol 를 옆에 켜두고 작업 한다.
어떤함수가 어떻게 사용되는지 확인하면서 작업했다.
IERC20에 있는 함수들만 이용해 내가 만들면 기본적인 스왑을 만들수있다는데 연습임으로 erc20 함수를 그대로 사용하거나 override만 했다.

ps. eERC와 IERC차이는 IERC는 Contract의 interface만 인용한것. ERC는 전체적인 함수 구조까지 가져오는것
참고 : https://blog.naver.com/PostView.naver?blogId=thswjdtmq4&logNo=222503519156&categoryNo=22&parentCategoryNo=0&viewDate=¤tPage=1&postListTopCurrentPage=1&from=search
https://ethereum.stackexchange.com/questions/99531/whats-the-difference-between-openzeppelins-ierc-vs-erc-contracts

이제 Remix로 작업을 해준다.
(참고로 이 스왑은 솔리디티 내에서만 작동하는걸 전제로 하기 때문에 돌아가는 기능이 있을수도 있다.)

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; 

기본 세팅이다. openzeppelin의 ERC20을 import해서 상속받아 사용하자.

우선 A토큰, B토큰을 만드는 코드를 짤것인데 ERC20을 상속받자.

ERC20의 constructor에는 name, symbol을 정해야 하기 때문에

contract A_simple is ERC20("A_Token", "AA"){

}

이렇게 A토큰의 이름과 심볼을 상속받을떄 정해줘서 넣어준다.(B토큰도 이름만 바꿔서 똑같이 작업해준다.)

이제 가장 먼저 사용될 함수인 _mint함수 A토큰이 생성될때 사용해주자.
_mint함수가 어떻게 생겼는지 확인하면

    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        unchecked {
            // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
            _balances[account] += amount;
        }
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

간단하게 생각하면 account가 0이 아닌걸 확인 후 amount만큼 해당 주소로 뚝딱 생겨난 토큰을 보낸다고 생각하면 될 것 같다.

해당 컨트랙트에 생기게 작성.

contract A_simple is ERC20("A_Token", "AA"){
    constructor(uint a) {
        //_mint(msg.sender, a);	->이 경우엔 컨트랙트 생성자에게 a만큼 전송
        _mint(address(this), a); //A_simple이 a만큼
    }
    
    receive() external payable {}	//해당 컨트랙트가 다른컨트랙트에게서 돈을 받게 해주는 코드
}

이제 CSMM 코드를 작성

contract CSMM {
    ERC20 public token_a;	//각각 토큰을 변수로 생성
    ERC20 public token_b;
    uint public k;  //x+y=k

    function tokenSetting(address _a, address _b) public {
        token_a = ERC20(_a);	//위에 베포한 A_simple의 주소로 세팅
        token_b = ERC20(_b);
    }
}

이제 CSMM에 3가지 기능을 추가해야하는데
1. 유동성 공급
2. 유동성 제거
3. swap이다.

우선 유동성 공급으로 Liquidity 풀에 토큰을 넣어주는 함수를 만들자.

유동성 공급

token_a의 토큰을 어떻게 CSMM의 Liquidity에 넣을지 생각해본 결과 오픈 제플린의 transfer나 transferFrom을 사용하면 될것같다.
그런데 문제가 생겼다. transfer과 _transfer함수를 보니

    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }
    ------
    function _transfer(address from, address to, uint256 amount) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
            // decrementing then incrementing.
            _balances[to] += amount;
        }

        emit Transfer(from, to, amount);

        _afterTokenTransfer(from, to, amount);
    }    

내가 원하는 것은 (함수를 실행하는 사람)에서 CSMM에 토큰을 보내는것인데
이 상태에서

//CSMM contract

    function addA(uint _a) public {
        token_a.transfer(address(this), _a);
    }

이렇게 넣어버리면

//simple_A contract

    function transfer(CMMR주소, uint256 _a) public virtual override returns (bool) {
        address owner = _msgSender();//_msgSender가 CMMR주소가 되어버림
        _transfer(owner, to, amount);
        return true;
    }

그럼 CMMR이 CMMR에게 보내는 꼴이 될뿐더러 CMMR은 토큰도 없어 실행이 안된다. (외부 web.js나 에서 실행할경우 msgsender가 외부가 될테니 되지 않을까?)

그러면 transferFrom을 써보기로 했다.

    function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }
    ------
    function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

이런식으로 쓰게 되면

//simple_A.sol

    function addA(uint _a) public {
        token_a.transferFrom(address(this), msg.sender, _a);
    }

또 spender와 owner가 꼬여버리게 된다. 누르는 사람의 토큰을 CSMM 풀에 넣는것이 최종 목적
그렇지만 위에처럼 쓰면 spender가 csmm이 되어버리고 owner도 csmm이 되어버린다.

결국 이러한 internal과 msg.sender의 변경으로 인해 approve와 transferFrom을 override로 재정의 하기로 한다.

//simple_A contract

contract A_simple is ERC20("A_Token", "AA"){
    constructor(uint a) {
        _mint(msg.sender, a);
        _mint(address(this), a);
    }

    mapping(address => bool) blacklist;

    function approve(address owner, uint amount) public override returns(bool) {
        _approve(owner, msg.sender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public override returns(bool) {
        _spendAllowance(from, to, amount);
        _transfer(from, to, amount);
        return true;
    }

	//blackList는 재밌어 보여서 추가 (신경쓰지말자)
    function blackList(address _a) public {
        blacklist[_a] = true;
    }

    function sendMoney(address _a, uint _b) public {
        require(blacklist[_a]==false);
        _transfer(address(this), _a, _b);
    }

    receive() external payable {}
}

이제 sender(사람)의 돈을 CSMM이 허락을 맡고?(approve) 쓸수 있다.

// CSMM contract

    function addLiquidity(uint _a, uint _b) public {
        addA(_a);
        addB(_b);
    }

    function addA(uint _a) public {
        token_a.approve(msg.sender, _a);
        token_a.transferFrom(msg.sender, address(this), _a);
        k+=_a;
    }

    function addB(uint _b) public {
        token_b.approve(msg.sender, _b);
        token_b.transferFrom(msg.sender, address(this), _b);
        k+=_b;
    }   

이렇게 작성하면 함수를 실행하는 사람의 _a만큼 토큰을 CSMM이 approve하고 _spendAllowance, transferFrom 모두 원하는대로 가능하다. A_simple은 ERC-20을 상속 받았기때문에 _transfer, _spendAllowance 등등 사용 가능

swap

이제 addLiquidity 함수로 유동성 공급을 했으니 swap을 해주자

swap은 총 3가지 단계이다. (x+y=k를 기억하자)
1. 사람이 CSMM에게 A토큰을 _a만큼 준다
2. CSMM는 얼만큼 내어줄지 계산
3. 2에서 계산한 값만큼 CSMM는 사람에게 B토큰 전송

(csmm에서는 1:1비율임으로 A토큰을 a'만큼 넣었다면 B토큰도 a'만큼 돌려주면 되지만 추후 cpmm연슴 겸 돌려주는 양을 y-y' = k-x - (k-x-a') ==> a' 으로 계산했다.)

// CSMM contract

    function swapA(uint _a) public { //일단 a코인을 넣는걸로 고정
        uint current_k = k; //현재 k를 저장
        addA(_a);
        uint out = k-current_k; //결국 == _a
        
        //contract B가 사람한테 B토큰 내어주기
        token_b.transfer(msg.sender, out);
        k=k-out;
    }

(비율이 1:1임으로 외부시장에서 가치가 변하여 한쪽 코인만 넣거나 뺼경우 한쪽의 양이 0이 될수도 있다. 일반적인 swap에서는 csmm을 사용하지않고 특별한 경우 사양한다고 한다.)

유동성 제거

유동성 제거는 간단하다.

// CSMM contract

    function subA(uint _a) public {
        token_a.transfer(msg.sender,  _a);
        k-=_a;
    }

하면

openzeplin의 transfer

    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

owner는 csmm이 맞고 to도 사람이 맞으니 유동성 공급처러 ㅁ난리치지 않고 얌전히 transfer을 사용하면 된다.

전체 코드

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; 

contract A_simple is ERC20("A_Token", "AA"){
    constructor(uint a) {
        _mint(msg.sender, a);
        _mint(address(this), a);
    }

    mapping(address => bool) blacklist;

    function approve(address owner, uint amount) public override returns(bool) {
        _approve(owner, msg.sender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public override returns(bool) {
        _spendAllowance(from, to, amount);
        _transfer(from, to, amount);
        return true;
    }

    function blackList(address _a) public {
        blacklist[_a] = true;
    }

    function sendMoney(address _a, uint _b) public {
        require(blacklist[_a]==false);
        _transfer(address(this), _a, _b);
    }

    receive() external payable {}
}

contract B_simple is ERC20("B_Token", "BB"){
    constructor(uint a) {
        _mint(msg.sender, a);
        _mint(address(this), a);
    }

    mapping(address => bool) blacklist;

    function blackList(address _a) public {
        blacklist[_a] = true;
    }

    function sendMoney(address _a, uint _b) public {
        require(blacklist[_a]==false);
        _transfer(address(this), _a, _b);
    }

    function approve(address owner, uint amount) public override returns (bool) {
        _approve(owner, msg.sender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
        //to = msg.sender;
        _spendAllowance(from, to, amount);
        _transfer(from, to, amount);
        return true;
    }

    receive() external payable {}
}

contract CSMM {
    ERC20 public token_a;
    ERC20 public token_b;
    uint public k;  //x+y=k

    function tokenSetting(address _a, address _b) public {
        token_a = ERC20(_a);
        token_b = ERC20(_b);
    }

    // 유일하게 k가 변할떄
    // 유동성 공급 -> 임의로 (+ 추후 LP 토큰으로 바꾸어 가격을 맞추어서)
    // function addLiquidity(uint _a, uint _b) public {
    //     //token_a.transfer(address(this), _a);    //이렇게 짜면 contractB가 _a에게 보내기떄문에 실행X(B는 돈을 가진적이 없음)
    //     //token_a.transferFrom(msg.sender, address(this), _a);    //이렇게 쓰면 transferFrom안에 _spendAllowance를 쓰려면 approve를 했어야하는데 과정 없음
    //     //1. approve를 다시 만들거나(override), A_simple, B_simple을 상속속

    //     token_a.approve(msg.sender, _a);
    //     token_b.approve(msg.sender, _b);
    //     token_a.transferFrom(msg.sender, address(this), _a);
    //     token_b.transferFrom(msg.sender, address(this), _b);
    //     k+=_a+_b;
    // }

    function addLiquidity(uint _a, uint _b) public {
        addA(_a);
        addB(_b);
    }

    function addA(uint _a) public {
        token_a.approve(msg.sender, _a);
        token_a.transferFrom(msg.sender, address(this), _a);
        k+=_a;
    }

    function addB(uint _b) public {
        token_b.approve(msg.sender, _b);
        token_b.transferFrom(msg.sender, address(this), _b);
        k+=_b;
    }    

    // 유동성 제거 -> 임의로 (+ 추후 LP 토큰 해제하는 방식으로 가격을 맞추어서)
    function removeRequidity(uint _a, uint _b) public {
        subA(_a);
        subB(_b);
    }

    function subA(uint _a) public {
        token_a.transfer(msg.sender,  _a);
        k-=_a;
    }

    function subB(uint _b) public {
        token_b.transfer(msg.sender,  _b);
        k-=_b;
    }    

    // swap -> (CSMM으로(a+b=k) + 추후 CPMM으로)
    function swapA(uint _a) public { //일단 a코인을 넣는걸로 고정
        //y-y' = k-x - (k-x-a')  ==> a'
        // 1. 사람이 B에게 A토큰을 _a만큼 준다
        // 2. B는 얼만큼 내어줄지 계산
        // 3. 2에서 계산한 값만큼 B는 사람에게 B토큰 전송

        uint current_k = k; //현재 k를 저장
        addA(_a);
        uint out = k-current_k; //결국 == _a
        
        //contract B가 사람한테 B토큰 내어주기
        token_b.transfer(msg.sender, out);
        k=k-out;
    }

    function getName() public view returns(string memory, string memory) {
        return (token_a.name(), token_b.name());
    }

    function checkBalance() public view returns(uint) {
        return msg.sender.balance;
    }

    //check token blanace
    function checkTokenBalance() public view returns(uint, uint) {
        return (token_a.balanceOf(address(this)), token_b.balanceOf(address(this)));
    }
    
    function deposit() public payable {}

    receive() external payable {}
}
profile
개발 취준생

0개의 댓글