상세 컨텐츠

본문 제목

ERC-20 토큰 - 개선(SafeMath, OwnerHelper, TokenLock 추가)

Programming Language/Solidity

by Yongari 2023. 2. 15. 18:37

본문

지난번  실습 이후로 ERC20 토큰을 개선하기 위한 코드를 포스팅 합니다.

공부한 내용을 정리할 목적으로 포스팅합니다.

https://next-block.tistory.com/entry/ERC-20-%ED%86%A0%ED%81%B0%EB%82%98%EB%A7%8C%EC%9D%98-%ED%86%A0%ED%81%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%8B%A4%EC%8A%B5

 

ERC-20 토큰(나만의 토큰 만들기 실습)

ERC-20(Ethereum Request for Comment20) ERC-20은 Ethereum Request for Comment20의 약자를 뜻하며 ERC 제안서의 일련번호다. ERC-20은 이더리움 블록체인 네트워크에서 정한 표준 토큰 스펙으로 EIPs에서 관리하는 공식

next-block.tistory.com



SafeMath 라이브러리를 사용하는 이유: 

SafeMath를 사용하는 이유는 데이터의 오버플로우, 언더플로우를 방지하기위함입니다. 오버플로우란 간단히 말하면 데이터 최댓값을 초과한 것을 말하고 언더플로우란 데이터의 최솟값 미만으로 값이 낮아지는 것을 말합니다. SafeMath는 정수 오버플로 및 언더플로 취약성을 방지하도록 설계된 Solidity의 라이브러리입니다. Solidity에서 정수를 사용할 때는 수학적 연산 결과가 데이터 유형에 저장할 수 있는 최대값 또는 최소값을 초과할 때 발생할 수 있는 정수 오버플로 및 언더플로의 위험에 유의해야 합니다.


장점:

  1. SafeMath 라이브러리는 연산 결과가 예상 범위 내에 있는지 확인하기 위해 추가 검사를 통해 정수에 대해 산술 연산을 수행하는 함수 집합을 제공합니다. 다음은 SafeMath in Solidity의 몇 가지 이점입니다:

  2. 정수 오버플로 및 언더플로 방지: SafeMath는 안전하고 안전한 방식으로 정수에 대한 산술 연산을 수행할 수 있는 방법을 제공하여 공격자가 악용할 수 있는 정수 오버플로 및 언더플로 취약성을 방지합니다.

  3. 데이터 무결성 보장: SafeMath를 사용하면 조작 중인 데이터의 무결성을 보장하여 스마트 계약에서 버그 및 기타 예기치 않은 동작을 방지할 수 있습니다.

  4. 보안 향상: SafeMath를 사용하면 정수 오버플로 및 언더플로 취약성을 이용하는 공격의 위험을 줄여 스마트 계약을 더욱 안전하게 보호할 수 있습니다.

  5. 개발 시간 절약: SafeMath는 Solidity 스마트 계약에서 쉽게 사용할 수 있는 일련의 사전 구축된 기능을 제공하여 개발 과정에서 시간과 노력을 절약합니다. 전반적으로 SafeMath in 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 계약을 사용하는 이유

 

 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 

 

 

tokenLock : 토큰의 전체 락에 대한 처리

tokenPersonalLock : 토큰의 개인 락에 대한 처리입니다.

함수 isTokenLock :  전체 락과, 보내는 사람의 락, 받는 사람의 락을 검사하여 락이 걸려 있는지 확인합니다.

Lock은, ERC-20에서 지원하는 토큰의 분배나 이동 등의 유동성을 제한하는 기능입니다.

 

isTokenLock


  //토큰의 전체 락에 대한 처리 
  //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);
    }

 

이 락들을 제거할 수 있는 removeTokenLockremovePersonalTokenLock도 존재합니다. 해당 함수들은 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 테스트 네트워크)

  • Provider에 메타마스크 설정을 합니다.
  • 이후 컨트랙트에서 Simple Token을 체크한 뒤
  • Deploy를 클릭합니다.
  • 편의상 계정1이라고 부르겠습니다. 

 

 

 

4. 배포 트랜잭션이 성공하면 다음과 같은 화면이 보입니다.

 

리믹스

 

 

 

이더스캔에서도 트랜잭션을 확인할 수 있습니다. 
배포한 컨트랙트 주소 : 0x0f2dbd07e36b447f59e8f39389453d7e6aa0162a  

 

https://goerli.etherscan.io/tx/0x1db12eb2b7fd9feb22ac6156dbe9ad156a9291359b2aebf1273c16f57905f02d 

 

Goerli Transaction Hash (Txhash) Details | Etherscan

Goerli (GTH) detailed transaction info for txhash 0x1db12eb2b7fd9feb22ac6156dbe9ad156a9291359b2aebf1273c16f57905f02d. The transaction status, block confirmation, gas fee, Ether (ETH), and token transfer are shown.

goerli.etherscan.io

 

 

5. 배포한 토큰 컨트랙트를 계정1에 추가해줍니다.
5-1 토큰 계약주소에 트랜잭션 성공시 확인했던 토큰 컨트랙트 주소를 넣어줍니다.

5-2 토큰 기호는 OHT, 또는 원하는 이름으로 작성합니다.

5-3 토큰 소수점은 18로 해주시면됩니다. 

 

6.  다른 브라우저에서 메타마스크 계정을 만든 뒤 계정1에서 새로 만든 계정2로 토큰을 전송합니다.

그러나 사진처럼 실패할 것입니다. 왜냐하면 현재 토큰은 락이 되어있기때문입니다.  

 

이더스캔 실패한 트랜잭션:

https://goerli.etherscan.io/tx/0x876febef9b4ecd45a921daa076c69ac1a08e0c1ea22009dfb203cb057ff2f9b8

 

Goerli Transaction Hash (Txhash) Details | Etherscan

Goerli (GTH) detailed transaction info for txhash 0x876febef9b4ecd45a921daa076c69ac1a08e0c1ea22009dfb203cb057ff2f9b8. The transaction status, block confirmation, gas fee, Ether (ETH), and token transfer are shown.

goerli.etherscan.io

 

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

 

Goerli Transaction Hash (Txhash) Details | Etherscan

Goerli (GTH) detailed transaction info for txhash 0xc1a1f3c9778d3edd125601e38eb6db3a6e2b9244426ddd0980ae3abadfa97242. The transaction status, block confirmation, gas fee, Ether (ETH), and token transfer are shown.

goerli.etherscan.io

 

관련글 더보기