Incentive Community Project

Hong·2023년 1월 13일
0
post-thumbnail







😪 잠만보와 털보

잠이 많은 팀원 두명, 수염기르는 팀원 두명해서 팀명이 잠만보와 털보로 지어졌다.
그래서 프로젝트에 사용된 Token이름도 SnorLaxToken이다.
2022년 초에 미국간다고 길렀던걸 어쩌다보니 아직도 기르고 있는데 한국서 취업하게되면 자르겠지..




👨‍💻 Role and Responsibility

🐬 SnorLax 1
- Design
- Front-end

👑 SnorLax 2 (Leader)
- Back-end

🧸 Beard 1
- Back-end

🧟 Beard 2 (Me)
- Smart Contract
- web3.js

지난 프로젝트에서 Front-endDesign만 맡아서 아쉬웠는데 이번에는 Back-end와 많이 소통할 수 있는 Smart Contract, Web3.js methods 작성의 역할을 맡아서 기뻤다. 그리고 재밌게 했다.
이번에도 정말 좋은 팀원들을 만나 무리없이 프로젝트를 진행할 수 있어 감사한 마음 뿐이다.





💨 ABOUT PROJECT AND STRUCTUAL LIMITATIONS

ABOUT PROJECT(프로젝트에 관하여)

SteemIt과 같은 블록체인 기반 SNS플랫폼은 전통적인 비즈니스에서는 불가능 했었던 사업모델을 가능하게 만들었다.

기존 SNS플랫폼(특히 블로그)의 수익모델은 2가지였다. 광고 혹은 유료화다. 하지만 SteemIt은 이 두가지 없이 컨텐츠 창작자에게 보상을 주는 비즈니스 모델을 만들어냈다.
전통적 플랫폼과 달리 게시글 작성자가 광고를 달지 않아도 수익을 가져갈 수 있는 구조를 만들어냈기 때문에 창작자가 광고주의 눈치를 보지 않고 진실성 있는 컨텐츠가 제작가능하게 되었다. 또한 WEB3의 철학을 위해 블록체인 네트워크 구조를 접목시켰기 때문에 중앙화된 운영자들이 독점적으로 가져가던 수익의 비중을 컨텐츠 생산자에게 많이 돌려줬다. 이러한 장점 때문에 SteemIt은 혁신적인 플랫폼이라 불렸다.

하지만 시간이 지날수록 SteemIt의 token economy와 기획구조는 허점이 가득했음이 들어났다.

나는 이러한 SteemIt과 같은 블록체인기반 SNS 앱을 직접 만들어봄으로써 SteemIt이 가지고 있는 새로운 플랫폼 구조의 혁신성을 이해하는 한편 SteemIt이 만들어낸 토큰 이코노미의 문제점을 파악하기 위해 프로젝트를 진행했다
(token economy를 제외하고서도 voting bot등과 같은 문제가 많았지만 아무래도 경제생태계 구조적인 측면의 문제가 가장 눈에 밟혔다).


STRUCTUAL LIMITATIONS(구조적 문제점)

커뮤니티 활동에 대한 보상의 수요가 실질적이지 않았다.

SteemIt은 커뮤니티 활동에 대한 보상으로 SteemIt이 발행한 token을 보상으로 지급했는데 이 token이 User들에게 실질적인 사용처를 제공하지 못했다.

우리는 지난 역사를 통해 지나친 양적완화를 통한 인플레이션과 수요없는 화폐에 대한 가치가 폭락한다는 것을 수차례 목격했다. SteemIt은 플랫폼 경제의 활성화를 위해 극심한 경기침체가 일어난 국가의 중앙은행처럼 사용자들에게 token을 계속해서 지급했다.
(실질적인 사용처, 즉 수요없는 token으로 매년 9.8%의 인플레율로 양적완화를 실행했으니 말다했다, SteemIt이 기축통화국이면 가능했을지도).

