지난번 실습 이후로 ERC20 토큰을 개선하기 위한 코드를 포스팅 합니다.
공부한 내용을 정리할 목적으로 포스팅합니다.
SafeMath를 사용하는 이유는 데이터의 오버플로우, 언더플로우를 방지하기위함입니다. 오버플로우란 간단히 말하면 데이터 최댓값을 초과한 것을 말하고 언더플로우란 데이터의 최솟값 미만으로 값이 낮아지는 것을 말합니다. SafeMath는 정수 오버플로 및 언더플로 취약성을 방지하도록 설계된 Solidity의 라이브러리입니다. Solidity에서 정수를 사용할 때는 수학적 연산 결과가 데이터 유형에 저장할 수 있는 최대값 또는 최소값을 초과할 때 발생할 수 있는 정수 오버플로 및 언더플로의 위험에 유의해야 합니다.
1. 가스 비용: SafeMath 연산은 일반 산술 연산에 비해 가스 사용량 측면에서 더 비쌀 수 있습니다. 이는 SafeMath 함수가 추가 검사를 수행하고 더 많은 계산 리소스를 필요로 하기 때문입니다. 결과적으로 SafeMath를 사용하면 스마트 계약과 관련된 가스 비용이 증가할 수 있습니다.
2. 제한된 기능: SafeMath 라이브러리는 덧셈, 뺄셈, 곱셈, 나눗셈과 같은 기본 산술 연산을 수행하는 기능만 제공합니다. 더 복잡한 작업을 수행해야 하는 경우 사용자 정의 라이브러리 또는 기능을 구현해야 할 수도 있습니다.
3. 코드 복잡성: SafeMath 라이브러리를 사용할 때는 라이브러리 함수를 사용하여 모든 정수 산술 연산을 수행해야 합니다. 이는 코드를 더 복잡하고 읽기 어렵게 만들 수 있으며, 이는 버그를 발생시킬 위험을 증가시킬 수 있다.
4. 다른 유형의 취약성에 대한 보호 없음: SafeMath는 정수 오버플로 및 언더플로 취약성에 대해서만 보호합니다. 재진입, 논리 오류 또는 타이밍 공격과 같은 다른 유형의 취약성에 대한 보호 기능을 제공하지 않습니다. 이러한 유형의 취약성을 해결하려면 다른 모범 사례 및 보안 조치를 사용하는 것이 중요합니다.
SafeMath 코드는 다음과 같습니다.
//오버플로우, 언더플로우를 방지하기 위한 라이브러리
//오버플로우 데이터 타입의 최댓값을 초과하면 발생하는 에러
//언더플로우 데이터 타입의 최솟값보다 미만이되면 발생하는 에러
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a / b;
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
OwnerHelper 계약은 다른 계약의 소유권을 관리하기 위한 도우미 계약의 역할을 하는 추상적인 계약입니다. 다음은 이 추상적 계약을 사용할 때의 3 가지 장점과 2가지 단점입니다:
1. 코드 재사용: 이 계약은 소유권 관리가 필요한 다른 계약으로 상속될 수 있습니다. 이렇게 하면 코드 중복을 방지하고 개발 시간을 절약할 수 있습니다.
2. 액세스 제어: 이 추상 계약에서 상속되는 다른 계약에서 소유자 한정자만 사용하여 계약 소유자에게 특정 기능 또는 데이터에 대한 액세스를 제한할 수 있습니다.
3. 이벤트 로깅: 소유권 이전 이벤트는 계약 소유권이 이전될 때 발생하며, 이는 투명성과 소유권 변경을 감사할 수 있는 방법을 제공합니다.
1. 제한된 기능: 이 추상 계약은 소유권 관리 기능만 제공하며 독립형 계약으로 사용할 수 없습니다.
2. 보안 취약성: 유일한 소유자 한정자는 발신자가 소유자인지 여부만 확인하고 전송 중인 주소가 유효한 주소인지는 확인하지 않습니다. 이로 인해 잘못된 주소가 지정될 경우 보안 취약성이 발생할 수 있습니다. >> 이 부분은 TokenLock 코드에서 보완해줍니다.
OwnerHelper 코드는 다음과 같습니다.
//추상화 컨트랙트
//abstract contract는 contract의 구현된 기능과 interface의 추상화 기능 모두를 포함
//abstract contract는 실제 contract에서 사용하지 않으면
//추상으로 표시되어 사용되지 않음
abstract contract OwnerHelper {
//_owner는 관리자
address private _owner;
//아래의 이벤트는 관리자가 변경됐을 때 이전 관리자의 주소와 새로운 관리자의 주소를 로그로 남깁니다.
event OwnershipTransferred(address indexed preOwner, address indexed nextOwner);
//함수 실행 이전에 함수를 실행시키는 사람이 관리자인지 확인
modifier onlyOwner {
require(msg.sender == _owner, "OwnerHelper: caller is not owner");
_;
}
constructor() {
_owner = msg.sender;
}
function owner() public view virtual returns (address) {
return _owner;
}
function transferOwnership(address newOwner) onlyOwner public {
require(newOwner != _owner);
require(newOwner != address(0x0));
address preOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(preOwner, newOwner);
}
}
tokenLock : 토큰의 전체 락에 대한 처리
tokenPersonalLock : 토큰의 개인 락에 대한 처리입니다.
함수 isTokenLock : 전체 락과, 보내는 사람의 락, 받는 사람의 락을 검사하여 락이 걸려 있는지 확인합니다.
Lock은, ERC-20에서 지원하는 토큰의 분배나 이동 등의 유동성을 제한하는 기능입니다.
//토큰의 전체 락에 대한 처리
//Lock은, ERC-20에서 지원하는 토큰의 분배나 이동 등의 유동성을 제한하는 기능입니다.
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;
}
}
솔리디티 전체 코드에 보시면 _transfer 함수에서도 lock이 걸려있는지 방지하기 위해 체크하는 부분을 볼 수 있습니다.
function _transfer(address sender, address recipient, uint256 amount) internal virtual {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
require(isTokenLock(sender, recipient) == false, "TokenLock: invalid token transfer");
uint256 senderBalance = _balances[sender];
require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
_balances[sender] = senderBalance.sub(amount);
_balances[recipient] = _balances[recipient].add(amount);
}
이 락들을 제거할 수 있는 removeTokenLock과 removePersonalTokenLock도 존재합니다. 해당 함수들은 onlyOwner를 적용하여 관리자만 락을 해제할 수 있도록 해야 합니다. 이렇게 락을 적용하게 되면 모든 락을 해제할 때만 토큰의 이동이 가능합니다.
//토큰 락 해제 함수
function removeTokenLock() onlyOwner public {
require(_tokenLock == true);
_tokenLock = false;
}
//개별 토큰락 삭제
function removePersonalTokenLock(address _who) onlyOwner public {
require(_personalTokenLock[_who] == true);
_personalTokenLock[_who] = false;
}
솔리디티 전체 코드
// SPDX-License-Identifier: GPL-3.0
// 관리자만 사용할 수 있는 함수인 OwnerHelper 함수를 만들었음
pragma solidity >=0.7.0 <0.9.0;
//ERC20의 표준 인터페이스
interface ERC20Interface {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function transferFrom(address spender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 amount);
event Transfer(address indexed spender, address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 oldAmount, uint256 amount);
}
//오버플로우, 언더플로우를 방지하기 위한 라이브러리
//오버플로우 데이터 타입의 최댓값을 초과하면 발생하는 에러
//언더플로우 데이터 타입의 최솟값보다 미만이되면 발생하는 에러
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a / b;
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
//추상화 컨트랙트
//abstract contract는 contract의 구현된 기능과 interface의 추상화 기능 모두를 포함
//abstract contract는 실제 contract에서 사용하지 않으면
//추상으로 표시되어 사용되지 않음
abstract contract OwnerHelper {
//_owner는 관리자
address private _owner;
//아래의 이벤트는 관리자가 변경됐을 때 이전 관리자의 주소와 새로운 관리자의 주소를 로그로 남깁니다.
event OwnershipTransferred(address indexed preOwner, address indexed nextOwner);
//함수 실행 이전에 함수를 실행시키는 사람이 관리자인지 확인
modifier onlyOwner {
require(msg.sender == _owner, "OwnerHelper: caller is not owner");
_;
}
constructor() {
_owner = msg.sender;
}
function owner() public view virtual returns (address) {
return _owner;
}
function transferOwnership(address newOwner) onlyOwner public {
require(newOwner != _owner);
require(newOwner != address(0x0));
address preOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(preOwner, newOwner);
}
}
contract SimpleToken is ERC20Interface, OwnerHelper {
using SafeMath for uint256;
mapping (address => uint256) private _balances;
mapping (address => mapping (address => uint256)) public _allowances;
uint256 public _totalSupply;
string public _name;
string public _symbol;
uint8 public _decimals;
bool public _tokenLock;
mapping (address => bool) public _personalTokenLock;
constructor(string memory getName, string memory getSymbol) {
_name = getName;
_symbol = getSymbol;
_decimals = 18;
_totalSupply = 100000000e18;
_balances[msg.sender] = _totalSupply;
_tokenLock = true;
}
function name() public view returns (string memory) {
return _name;
}
function symbol() public view returns (string memory) {
return _symbol;
}
function decimals() public view returns (uint8) {
return _decimals;
}
function totalSupply() external view virtual override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) external view virtual override returns (uint256) {
return _balances[account];
}
function transfer(address recipient, uint amount) public virtual override returns (bool) {
_transfer(msg.sender, recipient, amount);
emit Transfer(msg.sender, recipient, amount);
return true;
}
function allowance(address owner, address spender) external view override returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint 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 _transfer(address sender, address recipient, uint256 amount) internal virtual {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
require(isTokenLock(sender, recipient) == false, "TokenLock: invalid token transfer");
uint256 senderBalance = _balances[sender];
require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
_balances[sender] = senderBalance.sub(amount);
_balances[recipient] = _balances[recipient].add(amount);
}
//토큰의 전체 락에 대한 처리
//Lock은, ERC-20에서 지원하는 토큰의 분배나 이동 등의 유동성을 제한하는 기능입니다.
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() onlyOwner public {
require(_tokenLock == true);
_tokenLock = false;
}
function removePersonalTokenLock(address _who) onlyOwner public {
require(_personalTokenLock[_who] == true);
_personalTokenLock[_who] = false;
}
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);
}
}
(1차 토큰 전송 실패(락이 되어있어서))
(락 해제)
(2차 토큰 전송 성공)
1. 브라우저에서 위 코드를 remix에 복사 붙여넣기를 합니다.
2. 솔리디티 코드를 컴파일합니다.
3. 메타마스크 - 이더리움 테스트넷으로 설정한 뒤 배포합니다. (Goerli 테스트 네트워크)
4. 배포 트랜잭션이 성공하면 다음과 같은 화면이 보입니다.
리믹스
이더스캔에서도 트랜잭션을 확인할 수 있습니다.
배포한 컨트랙트 주소 : 0x0f2dbd07e36b447f59e8f39389453d7e6aa0162a
https://goerli.etherscan.io/tx/0x1db12eb2b7fd9feb22ac6156dbe9ad156a9291359b2aebf1273c16f57905f02d
5. 배포한 토큰 컨트랙트를 계정1에 추가해줍니다.
5-1 토큰 계약주소에 트랜잭션 성공시 확인했던 토큰 컨트랙트 주소를 넣어줍니다.
5-2 토큰 기호는 OHT, 또는 원하는 이름으로 작성합니다.
5-3 토큰 소수점은 18로 해주시면됩니다.
6. 다른 브라우저에서 메타마스크 계정을 만든 뒤 계정1에서 새로 만든 계정2로 토큰을 전송합니다.
그러나 사진처럼 실패할 것입니다. 왜냐하면 현재 토큰은 락이 되어있기때문입니다.
이더스캔 실패한 트랜잭션:
https://goerli.etherscan.io/tx/0x876febef9b4ecd45a921daa076c69ac1a08e0c1ea22009dfb203cb057ff2f9b8
7. 이후 다시 계약을 배포한 계정1로 돌아가서 다음과 같이 왼쪽 버튼에 있는 removeTokenLock 버튼을 클릭하면 다음과 같이 락이 해제된 것을 확인할 수 있습니다.
리믹스에서 보이는 로그
[block:8495220 txIndex:4]
from: 0x768...7d74c
to: SimpleToken.removeTokenLock() 0x0f2...0162A
value: 0 weidata: 0x697...12ffalogs: 0hash: 0x171...62f6d
status true Transaction mined and execution succeed
transaction hash 0x51bbb0f38a8396684530cae716897f5c10282367deb7d6f2f4c60245a265fe50
from 0x7684992428a8E5600C0510c48ba871311067d74c
to SimpleToken.removeTokenLock() 0x0f2DbD07E36b447f59E8f39389453d7e6aa0162A
gas 28736 gas
transaction cost 28736 gas
input 0x697...12ffa
decoded input {}
decoded output -
logs []
val 0 wei
8. 이후 다시 토큰을 계정1에서 계정2로 보내면 토큰 전송이 성공합니다.
메타마스크에서 확인
이더스캔에서 확인
https://goerli.etherscan.io/tx/0xc1a1f3c9778d3edd125601e38eb6db3a6e2b9244426ddd0980ae3abadfa97242
로컬 컴퓨터와 Remix 연결하기 (0) | 2023.02.16 |
---|---|
ERC-721(NFT) Solidity 코드 및 토큰 분석 (0) | 2023.02.15 |
ERC-20 토큰(나만의 토큰 만들기 실습) (0) | 2023.02.13 |
우분투에 솔리디티 컴파일러 설치 (0) | 2023.02.07 |
스마트 컨트랙트 구조와 Solidity 변수 (0) | 2023.02.07 |