// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 오픈 재플린은 솔리디 기반의 스마트 컨트랙트를 개발할 때 사용하는 표준 프레임 워크
// 컨트랙트를 개발할 때 표준토큰 규약을 지키면서 안정성있고 빠르게 개발할 수 있다.
// 오픈 재플린에서 제공하는 ERC20 인터페이스
interface IERC20 {
// 토큰의 총 발행량을 조회할 수 있는 함수
// external과 public의 다른 점은 외부에서 접근이 가능한 접근 제한자라는 것이다.
// external은 외부의 EOA또는 CA에서 호출이 가능(즉, 외부에서 호출이 가능)
// 내부에서는 접근 불가
// public은 내부,외부 접근 다 가능
// 그럼 왜 쓰냐? => public보다 가스비가 더 저렴
// public은 함수의 변수를 메모리에 복사하고 사용을 하는데
// external 복사를 하지 않아요 내부에서 복사를 해서 메모리에 값을 사용하느냐 CALLDATA를 직접 읽어서 사용하느냐의 차이
function totalSupply() external view returns (uint);
// 전달받은 매개변수(계정 주소) 토큰의 잔액을 조회하는 함수
function balanceOf(address account) external view returns (uint);
// 계정에서 토큰을 다른 계정으로 전달하는 함수
function transfer(address to, uint amount) external returns (bool);
// 소유자의 위임받은 토큰을 관리하는 공간
function allowance(address owner, address spender) external returns (uint);
// {0x1111111111.... : 100,0x1111112223123....: 200}
// => {0x1111111111.... : 100,{0x1111112223123....: 50}}
// 소유권을 제 3자에게 위임하는 함수
function approve(address spender, uint amount) external returns (bool);
// 소유권을 관리하는 토큰을 보내는 함수, 위임받은 소유권이 있는지 확인을 하고 보내는 함수
function transferFrom(address spender,address to,uint amount) external returns (bool);
// event 함수를 작성해서 함수에서 실행을 시키면 이더스캔 같은 사이트에서 로그 확인을 할 수 있다.
// 트랜잭션 내용에 로그를 만들어서 확인할 수 있다.
// Transfer 함수를 실행했을 때 이벤트 함수를 실행시켜 발생하는 이벤트를 트랜잭션 로그에 확인 해볼수 있다.
event Transfer (address from, address to, uint value);
// approve함수가 실행 했을 때 이벤트 함수도 실행을 해서 로그를 확인해 볼 수 있다.
event Approval(address owner, address spender, uint value);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./IERC20.sol";
// class에서 인터페이스 상속을 한 것 처럼
// implements 처럼
// is라는 구문을 사용하여 상속
contract ERC20 is IERC20 {
// ERC20 토큰의 규약
// 토큰의 이름 풀네임
string public name;
// 토큰의 심볼(토큰의 단위를 표현) ETH등
string public symbol;
// 토큰의 소수점 자리 기본 소수점 자리는 10단위
uint8 public decimals = 18;
// 토큰의 총 발행량
// 이미 선언이 되어있는 함수를 interface 함수는 virtual 함수로 돼 있는데
// 기본 선언하면 virtual 함수임
// override 상속 받은 함수를 새로 정의해서 사용 다형성
uint public override totalSupply;
// 컨트랙트 배포자, 현재 버전엔 상속받아서 사용중 필요가 없기 때문에 없앤것
address private owner;
// 생성자 함수 컨트랙트 배포자가 실행을 할 함수
// memory영역에서 사용을 하고 해제 시킨다는 구문
// uint는 256 정해져있는 양을 사용함.
// 동적으로 변할 수 있는 변수에는 memory를 다 붙인다.
constructor(string memory _name, string memory _symbol, uint256 _amount) {
// 어떤 이름의 토큰을 발행하고 있고
// 어떤 단위 심볼을 사용할지
// 처음에 총발행량을 얼마나 설정할지
owner = msg.sender;
name = _name;
symbol = _symbol;
// 토큰발행할 때 사용할 메서드 작성
// _amount * (10 ** uint256(decimals))
// 최초 토큰 발행량
mint(_amount * (10 ** uint256(decimals)));
}
// balance 토큰을 누가 얼마만큼 가지고 있는지의 내용을 담을 객체
mapping(address => uint) public balance;
// 토큰의 권한을 위임 받은 내용이 들어있는 객체
mapping(address => mapping(address => uint)) public override allowance;
// 잔액을 조회하는 함수
function balanceOf(address account) external view override returns (uint) {
return balance[account];
}
// 다른 지갑으로 잔액을 전달하는 함수
function transfer(
address to,
uint amount
) external override returns (bool) {
// 전달하는 사람의 잔액이 감소해야하고
balance[msg.sender] -= amount;
// 전달받은 사람의 잔액이 증가
balance[to] += amount;
// transfer 이벤트를 실행시킨 로그를 트랜잭션에서 확인
// emit 구문을 사용해서 이벤트 함수 실행
emit Transfer(msg.sender, to, amount);
// 성공은 true,실패는 false
return true;
}
// 토큰의 소유권을 위임하는 함수
function approve(
address spender,
uint amount
) external override returns (bool) {
// 소유권을 추가
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
// 권한을 가지고 있는 제 3자가 토큰을 보낼 때 사용하는 함수
function transferFrom(
address spender,
address to,
uint amount
) external override returns (bool) {
// 이 사람이 권한을 가지고 있는지 토큰의 양을 검사를 하고
require(allowance[spender][msg.sender] >= amount);
allowance[spender][msg.sender] -= amount;
balance[spender] -= amount;
balance[to] += amount;
return true;
}
// 토큰을 발행하는 함수
// internal 컨트랙트 내부에서만 실행시킬 함수
function mint(uint amount) internal {
balance[msg.sender] += amount;
totalSupply += amount;
}
// 토큰 소각 시키는 함수
// 토큰을 너무 많이 발행하면 가치가 떨어지기 때문에 소각을 시킨다.
function burn(uint amount) external {
balance[msg.sender] -= amount;
totalSupply -= amount;
}
}
중요한 점은 이 토큰은 이더리움(코인)이 아니란 것이다. 위 주석에서 설명됐듯이 코인은 메인넷을 가지고 있지만 토큰은 그렇지 않다.
때문에 토큰을 배포한다면 그 계좌는 이더리움,토큰 두개의 화폐를 가지고 있으며 이더리움을 가지고 오는 메서는 기본 메서드인 eth.getBalance를 사용하며 토큰은 우리가 만든 balanceOf를 사용하게 된다.
npm install -g @remix-project/remixd
remixd -s . --remix-ide https://remix.ethereum.org
# '.' 은 파일 경로를 의미한다. 여기서는 현재 경로에 있는 모든 폴더를 연동한다는 뜻이다.
리믹스에 대한 의존 파일을 설치하고 서버를 연다.
import React, { useEffect, useState } from "react";
import Web3 from "web3";
function useWeb3() {
const [user, setUser] = useState({
account: "",
balance: "",
});
const [web3, setWeb3] = useState(null);
useEffect(() => {
if (window.ethereum) {
window.ethereum
.request({ method: "eth_requestAccounts" })
.then(async ([data]) => {
const webProvider = new Web3(window.ethereum);
setWeb3(webProvider);
setUser({
account: data,
balance: webProvider.utils.toWei(
await webProvider.eth.getBalance(data),
"ether"
),
});
// 웹 메타마스크 지갑 다 뜰거고
// 그 지갑에 있는 토큰 양을 다 보여줄거고
// 컨트랙트를 배포한 네트워크가 맞는지 아니면 네트워크 변경할수 있게 함수 실행
// 지갑을 바꾸면 바꾼 지갑내용으로 브라우저에 보일 수 있게
});
} else {
alert("메타 마스크 설치해주세요");
}
}, []);
return { user, web3 };
}
export default useWeb3;
import React, { useEffect, useState } from "react";
import useWeb3 from "./hooks/web3.hook";
import abi from "./abi/ERC20.json";
import Web3 from "web3";
function App() {
const { user, web3 } = useWeb3();
const [ERC20Contract, setERC20Contract] = useState(null);
const [network, setNetwork] = useState(null);
const [accounts, setAccounts] = useState([]);
const [token, setToken] = useState("0");
const [value, setValue] = useState("");
const [value2, setValue2] = useState("");
useEffect(() => {
if (web3 !== null) {
if (ERC20Contract) return;
const ERC20 = new web3.eth.Contract(
abi,
"0xAC7da4aF2E5C3A26B049B265BFe89d0B601b8a34",
{ data: "" }
);
console.log(ERC20);
setERC20Contract(ERC20);
}
}, [web3]);
useEffect(() => {
// 이벤트 등록 네트워크가 변경되면 발생하는 이벤트 등록
window.ethereum.on("chainChanged", (chainId) => {
console.log("네트워크 변경", chainId);
console.log(network);
if (chainId === "0x539") {
getAccounts();
}
});
// 지갑이 변경되면 실행할 이벤트 등록
window.ethereum.on("accountsChanged", (account) => {
console.log("지갑 변경됨", account);
getAccounts();
});
if (!ERC20Contract) {
return;
}
// 컨트랙트 인스턴스가 있으면 실행시키지 않고
// 네트워크가 정상적일 때 실행시켜도 되겠다.
}, [network]);
const switchNet = async () => {
// 해당 네트워크가 맞는지 요청
// 메타마스크로 요청
// wallet_switchEthereumChain == chainId가 맞는지 확인 매개변수로 전달한 chainId가 맞는지
// 0x539우리가 설정한 chainId 1337
const net = await window.ethereum.request({
jsonrpc: "2.0",
method: "wallet_switchEthereumChain",
params: [{ chainId: "0x539" }],
});
console.log(net);
// net값이 null이 반환되면 해당 네트워크에 있다는 뜻.
setNetwork(net || true);
};
// 전달받은 매개변수 (지갑주소) 의 잔액을 보여주는 함수
const getToken = async (account) => {
if (!ERC20Contract) {
console.log(web3);
return;
}
let result = web3.utils
.toBigInt(await ERC20Contract.methods.balanceOf(account).call())
.toString(10);
result = await web3.utils.fromWei(result, "ether");
return result;
};
// 해당 계정의 이더리움을 가져오는 함수
const getETH = async (account) => {
if (!ERC20Contract) {
return;
}
let ETHResulut = await web3.eth.getBalance(account);
console.log(ETHResulut, "가져온 이더리움 계좌 잔액");
ETHResulut = web3.utils.fromWei(ETHResulut, "ether");
console.log(ETHResulut, "가져온 이더리움 계좌 이더변환");
return ETHResulut;
};
// 메타마스크의 모든 지갑을 보여줄 함수
const getAccounts = async () => {
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
// 배열을 돌릴 때 map에서 일어나는 promise반환값을 다 처리하고 넘어가기
// promise.all 요청이 다 끝나면 진행
const accountsCom = await Promise.all(
accounts.map(async (account) => {
const token = await getToken(account);
const eth = await getETH(account);
return { account, token, eth };
})
);
// console.log(await getToken(accounts[0]),'getAccounts');
setToken(await getToken(accounts[0]));
setAccounts(accountsCom);
};
// 지갑에서 다른 지갑으로 토큰 전송할 함수
const transfer = async () => {
await ERC20Contract.methods
.transfer(
value.replaceAll(" ", ""),
await web3.utils.toWei(value2, "ether")
)
.send({
from: user.account,
});
getAccounts();
};
// 0xAC7da4aF2E5C3A26B049B265BFe89d0B601b8a34
// 0xD1b0b915Ee4c470C02B445dE2cfEA715848bfa06
if (user.account === null) {
return "지갑 연결하셈";
}
return (
<div>
<button onClick={switchNet}>토큰 컨트랙트 연결</button>
<div> 지갑 주소:{user.account}</div>
<h2> 토큰 보유량:{token}</h2>
{accounts.map((item, index) => {
return (
<div key={index}>
계정:{item.account},
<br />
토큰량:{item.token}
<br />
이더리움 양:{item.eth}
</div>
);
})}
<div>
<label>누구한테 보낼거임? </label>
<input
onChange={(e) => {
setValue(e.target.value);
}}
></input>
<label>토큰수 </label>
<input
onChange={(e) => {
setValue2(e.target.value);
}}
></input>
</div>
<button onClick={transfer}>보내기</button>
계정들의 이더리움 잔액 보여주는 함수 만들어서 보여주자
</div>
);
}
export default App;
리믹스를 써서 확실히 편했지만 자유도가 떨어지는 느낌이다. 코어 개발을 할 만큼 아직 실력이 있진않지만, 실무에서는 코어개발이 가능할 정도로 공식문서, 라이브러리 형식을 꼼꼼히 익혀 토큰,nft발행과 같은 스마트 컨트랙트 로직을 독자적으로 설계하고 싶다.