SteemIt token의 가장 큰 사용처는 커뮤니티 내에서 권력을 행사할 수 있는 Steem Power로 전환하는 것이었다(Steem Power를 많이 가진 사람에게 vote를 받은 게시글은 더 많은 보상을 받게 된다).
물론 높은 Steem Power를 가진 User가 질높은 컨텐츠 생산자에게 Vote할 수도 있지만, 몇몇 Steem Power고래들은 그들의 인맥들과 담합하기 시작했고 작성된 컨텐츠의 퀄리티와 관계없이 서로 투표를 주고 받기를 반복했다.
이러한 담합유인을 억제하지 못하는 구조적 문제점도 있지만 담합을 통해 얻은 Steem Token의 소비처가 마땅하지 않았기 때문에 서버에서 배포한 막대한 양의 Token들은 다시 User들의 Steem Power를 강화하는데 쓰였고 커뮤니티는 기형적인 구조로 변해갔다.
결국 Steem Token을 가지고 할 수 있는 일은 Steem Token을 더 많이 버는 일뿐이었다.

간단하게 Steem Token거래소에서 현금으로 교환하면 되지 않느냐는 생각을 할 수 있지만 이것은 SteemIt Token의 공급적 측면만 고려했을 때 이뤄질 수 있는 환상이다.
거래가 이뤄지기 위해서는 수요와 공급이 동시에 존재해야한다. SteemIt Token을 사려는 사람(수요)이 존재하지 않았기 때문에(SteemIt Token을 가지고 커뮤니티에 좋아요와 게시글을 작성하는 것 외에 사용처가 없었음으로) 단기적으로는 Pyramid Scheme처럼 token의 가격이 올라가고 현금과 교환이 가능했을지언정 그것이 오래 지속될 수는 없었다.
이것이 SteemIt이라는 가장 큰 Blockchain기반 SNS 커뮤니티가 가진 근본적 문제점이다.
현재도 서비스를 진행하며 하드포크 등으로 토큰 이코노미를 정상화 시킬려하는 노력을 하고 있다는데 지켜봐야할 것 같다.


느낀점

기존 플랫폼 기업의 비즈니스 모델에서 web3의 가치를 실현하기 위해 블록체인 기술을 활용하면 새로운 수익구조의 비즈니스 모델이 탄생할 수 있다는 것을 깨달았다.

하지만 이러한 혁신적인 비즈니스 모델을 구현하기 위해 프로그래밍적 기술이 중요하기도 하지만 개발 전 기획단계에서 생태계 모델을 치밀하게 기획하는 것 역시 너무나 중요한 일이라는 것을 알게 되었다.
또한 직접 프로젝트를 만들고 구조를 이해하며 느꼈는데 대부분의 기업에서 출시하는 서비스에 '탈중앙화'가 붙는다면 이것은 완벽한 탈중앙화가 아니라 '(기존의 서비스보다 조금 더) 탈중앙화'로 이해하는 것이 바람직하다는 것이다. 아직까지는 기술적 한계 때문에 모든 데이터를 블록체인 서버에 올리기는 무리가 있다(중앙화된 DB등과 블록체인 네트워크를 혼합해서 사용해야한다).





🏅 WHAT I GOT FROM THE PROJECT

  • 블록체인기반 SNS의 아키텍쳐 이해

  • 블록체인기반 SNS의 한계점

  • Web3.js를 이용해 Solidity Smart Contract의 활용





📝 프로젝트 요구사항

  • 커뮤니티에 게시글을 작성할 수 있고, 이에 따른 보상으로 ERC-20 token을 지급하는 기능
    User가 게시글을 작성하면 back-end에서 서버계정이 들고 있는 token을 User에게 전송해줌
  • ERC-20 token을 소비해서 ERC-721 NFT를 발행할 수 있는 기능
    • ERC-20의 경우, 사용자와 사용자 간의 교환도 가능해야함
      client에서 메타마스크를 사용하지 않고 요청을 보내면 back-end 서버에서 web3.js를 통해 블록체인 네트워크와 통신하고 transaction을 처리함
  • 회원가입시 자동으로 지갑 주소를 부여받는 기능
    User는 id, password, nickname정도만 입력하면 back-end 서버에서 지갑주소와 private key를 생성하고 DB에 저장함
  • MyPage에서 내가 소유하고 있는 token의 갯수와 NFT, 내가 작성한 게시글을 볼 수 있음




