Web3-代幣ERC20/ERC721以及合約安全溢出和下溢的研究
以太坊上的代幣
如果你對以太坊的世界有一些了解,你很可能聽人們聊過代幣— ERC20代幣
一個 代幣 在以太坊基本上就是一個遵循一些共同規則的智能合約——即它實現了所有其他代幣合約共享的一組標準函數,例如 transfer(address _to, uint256 _value)
和 balanceOf(address _owner)
。
在智能合約內部,通常有一個映射, mapping(address => uint256) balances
,用于追蹤每個地址還有多少余額。
所以基本上一個代幣只是一個追蹤誰擁有多少該代幣的合約,和一些可以讓那些用戶將他們的代幣轉移到其他地址的函數。
ERC20的重要性
由于所有 ERC20 代幣共享具有相同名稱的同一組函數,它們都可以以相同的方式進行交互。
這意味著如果你構建的應用程序能夠與一個 ERC20 代幣進行交互,那么它就也能夠與任何 ERC20 代幣進行交互。 這樣一來,將來你就可以輕松地將更多的代幣添加到你的應用中,而無需進行自定義編碼。 你可以簡單地插入新的代幣合約地址,然后嘩啦,你的應用程序有另一個它可以使用的代幣了。
其中一個例子就是交易所。 當交易所添加一個新的 ERC20 代幣時,實際上它只需要添加與之對話的另一個智能合約。 用戶可以讓那個合約將代幣發送到交易所的錢包地址,然后交易所可以讓合約在用戶要求取款時將代幣發送回給他們。
交易所只需要實現這種轉移邏輯一次,然后當它想要添加一個新的 ERC20 代幣時,只需將新的合約地址添加到它的數據庫即可。
其他代幣標準
對于像貨幣一樣的代幣來說,ERC20 代幣非常酷。 但是要在我們僵尸游戲中代表僵尸就并不是特別有用。
首先,僵尸不像貨幣可以分割 —— 我可以發給你 0.237 以太,但是轉移給你 0.237 的僵尸聽起來就有些搞笑。
其次,并不是所有僵尸都是平等的。 你的2級僵尸"Steve"完全不能等同于我732級的僵尸"H4XF13LD MORRIS 💯💯😎💯💯"。(你差得遠呢,Steve)。
有另一個代幣標準更適合如 CryptoZombies 這樣的加密收藏品——它們被稱為*ERC721 代幣.*
*ERC721 代幣*是不能互換的,因為每個代幣都被認為是唯一且不可分割的。 你只能以整個單位交易它們,并且每個單位都有唯一的 ID。 這些特性正好讓我們的僵尸可以用來交易。
請注意,使用像 ERC721 這樣的標準的優勢就是,我們不必在我們的合約中實現拍賣或托管邏輯,這決定了玩家能夠如何交易/出售我們的僵尸。 如果我們符合規范,其他人可以為加密可交易的 ERC721 資產搭建一個交易所平臺,我們的 ERC721 僵尸將可以在該平臺上使用。 所以使用代幣標準相較于使用你自己的交易邏輯有明顯的好處。
ERC721標準 多重繼承
我們先看看ERC721標準
contract ERC721 {event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);function balanceOf(address _owner) public view returns (uint256 _balance);function ownerOf(uint256 _tokenId) public view returns (address _owner);function transfer(address _to, uint256 _tokenId) public;function approve(address _to, uint256 _tokenId) public;function takeOwnership(uint256 _tokenId) public;
}
我們在代碼中應該如何使用ERC721,我們這邊首先應用erc721,然后再繼承他
pragma solidity ^0.4.19;import "./zombieattack.sol";
// 在這里引入文件
import "./erc721.sol";
// 在這里聲明 ERC721 的繼承
contract ZombieOwnership is ZombieAttack, ERC721 {
}
balanceOf和ownerOf
我們將實現兩個方法balanceO
f和ownerOf
balanceOf:這個函數只需要一個傳入 address
參數,然后返回這個 address
擁有多少代幣。
function balanceOf(address _owner) public view returns (uint256 _balance);
ownerOf:這個函數需要傳入一個代幣 ID 作為參數 (我們的情況就是一個僵尸 ID),然后返回該代幣擁有者的 address
。
function ownerOf(uint256 _tokenId) public view returns (address _owner);
ERC721轉移標準
把所有權從一個人轉移給另一個人來繼續我們的 ERC721 規范的實現。
注意 ERC721 規范有兩種不同的方法來轉移代幣:
function transfer(address _to, uint256 _tokenId) public;function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
- 第一種方法是代幣的擁有者調用
transfer
方法,傳入他想轉移到的address
和他想轉移的代幣的_tokenId
。 - 第二種方法是代幣擁有者首先調用
approve
,然后傳入與以上相同的參數。接著,該合約會存儲誰被允許提取代幣,通常存儲到一個mapping (uint256 => address)
里。然后,當有人調用takeOwnership
時,合約會檢查msg.sender
是否得到擁有者的批準來提取代幣,如果是,則將代幣轉移給他。
你注意到了嗎,transfer
和 takeOwnership
都將包含相同的轉移邏輯,只是以相反的順序。 (一種情況是代幣的發送者調用函數;另一種情況是代幣的接收者調用它)。
所以我們把這個邏輯抽象成它自己的私有函數 _transfer
,然后由這兩個函數來調用它。 這樣我們就不用寫重復的代碼了。
ERC721 批準 approve
現在,讓我們來實現 approve
。
記住,使用 approve
或者 takeOwnership
的時候,轉移有2個步驟:
- 你,作為所有者,用新主人的
address
和你希望他獲取的_tokenId
來調用approve
- 新主人用
_tokenId
來調用takeOwnership
,合約會檢查確保他獲得了批準,然后把代幣轉移給他。
因為這發生在2個函數的調用中,所以在函數調用之間,我們需要一個數據結構來存儲什么人被批準獲取什么。
合約安全增強:溢出和下溢
在編寫智能合約的時候需要注意的一個主要的安全特性:防止溢出和下溢。
什么是 溢出 (*overflow*)?
假設我們有一個 uint8
, 只能存儲8 bit數據。這意味著我們能存儲的最大數字就是二進制 11111111
(或者說十進制的 2^8 - 1 = 255).
來看看下面的代碼。最后 number
將會是什么值?
uint8 number = 255;
number++;
在這個例子中,我們導致了溢出 — 雖然我們加了1, 但是 number
出乎意料地等于 0
了。 (如果你給二進制 11111111
加1, 它將被重置為 00000000
,就像鐘表從 23:59
走向 00:00
)。
下溢(underflow
)也類似,如果你從一個等于 0
的 uint8
減去 1
, 它將變成 255
(因為 uint
是無符號的,其不能等于負數)。
雖然我們在這里不使用 uint8
,而且每次給一個 uint256
加 1
也不太可能溢出 (2^256 真的是一個很大的數了),在我們的合約中添加一些保護機制依然是非常有必要的,以防我們的 DApp 以后出現什么異常情況。
使用SafeMath
為了防止這些情況,OpenZeppelin 建立了一個叫做 SafeMath 的 庫(*library*),默認情況下可以防止這些問題。
不過在我們使用之前…… 什么叫做庫?
一個**庫** 是 Solidity 中一種特殊的合約。其中一個有用的功能是給原始數據類型增加一些方法。
比如,使用 SafeMath 庫的時候,我們將使用 using SafeMath for uint256
這樣的語法。 SafeMath 庫有四個方法 — add
, sub
, mul
, 以及 div
。現在我們可以這樣來讓 uint256
調用這些方法:
using SafeMath for uint256;uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
SafeMath的部分核心代碼
library SafeMath {function mul(uint256 a, uint256 b) internal pure returns (uint256) {if (a == 0) {return 0;}uint256 c = a * b;assert(c / a == b);return c;}function div(uint256 a, uint256 b) internal pure returns (uint256) {// assert(b > 0); // Solidity automatically throws when dividing by 0uint256 c = a / b;// assert(a == b * c + a % b); // There is no case in which this doesn't holdreturn 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;}
}
首先我們有了 library
關鍵字 — 庫和 合約
很相似,但是又有一些不同。 就我們的目的而言,庫允許我們使用 using
關鍵字,它可以自動把庫的所有方法添加給一個數據類型:
using SafeMath for uint;
// 這下我們可以為任何 uint 調用這些方法了
uint test = 2;
test = test.mul(3); // test 等于 6 了
test = test.add(5); // test 等于 11 了
注意 mul
和 add
其實都需要兩個參數。 在我們聲明了 using SafeMath for uint
后,我們用來調用這些方法的 uint
就自動被作為第一個參數傳遞進去了(在此例中就是 test
)
我們來看看 add
的源代碼看 SafeMath 做了什么:
function add(uint256 a, uint256 b) internal pure returns (uint256) {uint256 c = a + b;assert(c >= a);return c;
}
基本上 add
只是像 +
一樣對兩個 uint
相加, 但是它用一個 assert
語句來確保結果大于 a
。這樣就防止了溢出。
assert
和 require
相似,若結果為否它就會拋出錯誤。 assert
和 require
區別在于,require
若失敗則會返還給用戶剩下的 gas, assert
則不會。所以大部分情況下,你寫代碼的時候會比較喜歡 require
,assert
只在代碼可能出現嚴重錯誤的時候使用,比如 uint
溢出。
所以簡而言之, SafeMath 的 add
, sub
, mul
, 和 div
方法只做簡單的四則運算,然后在發生溢出或下溢的時候拋出錯誤。
通常情況下,總是使用 SafeMath 而不是普通數學運算是個好主意,也許在以后 Solidity 的新版本里這點會被默認實現,但是現在我們得自己在代碼里實現這些額外的安全措施。
不過我們遇到個小問題 — winCount
和 lossCount
是 uint16
, 而 level
是 uint32
。 所以如果我們用這些作為參數傳入 SafeMath 的 add
方法。 它實際上并不會防止溢出,因為它會把這些變量都轉換成 uint256
:
function add(uint256 a, uint256 b) internal pure returns (uint256) {uint256 c = a + b;assert(c >= a);return c;
}// 如果我們在`uint8` 上調用 `.add`。它將會被轉換成 `uint256`.
// 所以它不會在 2^8 時溢出,因為 256 是一個有效的 `uint256`.
這就意味著,我們需要再實現兩個庫來防止 uint16
和 uint32
溢出或下溢。我們可以將其命名為 SafeMath16
和 SafeMath32
。
代碼將和 SafeMath 完全相同,除了所有的 uint256
實例都將被替換成 uint32
或 uint16
。
我們已經將這些代碼幫你寫好了,打開 safemath.sol
合約看看代碼吧。
總結
本文研究了以太坊智能合約中代幣標準ERC20/ERC721的實現及其安全問題。首先介紹了ERC20代幣作為可互換資產的合約標準,分析了其balanceOf和transfer等核心功能。其次探討了ERC721代幣作為不可互換資產的特性,詳細說明了其多重繼承的實現方式。文章重點分析了ERC721的兩種所有權轉移機制:直接transfer和approve/takeOwnership組合。最后強調了智能合約安全中防范數據溢出和下溢的重要性,建議使用SafeMath庫來確保數值運算的安全性。通過標準代幣接口和安全數學運算,開發者可以構建更可靠、更安全的去中心化應用。