[TIL] Part4 - DAY65 Project - CPMM, event emit, ERC20토큰으로 NFT 민팅

박동우·2023년 6월 28일

블록체인 스쿨 3기

목록 보기
61/72

[TIL] 2023-06-27 DAY65


Project

GIT 주소
https://github.com/dowoo303/SOLIDITY
https://github.com/dowoo303/hardhat


오늘 배운것들

  1. CPMM 실습
  2. event emit
  3. ERC20 토큰으로 NFT 민팅해보기(with react)

각종 팁

  1. 가스비 관련
    1) 단일매핑 2번보다는 이중매핑이 싸다
    2) 최대한 상태변수 쓰지말기
    3) 길이가 정해진 배열 사용하기(동적 어레이와 정적 어레이의 가스비 차이는 꽤 난다)
    4) modifier은 유무에 따른 가스비 차이는 거의 없음

회고

  1. react에서 컨트랙트와 함수명 일치하는지 확인하기




💻 Solidity 코딩

✅ CPMM 실습

x(a토큰갯수) x y(b토큰갯수) = k 일 때 코인 거래가 일어난다고 k값은 변화하지 않음 -> liquidity pool 변동이 일어날 때만 변화가 일어난다.

msg.sender는 컨트랙트가 될 수도 있지만 tx.origin은 무조건 사용자이다.

@openzeppelin/contracts/token/ERC20/ERC20.sol 형태로 받아온다면 npm사이트에서 코드를 봐야함.
https://www.npmjs.com/package/@openzeppelin/contracts?activeTab=code

transferFrom는 _spendAllowance가 있기 때문에 approve가 필요하다. -> cpmm 주소를 가지고 a,b토큰 컨트랙트로 가서 approve -> A토큰 주소를 가지고 가서 cpmm에서 풀 제공


🟥 코드

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;

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

contract Token_A is ERC20("A_token", "A") {
    constructor(uint a) {
        _mint(msg.sender, a);
    }

     function decimals() public pure override returns(uint8) {
        return 0;
    }
}

contract Token_B is ERC20("B_token", "B") {
    constructor(uint a) {
        _mint(msg.sender, a);
    }

     function decimals() public pure override returns(uint8) {
        return 0;
    }
}



contract CPMM is ERC20("ABLP_Token", "ABLP") {
    address owner;
    ERC20 public token_a;
    ERC20 public token_b;

    constructor(address _a, address _b) {
        owner = msg.sender;
        token_a = ERC20(_a);
        token_b = ERC20(_b);
    }

    function burn(uint _amount) public {
        _burn(msg.sender, _amount);
    }

    // x(a토큰의 수량)*y(b토큰의 수량)=k -> CPMM
    //uint public k;    

    modifier isitAorB(address _token) {
        require(_token == address(token_a) || _token == address(token_b));
        _;
    }

    // 토큰 가격 결정을 위하여 컨트랙트가 들고 있는 갯수 구하기 -> 토큰 A, B의 컨트랙트에서 가져오기
    function balanceOfAB() public view returns(uint, uint) {
        return (token_a.balanceOf(address(this)), token_b.balanceOf(address(this)));
    }

    // 유동성 공급
    // 초기 유동성 공급
    function initialSupply() public {
        require(owner == msg.sender);
        token_a.transferFrom(tx.origin, address(this), 100);
        token_b.transferFrom(tx.origin, address(this), 300);

        _mint(msg.sender, 50);
    }


    // A토큰만 공급한다고 가정
    function provideLiquidity(address _token, uint _a) public isitAorB(_token) {
        (uint bal_a, uint bal_b) = balanceOfAB();
        uint mint_lp;

        if(_token == address(token_a)) {
            // A토큰 기준 -> B토큰 = 사용자가 바꾸려는 A토큰 갯수 * B토큰 갯수 / A토큰 갯수
            uint _b = _a * bal_b / bal_a;

            token_a.transferFrom(tx.origin, address(this), _a);
            token_b.transferFrom(tx.origin, address(this), _b);

            // lp 토큰 지급 -> 사용자가 넣은 A토큰 갯수 * 현재 pool의 k갯수(LP토큰) / 현재 이 pool의 A토큰 갯수 
            mint_lp = _a * totalSupply() / bal_a;
        } else {
            uint _b = _a * bal_a / bal_b;

            token_a.transferFrom(tx.origin, address(this), _b);
            token_b.transferFrom(tx.origin, address(this), _a);

            mint_lp = _a * totalSupply() / bal_b;
        }

        _mint(msg.sender, mint_lp);
        // k = (bal_a + _a) * (bal_b + b);
    }

    // 유동성 제거
    function withdrawLiquidity(uint _a) public {
        // _a : 사용자가 빼고 싶은 LP토큰의 양
        require(balanceOf(msg.sender) >= _a, "not enough");
        (uint bal_a, uint bal_b) = balanceOfAB();

        uint a_out = _a * bal_a / totalSupply();
        uint b_out = _a * bal_b / totalSupply();

        burn(_a);       // LP 토큰 소각

        token_a.transfer(msg.sender, a_out);
        token_b.transfer(msg.sender, b_out);
        // k = (bal_a - _a) * (bal_b - _b);
    }


    // 거래
    function swapToken(address _token, uint _amount) public isitAorB(_token) {
        (uint bal_a, uint bal_b) = balanceOfAB();
        uint k = bal_a * bal_b;

        
        if(_token == address(token_a)) {
            // out = b-b` ->  b` = a*b/(a+a`)=k/(a+a`)
            uint out = bal_b - k / (bal_a + _amount);
            token_a.transferFrom(tx.origin, address(this), _amount);
            token_b.transfer(msg.sender, out);
        } else {
            uint out = bal_a - k / (bal_b + _amount);
            token_b.transferFrom(tx.origin, address(this), _amount);
            token_a.transfer(msg.sender, out);
        }
    }

    // 모인 수수료(보상) 지급
    function ratio() public view returns(uint) {
        uint _ratio = balanceOf(msg.sender) * 100 / totalSupply();
        // uint[2] memory _rewards = [fees[0] * ratio / 100, fees[1] * ratio/100];
        return _ratio;
    }

}