😼 How I went thorogh the project

Smart Contract 작성의 역할을 맡은 내가 가장 먼저 해야할 일은 erc-20을 통해 발행한 token으로 erc-721 NFT를 minting하는 일이었다.
이를 위해서 누군가가 작성한 erc-20 코드를 참고하고 필요한 기능을 추가하기로 했는데..

🚨 문제발생_1-1

approve 함수를 테스트 하던 중 이상한 일이 발생했다.

내가 가진 돈 보다 많이 approve할 수 있다고?

    function approve(address spender, uint256 amount)
        external
        virtual
        override
        returns (bool)
    {
        uint256 currentAllownace = _allowances[msg.sender][spender];
        require(
            amount >= currentAllownace,
            "ERC20: Transfer amount exceeds allowance"
        );
        _approve(msg.sender, spender, currentAllownace, amount);
        return true;
    }

살펴보니 approve함수를 호출할 때 나의 balanceapprove하는 amount의 양을 체크해야하는데
(나의 balance보다 많은 approve amount를 부여할 수 없어야한다)
currentAllownaceapprove하는 amount의 양을 체크하고 있었다..
(allowance는 transferFrom 함수를 호출할 때 검사해야한다) 심지어 부등호도 반대였다.


🚨 문제발생_1-2

transferFrom함수를 실행해도 allowance가 차감되지 않네?

approve후에 allowance를 체크하고 transferFrom으로 owner로부터 buyer로 token을 전송했는데 allowance가 차감되지 않는 이슈가 발생했다.

참고했던 코드를 사용하길 포기하고 OpenZeppelin Library를 사용해서 문제가 있는 함수를 override로 작성해서 문제를 고쳐볼려했지만 오히려 에러만 늘어갔다.

그래서 그냥 ERC-20을 직접 작성하기로 했다
(누군가가 사용한 erc-20, erc-721을 그대로 가져올 경우 해당 코드를 사용한 사람도 이상한 코드를 그대로 사용했을 가능성이 있기 때문에 주의를 기울여야 한다는 사실을 알게되었다. 다들 배포하신 프로젝트 DApp잘 돌아가시는거 맞습니까..)


😎 Solution

approve allowance, transferFrom부분이 잘 작동하도록 contract를 작성했고 새로작성하는 erc-20 contract의 기본적인 함수들과 함께 SafeMath(입력값 overflow를 막기 위함)와 OwnerHelper(함수호출 사용자 권한 부여를 위함) 라이브러리를 적용했다.
그리고 User들의 token전송을 통제할 수 있는 tokenLock기능을 추가했다.

배포하고 테스트해본 결과 잘 작동한다.

//approve, allowance, tokenLock부분만 보일 수 있도록 일부분은 생략했다.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

interface ERC20Interface {
  ...(생략)
}

library SafeMath {

  ...(생략)
}

abstract contract OwnerHelper {
  ...(생략)
}

