잠이 많은 팀원 두명, 수염기르는 팀원 두명해서 팀명이 잠만보와 털보로 지어졌다.
그래서 프로젝트에 사용된 Token이름도 SnorLaxToken이다.
2022년 초에 미국간다고 길렀던걸 어쩌다보니 아직도 기르고 있는데 한국서 취업하게되면 자르겠지..
🐬 SnorLax 1
- Design
- Front-end
👑 SnorLax 2 (Leader)
- Back-end
🧸 Beard 1
- Back-end
🧟 Beard 2 (Me)
- Smart Contract
- web3.js
지난 프로젝트에서
Front-end
와Design
만 맡아서 아쉬웠는데 이번에는Back-end
와 많이 소통할 수 있는Smart Contract
,Web3.js methods
작성의 역할을 맡아서 기뻤다. 그리고 재밌게 했다.
이번에도 정말 좋은 팀원들을 만나 무리없이 프로젝트를 진행할 수 있어 감사한 마음 뿐이다.
SteemIt
과 같은 블록체인 기반 SNS플랫폼은 전통적인 비즈니스에서는 불가능 했었던 사업모델을 가능하게 만들었다.기존 SNS플랫폼(특히 블로그)의 수익모델은 2가지였다. 광고 혹은 유료화다. 하지만
SteemIt
은 이 두가지 없이 컨텐츠 창작자에게 보상을 주는 비즈니스 모델을 만들어냈다.
전통적 플랫폼과 달리 게시글 작성자가 광고를 달지 않아도 수익을 가져갈 수 있는 구조를 만들어냈기 때문에 창작자가 광고주의 눈치를 보지 않고 진실성 있는 컨텐츠가 제작가능하게 되었다. 또한 WEB3의 철학을 위해 블록체인 네트워크 구조를 접목시켰기 때문에 중앙화된 운영자들이 독점적으로 가져가던 수익의 비중을 컨텐츠 생산자에게 많이 돌려줬다. 이러한 장점 때문에 SteemIt은 혁신적인 플랫폼이라 불렸다.하지만 시간이 지날수록
SteemIt
의 token economy와 기획구조는 허점이 가득했음이 들어났다.나는 이러한
SteemIt
과 같은블록체인기반 SNS 앱을 직접 만들어봄
으로써 SteemIt이 가지고 있는 새로운 플랫폼 구조의혁신성을 이해
하는 한편SteemIt이 만들어낸 토큰 이코노미의 문제점을 파악
하기 위해 프로젝트를 진행했다
(token economy를 제외하고서도 voting bot등과 같은 문제가 많았지만 아무래도 경제생태계 구조적인 측면의 문제가 가장 눈에 밟혔다).
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등과 블록체인 네트워크를 혼합해서 사용해야한다).
블록체인기반 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, 내가 작성한 게시글을 볼 수 있음
Smart Contract
작성의 역할을 맡은 내가 가장 먼저 해야할 일은erc-20
을 통해 발행한 token으로erc-721
NFT를 minting하는 일이었다.
이를 위해서 누군가가 작성한 erc-20 코드를 참고하고 필요한 기능을 추가하기로 했는데..
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함수
를 호출할 때 나의balance
와approve하는 amount
의 양을 체크해야하는데
(나의 balance보다 많은 approve amount를 부여할 수 없어야한다)
currentAllownace
와approve하는 amount
의 양을 체크하고 있었다..
(allowance는 transferFrom 함수를 호출할 때 검사해야한다) 심지어 부등호도 반대였다.
approve후에
allowance를 체크하고transferFrom
으로 owner로부터 buyer로 token을 전송했는데 allowance가 차감되지 않는 이슈가 발생했다.참고했던 코드를 사용하길 포기하고
OpenZeppelin Library
를 사용해서 문제가 있는 함수를 override로 작성해서 문제를 고쳐볼려했지만 오히려 에러만 늘어갔다.그래서 그냥
ERC-20을 직접 작성
하기로 했다
(누군가가 사용한 erc-20, erc-721을 그대로 가져올 경우 해당 코드를 사용한 사람도 이상한 코드를 그대로 사용했을 가능성이 있기 때문에 주의를 기울여야 한다는 사실을 알게되었다. 다들 배포하신 프로젝트 DApp잘 돌아가시는거 맞습니까..)
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);
}
}
erc-20
erc-721
을 연동해서 erc-20 token으로 nft를 발행할때(mintNFT 함수호출)
erc-20 token holder(User)가 erc-721 CA에 본인의 token을 인출할 수 있는 권한을 approve함수를 통해 부여해야 한다.
하지만 처음에 User가 어느 계정에 approve를 해줘야하는지, mintNFT의 함수실행 주체는 누가 되는지의 구조 자체를 이해하지 못해서 꽤나 애먹었다.
Solditiy code를 살펴보며 각 함수를 실핼할 때 어느 부분에
msg.sender
가 들어가는지 파악하고 위와 같은 flowChart를 그리며 작동방식과 큰 틀의 구조를 이해하니 쉽게 개념이 잡혔다.
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를 만들 필요가 없다).
//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;
}
}
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를 불러올 수 있다.
//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;
}
}
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을 받아야하는 서버계정으로 변경해줬다.
function transferFrom(address sender, address recipient, uint256 amount) external virtual override returns (bool) {
...
}
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;
}
Main Page
에서 게시글을 작성할 수 있고 게시글을 작성하거나 일정 갯수 이상의 좋아요를 받으면 커뮤니티 활동에 대한보상으로 token을 지급
받는다.
사용할
ID
,Password
,Nickname
을 입력하면 서버에서 해당 정보를 post받아지갑계정을 생성
하고 이것을DB에 저장
한다.
User가 회원가입시 입력했던
ID
와Password
를 입력해서LogIn
한다.
로그인이 되면 MyPage버튼이 생성된다.
MyPage
에서는 커뮤니티에서 보상으로 지급되는Token
을 후원하고자하는특정 User에게 transfer
할 수 있다.
그리고 지급받은token
을 사용해서NFT를 민팅
할 수 있다 (이 과정에서 token이 소모됨).
또한 내가 보유한 NFT는 나의Profile을 변경
하는데 사용될 수 있다!