✅ Event emit

이벤트 emit을 남기는 코드 구성 후 터미널에서 web3를 이용하여 트랜잭션 일으키고 filter로 특정 트랜잭션만 걸러내보기

트랜잭션 터미널(이더리움 테스트넷)

var tx = {from : account.address, to : c_addr, gas : 300000, gasPrice : 300000000, data : contract.methods.event_1(1, 'a').encodeABI()}

var signPromise = web3.eth.accounts.signTransaction(tx, account.privateKey)

signPromise.then((signedTx) => {var sentTx = web3.eth.sendSignedTransaction(signedTx.raw || signedTx.rawTransaction); 
sentTx.on("receipt", receipt => {console.log(receipt.logs)})})

web3.utils.sha3('이벤트이름(input값형태1,input값형태2)')

-filter 써보기
var tx = {from : account.address, to : c_addr, gas : 300000, gasPrice : 300000000, data : contract.methods.event_1(2, 'abc').encodeABI()}

var signPromise = web3.eth.accounts.signTransaction(tx, account.privateKey)

signPromise.then((signedTx) => {var sentTx = web3.eth.sendSignedTransaction(signedTx.raw || signedTx.rawTransaction); sentTx.on("receipt", receipt => {console.log(receipt.logs)})})


- 특정값만 조회해보기
async function getEvents() {var a = await contract.getPastEvents('index_1', {filter : {_num : [3,4]}, fromBlock : 0, toBlock : 'latest'}); console.log(a)}

async function getEvents() {var a = await contract.getPastEvents('index_1', {filter : {_num : [3,4]}, fromBlock : 0, toBlock : 'latest'}); console.log(a[0].returnValues)}
-> 맨 뒤 console.log(a[0]) 쪽 건드리기


웹소켓 터미널에서는 위와 컨트랙트 만드는것까지는 같음(웹소켓)

contract.events.index_1(()=>{}).on('data', function(event){console.log(event)})

await contract.getPastEvents('index_1', {fromBlock : 0, toBlock : 'latest'}, (err,events) => {console.log(events);});
-> 이 컨트랙트 내 모든 트랜잭션 읽어옴

await contract.getPastEvents('index_1', {fromBlock : 9247980, toBlock : 'latest'}, (err,events) => {console.log(events);});
-> fromBlock과 toBlock을 변경하여 원하는 기간동안의 정보만 받아 올 수도 있음

- filter 써보기
await contract.getPastEvents('index_1', {filter : {_num : [2]}, fromBlock : 0, toBlock : 'latest'}, (err,events) => {console.log(events);});
-> _num이 2인 것들만 filter해서 표시해줌


🟥 코드

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;

contract Indexed {
    event index_1(uint indexed _num, string _name);

    function event_1(uint _n, string memory _s) public {
        emit index_1(_n, _s);
    }
}


✅ ERC20 토큰으로 NFT 민팅

위 CPMM에서 했던 방식과 유사하다. -> approve도 현재 react에서 구현한 것이 아닌 remix에서 가서 해준 후 NFT 민팅을 진행했다.


🟥 코드

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

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


contract Changeable is ERC721Enumerable {
    string public URI;

    ERC20 public token_a;

    function setToken_A(address _a) public {
        token_a = ERC20(_a);
    }

    constructor(string memory _uri, address _a) ERC721("nft721", "n") {
        URI = _uri;
        token_a = ERC20(_a);
    }

    // ERC721이 받음
    function mintNFT(uint _ID) public payable {
        require(msg.value == 0.001 ether);
        _mint(msg.sender, _ID);            
    }

    // A토큰으로 민팅하기 -> ERC20(token_a)이 받음
    function mintNFTwithERC20(uint _ID) public {
        token_a.transferFrom(tx.origin, address(this), 1000);
        _mint(msg.sender, _ID);            
    }

    function tokenURI(uint _tokenID) public override view returns(string memory) {
        return string(abi.encodePacked(URI, '/', Strings.toString(_tokenID), '.json'));     // 공통적인 부분 /, 숫자, .json 붙여주기
    }
}
profile
HELLO!

0개의 댓글