contract SnorLaxToken is ERC20Interface, OwnerHelper {
  ...(생략)

    function allowance(address owner, address spender)
        external
        view
        override
        returns (uint256)
    {
        return _allowances[owner][spender];
    }

    function approve(address spender, uint256 amount)
        external
        virtual
        override
        returns (bool)
    {
        uint256 currentAllowance = _allowances[msg.sender][spender];
        require(
            _balances[msg.sender] >= amount,
            "ERC20: The amount to be transferred exceeds the amount of tokens held by the owner."
        );
        _approve(msg.sender, spender, currentAllowance, amount);
        return true;
    }

    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external virtual override returns (bool) {
        _transfer(sender, recipient, amount);
        emit Transfer(msg.sender, sender, recipient, amount);
        uint256 currentAllowance = _allowances[sender][msg.sender];
        require(
            currentAllowance >= amount,
            "ERC20: transfer amount exceeds allowance"
        );
        _approve(
            sender,
            msg.sender,
            currentAllowance,
            currentAllowance - amount
        );
        return true;
    }

  ...(생략)
  
    function isTokenLock(address from, address to)
        public
        view
        returns (bool lock)
    {
        lock = false;

        if (_tokenLock == true) {
            lock = true;
        }

        if (
            _personalTokenLock[from] == true || _personalTokenLock[to] == true
        ) {
            lock = true;
        }
    }

    function removeTokenLock() public onlyOwner {
        require(_tokenLock == true);
        _tokenLock = false;
    }

    function removePersonalTokenLock(address _who) public onlyOwner {
        require(_personalTokenLock[_who] == true);
        _personalTokenLock[_who] = false;
    }

    function assignTokenLock() public onlyOwner {
        require(_tokenLock == false);
        _tokenLock = true;
    }

    function assignPersonalTokenLock(address _who) public onlyOwner {
        require(_personalTokenLock[_who] == false);
        _personalTokenLock[_who] = true;
    }

    function _approve(
        address owner,
        address spender,
        uint256 currentAmount,
        uint256 amount
    ) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");
        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, currentAmount, amount);
    }
}




🚨 문제발생_2

Smart Contract 함수를 실행하는 주체설정

erc-20 erc-721을 연동해서 erc-20 token으로 nft를 발행할때(mintNFT 함수호출)
erc-20 token holder(User)가 erc-721 CA에 본인의 token을 인출할 수 있는 권한을 approve함수를 통해 부여해야 한다.
하지만 처음에 User가 어느 계정에 approve를 해줘야하는지, mintNFT의 함수실행 주체는 누가 되는지의 구조 자체를 이해하지 못해서 꽤나 애먹었다.


😎 Solution

Solditiy code를 살펴보며 각 함수를 실핼할 때 어느 부분에 msg.sender가 들어가는지 파악하고 위와 같은 flowChart를 그리며 작동방식과 큰 틀의 구조를 이해하니 쉽게 개념이 잡혔다.





🚨 문제발생_3

nodejs local환경에서 web3.js를 이용해서 Ethereum BlockChain과 소통하기

MetaMask가 존재하고 이것을 활용해서 transaction을 sign하고 거래를 만들 수 있으면 서버 측에서 web3.js를 사용할 때 크게 복잡한 일이 없다.

하지만 우리가 기획했던 프로젝트는 MetaMask를 사용하지 않아야했다. MetaMask계정 대신 회원가입시 User정보와 매칭되는 지갑주소를 web3.js로 생성해 DB에 저장하는 구조였다.

때문에 서버에서 web3.js method에 알맞는 transaction object를 만든다음 sign하고 transaction을 일으켜야 했다
(send method에서만 transaction일으키면 되고, call method는 transaction object를 만들 필요가 없다).


local ganache환경에서 transaction에 대한 sign없이 거래를 일으킬 때

//transfer
async function transfer_erc20() {
  const accounts = await web3.eth.getAccounts();

  try {
    const transferResult = await erc20Contract.methods
      .transfer(accounts[1], '100')
      .send({ from: accounts[2] });

    console.log(transferResult);
    return transferResult;
  } catch (e) {
    console.log(e);
    return e;
  }
}

😎 Solution

transaction object

