NFT (Non-Fungible Token)
대체 불가능 토큰
1. NFT가 유일한 이유?
NFT가 유일한 이유는 블록체인 기술을 사용하여 각각의 토큰에
고유한 식별자(CA)를 부여하기 때문입니다
간단히 비유를 들자면 객체와 마찬가지입니다
{} !== {} // true
즉 유일하다는 말의 의미는 같은 모양의 객체일지다라도 참조값이 다르다는 것
말장난이라는 생각도 들지만...
2. 어떻게 이미지가 보이는걸까?
사실 NFT의 컨트랙트에는 'URL'만이 저장됩니다
실제 파일 데이터를 저장하는 서버는 따로 있습니다
그러니 이미지든 음악이든 영상이든 어떠한 유형의 데이터도 NFT가 될 수 있습니다
그런데 파일을 저장하는 서버는 블록체인 네트워크가 아닙니다
그렇다면 특정 서버에 파일을 저장한다면 그것을 탈중앙화라고 부를 수 있을까요?
또 만약 그 서버에 문제가 생긴다면요?
네, 실제로 이런 지적들이 있습니다. 파일을 저장한 서버가 중앙화 서버인 경우,
해당 서버에 문제가 생긴다면 NFT 소유자는 해당 파일에 접근할 수 없게 되겠죠
이는 중앙화된 시스템의 한계를 그대로 따른다는 것을 의미합니다
이러한 이유로 여러 NFT 플랫폼에서는 분산 파일 시스템을 활용해서 데이터를 보관하는 방법을 제시하고 있습니다
IPFS (InterPlanetary File System)
= 분산 파일 시스템
IPFS는 데이터를 중앙화된 서버가 아닌 여러 컴퓨터에 분산 저장하는 기술입니다
(https://www.pinata.cloud/)
↑ ipfs 서비스를 제공하는 대표적인 플랫폼
일단 NFT의 개요에 대한 설명은 여기까지만...
지난 번에 이어서 이번엔 라이브러리를 활용해서 ERC20 토큰을 발행해보겠습니다
OpenZeppelin
OpenZeppelin
은 ERC 표준을 준수하는 스마트 컨트랙트를 쉽게 작성할 수 있도록 돕는
높은 신뢰성을 지닌 라이브러리입니다
설치
npm install @openzeppelin/contracts
설치시 node_modules
안에 낯익은 디렉토리 구조가 형성됩니다
이제 솔리디티로 해야할 것을 정리하자면
import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol"
contract ERC20 is Context, IERC20
ERC20.sol
파헤치기
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 = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount);
emit Transfer(address(0), account, amount);
}
↑ mint
함수의 구조
ERC20.sol
파일)string private _name
...
function name() public view virtual override returns (string memory) {
return _name;
}
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);
}
transfer
함수를 실행할 때는 트랜잭션 메세지의 sender
(from
절)를 owner로 가져와서
_transfer
함수를 실행합니다
(아마도 송금자의 어드레스를 숨기기 위한 보안적이 방편으로 보이는데,
송금 기능과 관련한 대부분의 함수가 이러한 2중 구조로 만들어져 있습니다)
그리고 결과적으로는 누가 누구에게 얼마만큼을 보냈는지가 네트워크에 전달되는 구조입니다
(emit Transfer(from, to, amount)
)
approve()
, allowance()
, transferFrom()
이 셋은 마켓 플레이스 구현에 필수적인 함수들입니다
approve(spender, amount)
spender
)가 사용자 대신 transfer
할 수 있는 권한을 부여하는 메서드 (대리 판매)amount
)을 필수 인자로 받습니다allowance(owner, spender)
amount
데이터를 확인하는 데 사용하는 메서드 (call
함수)transferFrom(from, to, amount)
transfer
기능을 수행할 수 있게끔 하는 메서드msgSender
=== spender
)이지만,owner
)의 어카운트를 전달받습니다이 함수가 발동하면 최종적으로 제3자의 대리 결제가 체결됩니다
그리고 이 대리 결제는 allowance()
로 확인되는 가용 금액(amount
)을 한도로 합니다
[myToken.sol]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
// ERC20() ~ 자바스크립트의 super()와 같습니다
constructor() ERC20("myToken", "MTK") {
_mint(msg.sender, 1000 * (10 ** 18)); // 배포자 계정을 대상으로 토큰 1000개 발행
}
}
추가로 코인 스왑을 위한 컨트랙트도 만들어보기로 합니다
이렇게 기능이 명확히 분리될 때는 코드를 따로 작성하는 것이 좋습니다 (객체지향 / 책임분리)
이 컨트랙트는 기준이 될 토큰의 CA값을 주입받아야 합니다
[EthSwap.sol]
// SPDX-License-Identifier: MIT
pragma solidity^0.8.0;
import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract EthSwap {
ERC20 public token;
// 인자인 _token은 CA값입니다
constructor(ERC20 _token) {
token = _token;
}
function get() public view returns(uint256) {
return token.totalSupply(); // 상속받은 함수입니다. 주입받은 CA에 대한 발행량을 반환
}
}
↓ 컨트랙트 파일(.sol
)이 여럿일 때의 migration 파일 작성 예재
const MyToken = artifacts.require("myToken");
const EthSwap = artifacts.require("EthSwap");
module.exports = async (deployed) => {
await deployed.deploy(MyToken) // 컨트랙트 배포
const token = await MyToken.deployed() // 인스턴스를 얻기 위해, === new wb3.eth.Contract(abi, CA)
await deployed.deploy(EthSwap, token.address) // 두번째 인자는 생성자 함수에 대한 의존성 주입 (CA)
const swap = await EthSwap.deployed()
console.log((await swap.get()).toString()) // 1000000000000000000000 (1000개)
}
// 리팩토링
module.exports = async (deployed) => {
await deployed.deploy(MyToken)
// const token = await MyToken.deployed() // === new wb3.eth.Contract(abi, CA)
// console.log(`toEqual:::`, MyToken.address, token.address)
await deployed.deploy(EthSwap, MyToken.address)
}
↓ EthSwap 컨트랙트 완성본
// SPDX-License-Identifier: MIT
pragma solidity^0.8.0;
import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract EthSwap {
ERC20 public token;
uint256 public rate = 100; // 교환비
constructor(ERC20 _token) {
token = _token;
}
function getToken() public view returns(address) {
return address(token); // 주입된 인자를 address 타입(CA)으로 형변환
}
// Tx 발동 {from: "EOA(sender)", to: "EthSwap(CA)", data: getSwapBalance} ERC20 밸런스값을 반환합니다
function getSwapBalance() public view returns(uint256) {
return token.balanceOf(msg.sender);
}
function getThisAddress() public view returns(address) {
return address(this); // 자바스크립트와 마찬가지로 this는 어카운트(인스턴스). 반환받을 값은 address이기 때문에 형변환이 필요합니다
}
function getTokenOwner() public view returns(address) {
return token.owner(); // 이 컨트랙트 배포자(EOA)의 어드레스를 반환받을 함수
}
function buyToken() public payable {
uint256 tokenAmount = msg.value * rate; // value는 wei 단위입니다
// 모든 발행량은 Token Owner(EOA)가 지니고 있기 때문에 EthSwap CA에 대한 권한 위임이 필요합니다(transferFrom)
// {from: "EOA(sender)", to: "EthSwap(CA)", value}
require(token.balanceOf(token.owner()) >= tokenAmount, "Error");
// 현재 token.balanceOf()는 1000. 즉 10ETH를 넘어가면 에러를 발생시키는 조건문입니다
token.transferFrom(token.owner(), msg.sender, tokenAmount); // buyToken 실행시 최종적으로 트랜잭션을 발동시킬 메서드
}
function sellToken(uint256 _amount) public payable {
require(token.balanceOf(msg.sender) >= _amount);
uint256 etherAmount = _amount/rate;
require(address(this).balance >= etherAmount);
token.approve2(msg.sender ,address(this), _amount); // Token CA(msg.sender) > Swap CA(address(this))에 권한 위임
token.transferFrom(msg.sender, token.owner2(), _amount);
payable(msg.sender).transfer(etherAmount);
}
}
EOA -> EthSwap(CA) -> MyToken(CA)
EOA가 사용자라면 스왑 컨트랜트는 프론트, 토큰 컨트랙트는 백이라고 볼 수도 있겠습니다
↓ 테스트 코드
const MyToken = artifacts.require("myToken");
const EthSwap = artifacts.require("EthSwap");
contract('EthSwap', ([deployer1, deployer2, account3, account4])=> {
describe("Account 확인", ()=> {
it("deployer", ()=> {
assert.equal(deployer1, "0x61dC3D704d307Ed8dC7ac9918657BD37EEED95D3")
assert.equal(deployer2, '0x1183Bf9e54b6Acf0DAb67ee0dc6E5F6b140092D8')
})
})
describe("token deployed", ()=>{
it("token", async () => {
console.log(MyToken.address) // CA ~ 0xe1EA6B87316b2ABE3cAaf370c07AbFbb459097Eb
})
})
describe("deployed", ()=> {
let token
let swap
it("배포 초기화", async () => {
token = await MyToken.deployed()
swap = await EthSwap.deployed()
})
it("토큰 배포자의 밸런스 확인", async ()=> {
const balance = await token.balanceOf(deployer1)
assert.equal(balance.toString(), 1000 * 10 ** 18)
})
it("buyToken", async ()=> {
const amount = await token.balanceOf(swap.address)
console.log(amount.toString()) // 0
const approve = await token.approve(swap.address, web3.utils.toWei("1000", "ether"))
console.log(approve)
const a = await swap.buyToken({from: account3 , to: swap.address, value: web3.utils.toWei("1", "ether")})
// console.log(a)
assert.equal(await swap.getTokenOwner(), await token.owner2())
console.log(await web3.eth.getBalance(account3))
console.log((await token.balanceOf(account3)).toString())
console.log(await web3.eth.getBalance(deployer1))
console.log((await token.balanceOf(deployer1)).toString())
console.log(await web3.eth.getBalance(swap.address))
// pass
// 90986257720000000000
// 100000000000000000000
// 98407029600000000000
// 900000000000000000000
// 1000000000000000000
})
it("sellToken", async () => {
const balance = await token.balanceOf(account3)
console.log(`token:`, balance.toString())
console.log(`eth:`, await web3.eth.getBalance(account3))
console.log('owner:', (await token.balanceOf(deployer1)).toString())
await swap.sellToken(balance, {
from: account3,
})
console.log(`token2:`, balance.toString())
console.log(`eth2:`, await web3.eth.getBalance(account3))
console.log('owner2:', (await token.balanceOf(deployer1)).toString())
// token: 100000000000000000000
// eth: 80960585160000000000
// owner: 900000000000000000000
// token2: 0
// eth2: 81959514880000000000
// owner2: 1000000000000000000000
})
})
})
코드 이해의 요점은 msg.sender가 누구냐를 파악하는 것