devops

OpenZeppelin ERC-20 Solidity 코드 리뷰 본문

개발/Solidity

OpenZeppelin ERC-20 Solidity 코드 리뷰

vata500 2023. 10. 9. 20:36
반응형

OpenZepplin

오픈제플린은 EVM 스마트 컨트랙트 개발에 많이 사용되는 오픈 소스 라이브러리다. 사실상 Solidity 기반 컨트랙트 개발에 있어선 표준으로 자리 잡을 정도로 사용된다. 

  • Security-Centric : 보안 전문가들에 의해서 검토되어 있으며, 실사용되고 있는 검증된 컨트랙트들로 구성되어 있다.
  • Standard Contracts : ERC-20, ERC-721과 같은 표준 토큰 컨트랙트를 제공함.
  • Reusable Components : 공통기능을 제공하는 다양한 스마트 계약 구성 요소를 포함하고 있어서, 개발자들은 이를 조합해 복잡한 기능을 빠르고 안전하게 구현가능.
  • Community-Driven : 활발한 커뮤니티에 의해 지원되고 있으며, 보안과 최적화 및 기타 주제에 대해서 지식을 공유하고 있음.
  • Plugins and Extensibility : Hardhat, Truffle 등의 인기있는 이더리움 개발 도구를 지원

오픈제플린에는 여러 코드들이 있는데, 그 중 컨트랙트 개발에 있어서 가장 기본이 되는 ERC20 코드를 리뷰하려 한다.

Defi, Dex 등 크립토 씬의 모든 섹터를 통틀어 자산거래에 있어 ERC20은 다 사용되기 때문에 살펴보면 좋다. 특히 오픈제플린의 소스코드가 얼마나 보안적으로 안전하고 검증되었는지를 확인해보자.

https://docs.openzeppelin.com/contracts/5.x/api/token/erc20

  • IERC20 : ERC20의 모든 실행의 인터페이스.
  • IERC20Metadata : ERC20 인터페이스의 익스텐션으로 name, symbol, decimals 함수를 포함함. 
  • ERC20 : ERC20 인터페이스 Implementation으로 standard extension

[ERC20Base.sol]

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol)

pragma solidity ^0.8.0;

import { IERC20 } from "./IERC20.sol";
import { ERC20Metadata } from "./ERC20Metadata.sol";

import { ERC20SlotBase } from "./ERC20SlotBase.sol";
import { StorageSlot, Uint256Slot } from "../../utils/StorageSlot.sol";
// 아래에 StorageSlot에 대한 추가 설명 있음

import { Context } from "../../utils/Context.sol";