nonce : from주소에서 보낸 트랜잭션 수를 추적하는데 사용된다. replay공격을 방지하기 위해 필요하다. web3.eth.getTransactionCount(트랜잭션 생성 주소) 를 사용하면 쉽게 nonce를 얻어낼 수 있다.
from : 트랜잭션을 발생시키는 주소
to : 트랜잭션을 보낼 주소(contract의 method를 요청한다면 contract CA주소)
value : 보낼 ether의 양이다. wei로 표현되어야하기 때문에 toWei로 변환해주면 편하다.
gasPrice : 21000이 트랜잭션을 수행하는데 최소로 드는 gas의 양이기 때문에 30000정도를 넣어준다
gasLimit : 블록체인 네트워크 상태에 따라 내가 내야할 gas의 양이 높아질 수 있는데, 이것에 대한 상한을 건다.
data : optional이다. 트랜잭션과 함께 특정 메세지를 포함시키거나, 특정 contract의 method를 불러올 수 있다.

Ethereum Blockchain과 직접 소통할 때

//transfer
async function transfer_erc20() {
  var txObj = {
    nonce: web3.eth.getTransactionCount(ownerAccount.address),
    gasPrice: web3.eth.gasPrice,
    gasLimit: 1000000,
    to: erc20ContractAddr,
    from: ownerAccount.address,
    value: '',
    data: erc20Contract.methods.transfer(user2Account, '200').encodeABI(),
  };

  try {
    const signedTx = await web3.eth.accounts.signTransaction(
      txObj,
      PRIVATE_KEY,
    );
    const transferResult = await web3.eth.sendSignedTransaction(
      signedTx.rawTransaction,
    );

    console.log(transferResult);
    return transferResult;
  } catch (e) {
    console.log(e);
    return e;
  }
}

Reference





🚨 문제발생_4

mintNFT함수를 실행하면 해당 함수를 호출한 User계정에서 token 50개가 빠져나가야 했다.
하지만 mintNFT함수를 호출하면 transaction도 정상적으로 일어나고 NFT도 정상적으로 발행되지만 User계정에서 token이 빠져나가지 않는 현상이 일어났다.

살펴보니 mintNFT함수는 내부적으로 transferFrom함수를 작동시키는데 transferFrom의 두번째 인자(address recipient)가 msg.sender로 설정되어 있었다.
때문에 mintNFT함수를 호출한 User의 계정으로 다시 token이 들어왔고 balance에 변화가 없었던 것이다.

이 문제는 간단하게 mintNFT함수의 인자를 3개로 만들고 내부적으로 실행되는 transferFrom함수의 두번째 인자를 msg.sender에서 token을 받아야하는 서버계정으로 변경해줬다.


transferFrom함수

function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) {
	...
}

😎 Solution

mintNFT함수

    function mintNFT(
        address recipient,
        address tokenRecipient,
        string memory tokenURI
        /*
        was
        address recipient,
        string memory tokenURI
        */
    ) public returns (uint256) {
        require(token.balanceOf(recipient) > nftPrice);

        token.transferFrom(recipient, tokenRecipient, nftPrice); 
        //was token.transferFrom(recipient, msg.sender, nftPrice);

        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);
        getToken[tokenURI] = newItemId;

        return newItemId;
    }




😴 Project App

Main Page

Main Page에서 게시글을 작성할 수 있고 게시글을 작성하거나 일정 갯수 이상의 좋아요를 받으면 커뮤니티 활동에 대한 보상으로 token을 지급받는다.



SignUp Page

사용할 ID, Password, Nickname을 입력하면 서버에서 해당 정보를 post받아 지갑계정을 생성하고 이것을 DB에 저장한다.



LogIn Page

User가 회원가입시 입력했던 IDPassword를 입력해서 LogIn한다.
로그인이 되면 MyPage버튼이 생성된다.



MyPage

MyPage에서는 커뮤니티에서 보상으로 지급되는 Token을 후원하고자하는 특정 User에게 transfer할 수 있다.
그리고 지급받은 token을 사용해서 NFT를 민팅할 수 있다 (이 과정에서 token이 소모됨).
또한 내가 보유한 NFT는 나의 Profile을 변경하는데 사용될 수 있다!





🙍‍♂️ User Flow





📜 Contract Doc

Notion Link

profile
Notorious

0개의 댓글