Notice
Recent Posts
Recent Comments
Link
devops
OpenZeppelin ERC-20 Solidity 코드 리뷰 본문
반응형
OpenZepplin
오픈제플린은 EVM 스마트 컨트랙트 개발에 많이 사용되는 오픈 소스 라이브러리다. 사실상 Solidity 기반 컨트랙트 개발에 있어선 표준으로 자리 잡을 정도로 사용된다.
- Security-Centric : 보안 전문가들에 의해서 검토되어 있으며, 실사용되고 있는 검증된 컨트랙트들로 구성되어 있다.
- Standard Contracts : ERC-20, ERC-721과 같은 표준 토큰 컨트랙트를 제공함.
- Reusable Components : 공통기능을 제공하는 다양한 스마트 계약 구성 요소를 포함하고 있어서, 개발자들은 이를 조합해 복잡한 기능을 빠르고 안전하게 구현가능.
- Community-Driven : 활발한 커뮤니티에 의해 지원되고 있으며, 보안과 최적화 및 기타 주제에 대해서 지식을 공유하고 있음.
- Plugins and Extensibility : Hardhat, Truffle 등의 인기있는 이더리움 개발 도구를 지원
오픈제플린에는 여러 코드들이 있는데, 그 중 컨트랙트 개발에 있어서 가장 기본이 되는 ERC20 코드를 리뷰하려 한다.
Defi, Dex 등 크립토 씬의 모든 섹터를 통틀어 자산거래에 있어 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