帶有安全措施的 ERC-20
介紹
以太坊的一大優點是沒有中央機構可以修改或撤銷您的交易。 以太坊的一大問題是沒有中央機構有權力撤銷使用者錯誤或非法交易。 在本文中,您將了解使用者在使用 ERC-20 代幣時常犯的一些錯誤,以及如何創建 ERC-20 合約來幫助使用者避免這些錯誤,或賦予中央機構某些權力(例如凍結帳戶)。
請注意,雖然我們將使用 OpenZeppelin ERC-20 代幣合約 (opens in a new tab),但本文不會詳細解釋它。 您可以在此處找到此資訊。
如果您想查看完整的原始碼:
- 開啟 Remix IDE (opens in a new tab)。
- 點擊複製 github 圖示 (
)。 - 複製 github 儲存庫
https://github.com/qbzzt/20220815-erc20-safety-rails。 - 開啟 contracts > erc20-safety-rails.sol。
建立 ERC-20 合約
在新增安全措施功能之前,我們需要一個 ERC-20 合約。 在本文中,我們將使用 OpenZeppelin Contracts Wizard (opens in a new tab)。 在另一個瀏覽器中開啟它,並按照以下說明操作:
-
選擇 ERC20。
-
輸入以下設定:
參數 數值 名稱 SafetyRailsToken 符號 SAFE Premint 1000 功能 無 存取控制 Ownable 可升級性 無 -
向上捲動並點擊 在 Remix 中開啟 (適用於 Remix) 或 下載 以使用不同的環境。 我將假設您正在使用 Remix,如果您使用其他工具,請進行相應的變更。
-
我們現在有了一個功能齊全的 ERC-20 合約。 您可以展開
.deps>npm以查看匯入的程式碼。 -
編譯、部署並操作合約,以確認它能作為 ERC-20 合約運作。 如果您需要學習如何使用 Remix,請使用此教學 (opens in a new tab)。
常見錯誤
錯誤
使用者有時會將代幣傳送到錯誤的地址。 雖然我們無法讀懂他們的心思來了解他們想做什麼,但有兩種經常發生且易於偵測的錯誤類型:
-
將代幣傳送到合約自己的地址。 例如,Optimism 的 OP 代幣 (opens in a new tab) 在不到兩個月的時間裡累積了超過 120,000 (opens in a new tab) 個 OP 代幣。 這代表著一筆巨大的財富,推測是人們剛剛損失的。
-
將代幣傳送到一個空地址,該地址不對應於外部擁有帳戶或智能合約。 雖然我沒有關於這種情況發生頻率的統計數據,但一次事件可能造成 20,000,000 個代幣的損失 (opens in a new tab)。
防止轉帳
OpenZeppelin ERC-20 合約包含一個掛鉤 _beforeTokenTransfer (opens in a new tab),它在代幣轉移之前被調用。 預設情況下,這個掛鉤不做任何事情,但我們可以在其上掛載自己的功能,例如在出現問題時回復的檢查。
要使用此掛鉤,請在建構函式後新增此函式:
1 function _beforeTokenTransfer(address from, address to, uint256 amount)2 internal virtual3 override(ERC20)4 {5 super._beforeTokenTransfer(from, to, amount);6 }如果您對 Solidity 不太熟悉,此函式的某些部分可能對您來說是新的:
1 internal virtualvirtual 關鍵字表示,正如我們從 ERC20 繼承功能並覆寫此函式一樣,其他合約也可以從我們這裡繼承並覆寫此函式。
1 override(ERC20)我們必須明確指定我們正在覆寫 (opens in a new tab) _beforeTokenTransfer 的 ERC20 代幣定義。 一般來說,從安全角度來看,明確的定義比隱含的定義好得多——如果事情就在您眼前,您就不會忘記您做了什麼。 這也是我們需要指定我們正在覆寫哪個父類的 _beforeTokenTransfer 的原因。
1 super._beforeTokenTransfer(from, to, amount);此行調用我們從其繼承的合約中擁有 _beforeTokenTransfer 函式的函式。 在這種情況下,只有 ERC20 有這個掛鉤,Ownable 沒有。 儘管目前 ERC20._beforeTokenTransfer 不做任何事情,我們還是調用它,以防將來新增功能(然後我們決定重新部署合約,因為合約在部署後不會改變)。
編寫要求
我們希望向函式新增這些要求:
to地址不能等於address(this),即 ERC-20 合約本身的地址。to地址不能為空,它必須是:- 一個外部擁有帳戶 (EOA)。 我們無法直接檢查一個地址是否為 EOA,但我們可以檢查一個地址的 ETH 餘額。 EOA 幾乎總是有餘額,即使它們不再使用——很難將它們清零到最後一個 wei。
- 一個智能合約。 測試一個地址是否為智能合約有點困難。 有一個檢查外部程式碼長度的 opcode,稱為
EXTCODESIZE(opens in a new tab),但它在 Solidity 中不能直接使用。 我們必須為此使用 Yul (opens in a new tab),它是一種 EVM 組合語言。 我們可以使用 Solidity 中的其他值(<address>.code和<address>.codehash(opens in a new tab)),但它們的成本更高。
讓我們逐行查看新的程式碼:
1 require(to != address(this), "Can't send tokens to the contract address");這是第一個要求,檢查 to 和 this(address) 是否不是同一個東西。
1 bool isToContract;2 assembly {3 isToContract := gt(extcodesize(to), 0)4 }這是我們檢查一個地址是否為合約的方式。 我們無法直接從 Yul 接收輸出,因此我們定義一個變數來儲存結果(在本例中為 isToContract)。 Yul 的工作方式是每個 opcode 都被視為一個函式。 所以首先我們調用 EXTCODESIZE (opens in a new tab) 來獲取合約大小,然後使用 GT (opens in a new tab) 來檢查它是否不為零(我們處理的是無符號整數,所以它當然不能是負數)。 然後我們將結果寫入 isToContract。
1 require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");最後,我們有了對空地址的實際檢查。
管理員存取權
有時,有一個可以撤銷錯誤的管理員是很有用的。 為了減少濫用的可能性,這個管理員可以是一個多重簽名 (opens in a new tab),這樣就需要多個人同意一項操作。 在本文中,我們將介紹兩種管理功能:
-
凍結和解凍帳戶。 這很有用,例如,當一個帳戶可能被盜用時。
-
資產清理。
有時,詐騙者會將欺詐性代幣傳送到真實代幣的合約中以獲得合法性。 例如,請看這裡 (opens in a new tab)。 合法的 ERC-20 合約是 0x4200....0042 (opens in a new tab)。 冒充它的詐騙是 0x234....bbe (opens in a new tab)。
人們也可能錯誤地將合法的 ERC-20 代幣傳送到我們的合約中,這也是我們希望有辦法將它們取出的另一個原因。
OpenZeppelin 提供了兩種啟用管理員存取權的機制:
Ownable(opens in a new tab) 合約只有一個擁有者。 具有onlyOwner修飾符 (opens in a new tab)的函式只能由該擁有者調用。 擁有者可以將所有權轉讓給其他人或完全放棄。 所有其他帳戶的權利通常是相同的。AccessControl(opens in a new tab) 合約具有基於角色的存取控制 (RBAC) (opens in a new tab)。
為簡單起見,本文我們使用 Ownable。
凍結和解凍合約
凍結和解凍合約需要進行幾項變更:
-
一個從地址到布林值 (opens in a new tab)的對應 (opens in a new tab),用於追蹤哪些地址被凍結。 所有值最初都為零,對於布林值,這被解釋為 false。 這就是我們想要的,因為預設情況下帳戶是未凍結的。
1 mapping(address => bool) public frozenAccounts; -
當帳戶被凍結或解凍時,使用事件 (opens in a new tab)通知任何感興趣的人。 從技術上講,這些操作不需要事件,但它有助於鏈外程式碼能夠監聽這些事件並了解正在發生的事情。 當發生可能與他人相關的事情時,智能合約發出事件被認為是一種良好的習慣。
事件被索引,因此可以搜尋帳戶被凍結或解凍的所有次數。
1 // 當帳戶被凍結或解凍時2 event AccountFrozen(address indexed _addr);3 event AccountThawed(address indexed _addr); -
用於凍結和解凍帳戶的函式。 這兩個函式幾乎相同,所以我們只會介紹凍結函式。
1 function freezeAccount(address addr)2 public3 onlyOwner標記為
public(opens in a new tab) 的函式可以從其他智能合約或直接透過交易調用。1 {2 require(!frozenAccounts[addr], "Account already frozen");3 frozenAccounts[addr] = true;4 emit AccountFrozen(addr);5 } // freezeAccount如果帳戶已凍結,則回復。 否則,凍結它並
emit一個事件。 -
變更
_beforeTokenTransfer以防止資金從凍結帳戶中移出。 請注意,資金仍然可以轉入凍結帳戶。1 require(!frozenAccounts[from], "The account is frozen");
資產清理
要釋放此合約持有的 ERC-20 代幣,我們需要調用它們所屬代幣合約上的一個函式,可以是 transfer (opens in a new tab) 或 approve (opens in a new tab)。 在這種情況下,在授權上浪費 Gas 是沒有意義的,我們不如直接轉帳。
1 function cleanupERC20(2 address erc20,3 address dest4 )5 public6 onlyOwner7 {8 IERC20 token = IERC20(erc20);這是我們在收到地址時為合約建立物件的語法。 我們可以這樣做,因為我們的原始碼中包含了 ERC20 代幣的定義(參見第 4 行),並且該檔案包含了 IERC20 的定義 (opens in a new tab),這是 OpenZeppelin ERC-20 合約的介面。
1 uint balance = token.balanceOf(address(this));2 token.transfer(dest, balance);3 }這是一個清理函式,所以我們大概不希望留下任何代幣。 與其手動從使用者那裡獲取餘額,我們不如自動化這個過程。
結論
這不是一個完美的解決方案——對於「使用者犯錯」這個問題,沒有完美的解決方案。 然而,使用這類檢查至少可以防止一些錯誤。 凍結帳戶的能力雖然危險,但可以用來限制某些駭客攻擊的損害,方法是拒絕駭客竊取資金。
在此查看我的更多作品 (opens in a new tab)。
頁面最後更新時間: 2026年3月3日