abstract contract ERC20Base is IERC20, ERC20Metadata, Context {
    function getSlotTotalSupply() private pure returns (Uint256Slot storage) {
        // TODO: slot offset conventions
        return StorageSlot.getUint256Slot(ERC20SlotBase-4);
    }

    function getSlotBalance(address account) private pure returns (Uint256Slot storage) {
        // TODO: slot offset conventions
        return StorageSlot.getUint256Slot(keccak256(abi.encode(ERC20SlotBase, account)));
    }
    function getSlotAllowance(address owner, address spender) private pure returns (Uint256Slot storage) {
        // TODO: slot offset conventions
        return StorageSlot.getUint256Slot(keccak256(abi.encode(ERC20SlotBase, owner, spender)));
    }
		// StorageSlot의 totalSupply, balance, allowance의 슬롯 위치를 반환하는 getter 메소드.

    function setTotalSupply(uint256 _totalSupply) internal {
        getSlotTotalSupply().value = _totalSupply;
    }
    function setBalance(address account, uint256 balance) internal {
        getSlotBalance(account).value = balance;
    }
    function setAllowance(address owner, address spender, uint256 _allowance) internal {
        getSlotAllowance(owner, spender).value = _allowance;
    }
		// StorageSlot 위치를 반환해서 해당 슬롯의 변수값 totalSupply, balance, allowance를 변경하는 setter 메소드

    function totalSupply() public view override returns (uint256) {
        return getSlotTotalSupply().value;
    }
    function balanceOf(address account) public view override returns (uint256) {
        return getSlotBalance(account).value;
    }
    function allowance(address owner, address spender) public view override returns (uint256) {
        return getSlotAllowance(owner, spender).value;
    }
		// IERC20 인터페이스에 정의된 메소드를 재정의

    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, allowance(owner, spender) + addedValue);
        return true;
    }

    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        address owner = _msgSender();
        uint256 currentAllowance = allowance(owner, spender);
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }

        return true;
    }
		// 외부에서 호출가능한 메소드들로 virtual로 선언되어 override가 가능함.
		// 실제 동작하는 메소드는 _(언더스코어)로 선언된 internal 메소드들임

    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");
		// require을 통해 조건부 검증 진행, from, to가 0이면 트랜잭션 실패

        _beforeTokenTransfer(from, to, amount);
		// hook 함수 실행

        // TODO: balanceOf optimization?
        uint256 fromBalance = balanceOf(from);
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            setBalance(from, fromBalance - amount);
            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
            // decrementing then incrementing.
            setBalance(to, balanceOf(to)+amount);
        }
		// overlfow, underflow 발생 시 revert

        emit Transfer(from, to, amount);
		// transfer 이벤트 발생

        _afterTokenTransfer(from, to, amount);
		// hook 함수 실행
    }

    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");
		// account가 0인 경우 트랜잭션 실패

        _beforeTokenTransfer(address(0), account, amount);
		// hook 메소드 실행

        // TODO: totalSupply optimization?
        setTotalSupply(totalSupply() + amount);
		// totalSupply 추가 발행
 
        unchecked {
            // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
            setBalance(account, balanceOf(account) + amount);
        }
		// account의 토큰 수량 증가, 오버플로우, 언더플로우 시 revert

        emit Transfer(address(0), account, amount);
		// Transfer 이벤트 실행

        _afterTokenTransfer(address(0), account, amount);
		// hook 메소드 실행
    }

    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");
		// account가 0인 경우 트랜잭션 실패

        _beforeTokenTransfer(account, address(0), amount);
		// hook 메소드 실행

        // TODO: balanceOf optimization?
        uint256 accountBalance = balanceOf(account);
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
		// account 보유 수량이 burn 수량보다 적으면 트랜잭션 실패

        unchecked {
            setBalance(account, accountBalance - amount);
            // Overflow not possible: amount <= accountBalance <= totalSupply.
            setTotalSupply(totalSupply()-amount);
        }
		// 오버플로우와 언더플로우 시 revert

        emit Transfer(account, address(0), amount);
		// Transfer 이벤트 실행

        _afterTokenTransfer(account, address(0), amount);
		// hook 메소드 실행
    }

    function _approve(address owner, address spender, uint256 amount) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");
		// owner, spender의 address가 0이면 트랜잭션 실패

        setAllowance(owner, spender, amount);
		// owner의 토큰을 spender가 사용할 수 있는 수량 설정
        emit Approval(owner, spender, amount);
		// approval 이벤트 실행
    }

    function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
		// 현재 allowance가 사용하려는 amount보다 적을 때 트랜잭션 실패
            unchecked {
                _approve(owner, spender, currentAllowance - amount);
            }
		// approve 메소드 실행, 오버플로우 언더플로우 발생시 revert
        }
    }

    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}
		// 토큰 전송 전에 실행되는 훅 메소드 정의

    function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
		// 토큰 전송 후에 실되는 훅 메소드 정의
}

+ 추가 개념

StorageSlot

  • Storage Slot에 대한 접근과 관리를 도와주는 유틸리티 라이브러리. Storage Slot은 이더리움 스토리지의 각 위치를 나타냄
  • Ethereum 스토리지는 key-value 저장소로 구성되어 있음. 각 key는 256 bit Slot
  • Smart Contract의 변수는 이 Slot에 저장되며, Slot의 개념은 Ehtereum Gas비용과 스토리지 최적화와 관련이 있음

StorageSlot이 필요한 이유

  • Gas 최적화 : Ethereum에서는 스토리지 변경 때마다 gas가 소비됨. 변수의 배치와 사용 방식으로 스토리지를 효율적으로 활용하면 Gas 비용을 절약할 수 있음
  • 데이터 무결성 : 스마트 컨트랙트 코드가 업그레이드 되거나 변경될 때 스토리지의 레이아웃이 변경되지 않도록 해야함. 잘못된 스토리지 구조 변경은 데이터 손실 유발가능
  • 저수준 접근 : Storage Slot을 조작해야하는 경우가 있음. 프록시 패턴으로 컨트랙트를 업그레이드할 때 컨트랙트 스토리지 레이아웃을 그대로 유지해야함
  • 최적화된 데이터 구조 : 배열, 매핑 등의 복잡한 데이터 구조를 사용할 때, 내부적으로 여러 슬롯에 대해 이해하면 데이터 구조를 효율적으로 구성하고 관리가능

[IERC20.sol]

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

interface IERC20 {
    event Transfer(address indexed from, address indexed to, uint256 value);

    event Approval(address indexed owner, address indexed spender, uint256 value);

    function totalSupply() external view returns (uint256);

    function balanceOf(address account) external view returns (uint256);

    function transfer(address to, uint256 amount) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint256);

    function approve(address spender, uint256 amount) external returns (bool);

    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

위 인터페이스를 이용해서 ERC20 컨트랙트를 배포

반응형

'개발 > Solidity' 카테고리의 다른 글

ERC1155, Multi-token standard  (0) 2023.11.12
ERC-721, Non-Fungible Token  (0) 2023.11.05
ERC-6551, NFT Bound Account  (0) 2023.11.05
Comments