跳至內容
0%

12 個 Solidity 智能合約安全最佳實踐

Usman Asim headshot

作者 Usman Asim

發布於 2025年11月13日閱讀時間 4 分鐘

展示程式碼安全性的抽象盾牌圖示

智能合約是區塊鏈魔法背後的巫師。它們是自動執行鏈上邏輯的程式碼區塊,從去中心化借貸協議的清算門檻,到現實世界資產的代幣化,無所不包。

但能力越大,責任也越大,尤其是在安全性方面。一個 bug 就可能導致大規模的漏洞利用,沒有人希望自己的專案成為下一個數百萬美元駭客攻擊的頭條新聞。

僅在 2025 年上半年,就有超過 23 億美元的加密貨幣因漏洞利用和資安事件而流失,其中光是存取控制問題就佔了超過 16 億美元。如果你正在 Ethereum 或類似鏈上使用 Solidity 進行開發,把安全性做好不是可選項:這關係到用戶資金的安全,也關係到你的聲譽。

這篇文章會拆解智能合約安全性的真正含義,深入探討一些最重大的合約漏洞,並分享附有程式碼範例的實務最佳做法,幫助你把程式碼打造得更堅固,並在主網上部署安全的合約。

還有,記得在部署到主網之前一定要先在測試網上測試。在用戶開始把資金送進你的合約_之前_找到漏洞,遠比之後好。

什麼是智能合約安全性?

智能合約是在特定條件滿足時自動執行的程式碼片段。可以把它想成是不需要中間人、就能自動處理轉帳、投票、甚至複雜 DeFi 操作的自動化協議。它們以 bytecode 的形式部署在區塊鏈上,一旦上線就是不可變的,因此合約中的任何缺陷都會公開暴露。

這裡所謂的安全性,說到底就是確保程式碼萬無一失,因為合約本身經常直接保管鏈上資產。駭客能否從特定合約中掏空資金?合約邏輯在奇怪的邊界情況下是否依然成立?資產是否確實不會被未授權存取?

上述任一問題若存在漏洞,就意味著惡意行為者可能掏空合約中的用戶資金,占為己有。要防範這些漏洞,你需要在主網部署前,考慮實作輸入檢查、設計模式,以及能強化程式碼的程式碼審計。

想深入了解,可以參考 Ethereum 官方文件中關於智能合約安全性的說明

為什麼安全性對你很重要

建構不安全的合約不僅對用戶有風險,還可能讓你的專案一夜之間垮台。當一個專案被掏空,用戶會失去信任,而且往往不會再回來。區塊鏈的不可逆性意味著資金一旦消失,就是消失了。這裡沒有退款機制。就像資金可能瞬間歸零一樣,你的聲譽也可能瞬間毀掉。

隨著數十億美元的資金在鏈上流動,駭客始終在尋找弱點;近期的統計數字揭示了風險有多高:2025 年上半年就損失了 23 億美元。這些損失大多來自本可避免的 bug,例如糟糕的存取控制與 reentrancy。透過優先考慮安全性,你可以保護用戶、建立信任,並避免專案在一筆交易中歸零的噩夢。

市面上已有大量工具和最佳做法,你可以善用它們來撰寫不僅功能正常、還具備韌性的程式碼。

理解智能合約漏洞

理解智能合約漏洞的一個有用資源是 OWASP(Open Web Application Security Project 的縮寫),這是一個致力於在全球範圍內免費提供資源以改善軟體安全性的非營利組織。他們的 Smart Contract Top 10 就像是區塊鏈應用程式中最危險漏洞的通緝名單,內容根據真實世界的漏洞利用事件與造成數十億美元損失的資安事件資料整理而成。

這份清單根據各生態系駭客攻擊中的模式,為開發者提供了一份在審計和合約設計時應優先關注漏洞的清單。2025 年版本特別點出了存取缺陷等風險,光是去年就造成 9.53 億美元的損失。在審查程式碼時,留意這些常見漏洞是打好程式碼韌性基礎的絕佳框架。

  1. 存取控制漏洞:由於缺少權限檢查,讓未授權者得以竄改資料或函式的缺陷。這個問題排名第一是有原因的。想想被竊取的管理員金鑰把合約掏空的情境。簡單,但有效且普遍。
  2. 價格 oracle 操縱:駭客可以竄改外部資料來源(oracle)以扭曲價格,導致壞帳或不當交易。這在需要準確價格的 DeFi 中很常見。
  3. 邏輯錯誤:合約做出非預期行為的 bug,例如多鑄造代幣或錯誤計算獎勵。這種漏洞要求你涵蓋每一種邊界情況,需要對合約邏輯進行深入審計。
  4. 缺乏輸入驗證:不檢查用戶輸入,可能讓垃圾資料破壞邏輯或觸發溢位漏洞。
  5. Reentrancy 攻擊:外部呼叫可能讓駭客在執行過程中重新進入函式,往往用來重複提領資金。
  6. 未檢查的外部呼叫:未處理失敗的呼叫,可能導致合約在錯誤假設下繼續執行。
  7. 閃電貸攻擊:濫用即時貸款,在單筆交易中操縱市場或協議。
  8. 整數溢位與下溢:數字超出界限而環繞時發生的數學錯誤,可能導致計算錯誤或資產被竊。
  9. 不安全的隨機性:可預測的糟糕 RNG,可能在遊戲或抽獎中被利用。
  10. 阻斷服務(DoS)攻擊:以耗費大量 gas 的操作使合約超載,令其無法使用。

完整的 OWASP 詳細內容,請參閱他們的 Smart Contract Top 10 頁面。至於 Ethereum 特定的建議,請參閱他們的安全指南

12 個智能合約安全最佳做法

現在你已了解威脅全貌,讓我們深入探討實務上的防禦措施。OWASP 清單上的每個漏洞,都對應著經過數千個合約實戰考驗的最佳做法。

以下各節會逐一拆解這些常見缺陷,並附上具體的程式碼範例,展示有漏洞的模式與安全的實作方式。可以把這當成你的防禦手冊:只要持續套用這些簡單的技巧,就能縮小你的攻擊面。

1. 謹慎使用 delegatecall

Delegatecall 讓一個合約可以在使用自己儲存空間的同時執行另一個合約的程式碼——這對函式庫或升級來說非常有用,但也有風險,因為如果被呼叫的程式碼動了你的變數,可能導致意外的狀態變更。它是許多重大漏洞利用事件背後的元凶,駭客藉此注入惡意邏輯,覆寫像 owner 位址這類關鍵狀態,或掏空資金。

危險之處在於 delegatecall 會保留呼叫方合約的上下文(msg.sendermsg.value、storage),因此外部程式碼會以完整權限執行。只有在絕對必要時才使用它,並確保合約之間的儲存版面配置完全一致——錯位的 slot 可能會破壞你的資料。

以下是一個有漏洞的範例:

solidity
Copied
contract LibraryContract { address public owner; // Slot 0 function updateOwner() public { owner = msg.sender; // Changes caller's storage slot 0! } } contract VulnerableProxy { uint256 public value; // Slot 0 - MISMATCH! address public owner; // Slot 1 function delegateCall(address lib) public { // DANGER: updateOwner() will overwrite 'value', not 'owner'! lib.delegatecall(abi.encodeWithSignature("updateOwner()")); } }

以下是更安全的做法:

solidity
Copied
contract SecureProxy { address public implementation; address public owner; modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function execute(bytes memory data) public onlyOwner { (bool success,) = implementation.delegatecall(data); require(success, "Execution failed"); } }

最佳做法: 使用如 OpenZeppelin 的可升級合約等成熟的 proxy 模式,在不同版本之間維持完全相同的儲存版面配置(絕不要重新排序或改變變數型別),將 delegatecall 限制在受信任且經過審計的合約上,並考慮使用內建 library 關鍵字的函式庫,這類函式庫在底層安全地使用 delegatecall,且絕不允許用戶控制的位址出現在 delegatecall 中:那是一個立即遭到接管的攻擊途徑。

2. 使用 reentrancy guard

當外部呼叫在你的函式完成狀態更新之前就把控制權交出去時,就會發生 reentrancy,讓呼叫方得以在餘額更新之前重新進入並重複執行像提款這樣的動作。它在 OWASP 的 Web3 十大風險中排名第 5,並促成了加密貨幣圈一些最重大的駭客事件,例如 臭名昭著的 DAO 駭客事件(損失 6,000 萬美元)

攻擊者可以利用資金發送與狀態更新之間的這個 reentrancy 空窗期,透過遞迴呼叫提款函式來掏空合約。你應該始終假設外部合約是有敵意的。

有漏洞的範例:

solidity
Copied
contract VulnerableBank { mapping(address => uint256) public balances; function withdraw() public { uint256 bal = balances[msg.sender]; require(bal > 0); // DANGER: Sends Ether before updating balance! (bool sent,) = msg.sender.call{value: bal}(""); require(sent); balances[msg.sender] = 0; // Too late - already reentered! } } contract Attacker { VulnerableBank bank; fallback() external payable { if (address(bank).balance >= 1 ether) { bank.withdraw(); // Calls withdraw again! } } function attack() external payable { bank.withdraw(); // Starts the loop } }

安全的實作方式:

solidity

`import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureBank is ReentrancyGuard { mapping(address => uint256) public balances;

solidity
Copied
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract SecureBank is ReentrancyGuard { mapping(address => uint256) public balances; function withdraw() public nonReentrant { uint256 bal = balances[msg.sender]; require(bal > 0); // Update state BEFORE external call balances[msg.sender] = 0; (bool sent,) = msg.sender.call{value: bal}(""); require(sent); } }

最佳做法: 使用 OpenZeppelin 的 ReentrancyGuard 修飾詞,遵循 Checks-Effects-Interactions 模式(驗證 → 更新狀態 → 外部呼叫),並考慮採用 pull-over-push 模式,讓用戶自行提領資金。

3. 使用 msg.sender 而非 tx.origin 進行身分驗證

tx.origin 這個呼叫追溯回原始交易的發起者,而 msg.sender 則指的是直接呼叫者;這個區別在安全性上非常重要。在多合約呼叫鏈中,tx.origin 保持不變,但這正好可能被惡意的中間合約利用,誘騙你的程式碼誤以為原始用戶已獲授權。

攻擊者可以建立一個釣魚合約來呼叫你的函式,如果你檢查的是 tx.origin,它會指向發起交易的受害者,完全繞過你的身分驗證。msg.sender 則永遠是直接呼叫者,因此在存取控制上更可靠。只有在特別需要交易發起者的極少數情況下才使用 tx.origin,絕不要用於身分驗證。這個漏洞與 OWASP 排名第一的存取控制缺陷直接相關。

有漏洞的範例:

solidity
Copied
contract VulnerableWallet { address public owner; constructor() { owner = msg.sender; } function transfer(address payable to, uint256 amount) public { require(tx.origin == owner, "Not owner"); // BAD! to.transfer(amount); } } // Attacker tricks owner into calling this contract MaliciousContract { function attack(address wallet) public { // When owner calls this, tx.origin == owner// So the wallet thinks the call is authorized! VulnerableWallet(wallet).transfer(payable(msg.sender), 1 ether); } }

安全的實作方式:

solidity
Copied
contract SecureWallet { address public owner; constructor() { owner = msg.sender; } function transfer(address payable to, uint256 amount) public { require(msg.sender == owner, "Not owner"); // GOOD! to.transfer(amount); } }

為什麼這很重要: 如果 owner 不小心與惡意合約互動(點擊釣魚連結、使用被入侵的應用程式),該合約就能掏空這個有漏洞的錢包,因為 tx.origin 仍然指向該 owner。而使用 msg.sender 時,只有 owner 位址本身才能授權轉帳。務必確保在身分驗證與存取控制檢查中一律使用 msg.sender

4. 正確使用 Solidity 可見性修飾詞

可見性修飾詞定義了存取的邊界:public(不論內部或外部,任何人都可呼叫)、external(僅能從合約外部呼叫)、internal(本合約以及繼承此合約的合約)、以及 private(僅限本合約)。在較舊的 Solidity 版本中,省略修飾詞會預設為 public,這會不小心把敏感函式暴露給外部攻擊者。

錯誤的可見性設定曾導致駭客呼叫管理員函式或操縱本應受限的關鍵狀態變數。務必始終明確宣告可見性:這既是安全做法,也有助於提升可讀性。

solidity

`contract VulnerableBank { mapping(address => uint) balances;

solidity
Copied
contract VulnerableBank { mapping(address => uint) balances; // BAD: accidentally public in old Solidity function resetBalance(address user) { balances[user] = 0; // anyone can call this! } // GOOD: explicit visibility function deposit() external payable { balances[msg.sender] += msg.value; } function _updateInternal() internal { // only this contract + children } }

在部署前,使用像 Slither 這樣的靜態分析工具來抓出缺漏或錯誤的可見性修飾詞。

5. 避免 block timestamp 操縱

驗證者可以在一個小範圍內(在 Ethereum 上大約是 ±15 秒)操縱 block.timestamp,這使得它不適合用於精確計時或作為隨機性來源。礦工或驗證者可以調整時間戳以謀取自身利益,觸發撥款、贏得抽獎,或繞過基於時間的檢查。

因此,凡是秒數關鍵的重要邏輯,絕不要使用 block.timestamp,而且絕對不要用它來產生隨機數。它適合用於粗略的時間估計(例如「是否已過 24 小時?」),但用在精確條件上就很危險。

有漏洞的範例:

solidity
Copied
contract TimestampLottery { uint public lastPlay; fallback() external payable { require(msg.value == 10 ether); require(block.timestamp != lastPlay); lastPlay = block.timestamp; // BAD: validator can manipulate timestamp to win if (block.timestamp % 15 == 0) { payable(msg.sender).transfer(address(this).balance); } } }

計時方面更好的做法:

solidity
Copied
contract BlockBasedTiming { uint public startBlock = block.number; // Use block numbers instead (~12s per block on Ethereum) function isTimePassed(uint blocks) public view returns (bool) { return block.number >= startBlock + blocks; } }

至於隨機性,絕不要自行實作。應該改用 Chainlink VRF 或類似的可驗證隨機函式 oracle,它們能提供具密碼學安全性、不可竄改的隨機性。

6. 避免算術溢位與下溢

在 0.8 之前的 Solidity 版本中,整數在超過其最大值或最小值時會無聲地環繞。也就是說,uint8 在 255 時遞增會變成 0,或在 0 時遞減會變成 255。

這曾導致災難性的漏洞利用事件,攻擊者藉此無限鑄造代幣、製造出環繞成巨額數字的負餘額,或繞過關鍵檢查。

有漏洞的範例(0.8 之前的版本):

solidity
Copied
pragma solidity 0.7.0; contract UnsafeToken { mapping(address => uint8) public balances; function transfer(address to, uint8 amount) public { balances[msg.sender] -= amount; // Underflow: 0 - 1 = 255 balances[to] += amount; // Overflow: 255 + 1 = 0 } }

升級到 Solidity >=0.8 以取得自動溢位保護:

solidity
Copied
pragma solidity ^0.8.0; contract SafeToken { mapping(address => uint8) public balances; function transfer(address to, uint8 amount) public { balances[msg.sender] -= amount; // Reverts on underflow balances[to] += amount; // Reverts on overflow } }

如果你還卡在舊版 Solidity 上,可以使用 OpenZeppelin 的 SafeMath 函式庫。在 0.8 以上版本中,檢查機制已內建並會自動 revert,但如果你有意需要環繞行為以做 gas 最佳化,也可以使用 unchecked \{\} 區塊。

7. 實作穩健的存取控制

損壞的存取控制讓未授權用戶得以執行特權函式,這是 OWASP Web3 十大漏洞中排名第一的問題,僅在 2025 年上半年就造成了超過 16 億美元的損失。若缺乏適當的防護,攻擊者可以掏空資金、鑄造代幣、暫停合約,或變更擁有權。

常見的錯誤包括缺少修飾詞、依賴 tx.origin 進行身分驗證,或使用簡單的 require\(msg.sender == owner\) 檢查,而這種檢查在升級過程中很容易被忽略。解決方法是採用基於角色的存取控制,並遵循最小權限原則:每個位址只給予它絕對需要的權限。

使用 OpenZeppelin 的安全實作方式:

solidity
Copied
pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; contract SecureVault is AccessControl { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(ADMIN_ROLE, msg.sender); } function withdrawFunds() public onlyRole(ADMIN_ROLE) { // Only admins can withdraw } function updateConfig() public onlyRole(OPERATOR_ROLE) { // Operators handle config, not full admin } }

定期審核角色分配,對高風險變更實作延遲執行的管理員操作,並對關鍵角色使用多簽錢包。身分驗證絕不要使用 tx.origin。善用像 OpenZeppelin AccessControl 這類經過實戰考驗的函式庫,而不是自行從零開始建置。

8. 保護 oracle 整合免受操縱

Oracle 將區塊鏈與現實世界的資料結合,但如果它們是中心化的或容易被操縱,攻擊者就能餵入虛假資訊來利用你的合約。這類漏洞利用在 OWASP Web3 風險中排名第 2,已從 DeFi 協議中掏空數億美元。

閃電貸攻擊常常操縱鏈上價格 oracle(例如僅使用單一 DEX 作為價格來源),讓駭客得以人為地拉高或壓低價格,藉此清算部位、掏空流動性池,或鑄造抵押不足的貸款。絕不要依賴單一價格來源,或依賴能在一筆交易內被操縱的即時價格。

使用 Chainlink 的安全 oracle 用法:

solidity
Copied
pragma solidity ^0.8.20; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract SecurePriceFeed { AggregatorV3Interface internal priceFeed; uint256 private constant STALENESS_THRESHOLD = 3600; // 1 hour constructor(address _aggregator) { priceFeed = AggregatorV3Interface(_aggregator); } function getLatestPrice() public view returns (int) { ( uint80 roundId, int price, , uint256 updatedAt, uint80 answeredInRound ) = priceFeed.latestRoundData(); require(price > 0, "Invalid price"); require(answeredInRound >= roundId, "Stale price"); require(block.timestamp - updatedAt < STALENESS_THRESHOLD, "Price too old"); return price; } }

最佳做法:使用具有多個資料來源的去中心化 oracle 網路,例如 Chainlink;對關鍵操作實作時間加權平均價格(TWAP);驗證價格的新鮮度並檢查其是否落在合理範圍內;並在可能的情況下聚合多個 oracle。可查閱 Chainlink 文件以了解各網路特定的 feed 與安全性考量。

9. 徹底驗證所有輸入

未能驗證用戶輸入,會讓攻擊者得以注入惡意資料、觸發非預期行為,或利用程式碼中的邊界情況。這在 OWASP Web3 漏洞中排名第 4,也是掏空資金或破壞合約邏輯的常見手法。

若缺乏適當的檢查,用戶可能傳入零值以繞過手續費、傳入負值以利用算術漏洞、傳入指向零位址或惡意合約的位址,或傳入導致陣列越界存取的索引值。每一筆外部輸入在被證明安全之前都應被視為不可信。縱深防禦意味著要在每一個邊界都進行驗證:在處理任何用戶提供的資料之前,檢查範圍、空值、陣列長度以及業務邏輯限制。

正確的輸入驗證:

solidity
Copied
pragma solidity ^0.8.20; contract SecureDeposit { mapping(address => uint256) public balances; uint256 public constant MAX_DEPOSIT = 1000 ether; function deposit(uint256 amount) public payable { require(amount > 0, "Amount must be positive"); require(amount == msg.value, "Amount mismatch"); require(amount <= MAX_DEPOSIT, "Exceeds max deposit"); require( amount <= type(uint256).max - balances[msg.sender], "Balance overflow" ); balances[msg.sender] += amount; } function transfer(address to, uint256 amount) public { require(to != address(0), "Invalid recipient"); require(to != address(this), "Cannot transfer to contract"); require(amount <= balances[msg.sender], "Insufficient balance"); balances[msg.sender] -= amount; balances[to] += amount; } }

}`

務必驗證金額為非零且在合理範圍內、位址並非空值或無效值、陣列索引在有效範圍內,以及業務邏輯限制都已被滿足。

在 Solidity 0.8.4 以上版本中,使用自訂錯誤來取得具備詳細訊息、且更省 gas 的 revert 方式。

10. 安全地處理外部呼叫

如果你不檢查回傳值,對其他合約的外部呼叫可能會無聲地失敗。這個問題在 OWASP Web3 風險中排名第 6,曾導致資金卡住,或是合約在操作實際失敗時卻誤以為成功。

call\(\)delegatecall\(\)send\(\) 這類底層呼叫回傳的是布林值來表示成功與否,而不是自動 revert。如果你忽略這些回傳值,你的合約可能會在錯誤假設下繼續執行,誤以為資金已轉出,或誤以為某個關鍵操作已完成而其實已失敗。ERC-20 的 transfer\(\) 在某些回傳 false 而非 revert 的實作中,也存在同樣的問題。

有漏洞的模式:

solidity
Copied
function unsafeTransfer(address payable recipient) public { recipient.send(1 ether); // Returns false on failure, but continues!// Contract thinks transfer succeeded }

安全的實作方式:

solidity
Copied
contract SecureCaller { event CallExecuted(address target, bool success); function safeCall(address target, bytes memory data) public returns (bytes memory) { (bool success, bytes memory result) = target.call(data); require(success, "External call failed"); emit CallExecuted(target, success); return result; } function safeTransfer(address payable recipient, uint256 amount) public { (bool success, ) = recipient.call{value: amount}(""); require(success, "Transfer failed"); } // For ERC-20 tokens, use SafeERC20 from OpenZeppelin }

}`

務必擷取並檢查外部呼叫的回傳值。對於 Ether 轉帳,優先使用 call\{value: x\}\(""\),而非已棄用的 send\(\)transfer\(\)。對於代幣轉帳,使用 OpenZeppelin 的 SafeERC20 wrapper,它能處理那些失敗時不會 revert 的非標準 ERC-20 實作。

11. 緩解閃電貸攻擊

閃電貸允許在同一筆交易內、無需抵押就借出巨額資金,只要在交易結束前還清即可。攻擊者可以利用這個邏輯來操縱價格、掏空資金池,或利用協議邏輯漏洞,這使它在 OWASP Web3 風險中排名第 7,在整個 DeFi 領域造成了數十億美元的損失。

駭客會利用閃電貸取得的資金,人為地扭曲 oracle 價格、大規模利用捨入誤差,或製造出能觸發脆弱合約邏輯的暫時性市場條件。一種在多次駭客事件中反覆出現的經典模式是:借入數百萬資金、操縱價格 oracle 或流動性池、利用你協議中的錯誤定價、償還貸款,並把差額收入囊中——這一切都在一筆交易內原子性地完成。

有漏洞的模式:

solidity
Copied
contract VulnerablePool { function getPrice() public view returns (uint) { // BAD: using spot price that can be manipulated return tokenReserve / ethReserve; } function borrow(uint amount) public { uint collateral = getPrice() * userCollateral[msg.sender]; require(collateral >= amount, "Insufficient collateral"); // Attacker manipulates price in same transaction! } }

更好的做法:

solidity
Copied
contract SecureLending { mapping(address => uint256) public lastBorrow; function borrow(uint256 amount) public { // Add cooldown between borrows require(block.number > lastBorrow[msg.sender] + 2, "Too soon"); lastBorrow[msg.sender] = block.number; // Use TWAP or Chainlink instead of spot price uint256 price = chainlinkOracle.getPrice(); uint256 collateral = price * userCollateral[msg.sender]; require(collateral * 150 / 100 >= amount, "Need 150% collateral"); } }

防禦策略:對於關鍵操作,使用時間加權平均價格(TWAP)或 Chainlink,而非即時價格。為關鍵操作加入跨多個區塊的冷卻期,要求超額抵押,並在狀態變更後驗證協議的健康狀況。如果你不需要閃電貸功能,可以考慮直接完全封鎖它。

12. 避免關鍵函式中的邏輯錯誤

關鍵函式中的邏輯錯誤會破壞合約核心的安全假設,後果可能是災難性的。這類錯誤在 OWASP Web3 風險中排名第 3,因為儘管程式碼在技術上「正確」,這些缺陷卻會破壞整個系統。

與編譯器能抓出的語法錯誤不同,邏輯錯誤能通過所有檢查,卻產生錯誤的結果:迴圈中的差一錯誤、錯誤的運算順序、缺少邊界情況處理,或有缺陷的條件判斷。常見的例子包括轉帳後忘記更新餘額、使用 &gt;= 而非 &gt;,或以錯誤的順序計算手續費而導致精度損失。

常見的邏輯錯誤:

solidity
Copied
contract LogicErrors { mapping(address => uint256) public balances; // ERROR 1: Balance updated AFTER transfer (reentrancy risk!) function withdraw(uint256 amount) public { payable(msg.sender).transfer(amount); balances[msg.sender] -= amount; // Too late! } // ERROR 2: Off-by-one lets loop access invalid index function distribute(address[] memory users) public { for(uint i = 0; i <= users.length; i++) { // Should be // Crashes on last iteration! } } }

修正後的版本:

solidity
Copied
contract SecureLogic { mapping(address => uint256) public balances; function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance"); // Update state BEFORE external call balances[msg.sender] -= amount; payable(msg.sender).transfer(amount); } function distribute(address[] memory users) public { for(uint i = 0; i < users.length; i++) { // Correct boundary// Safe iteration } } }

防禦策略: 遵循 checks-effects-interactions 模式(驗證 → 更新狀態 → 外部呼叫)、為邊界情況(零值、最大值、空陣列)撰寫單元測試、使用 Foundry 的 fuzzing 功能測試隨機輸入,並定義像「已分配總量絕不應超過資金池餘額」這樣的不變量。對關鍵函式務必進行同儕審查。

6 個熱門的智能合約安全工具

上述最佳做法有助於提升韌性,但你也需要能捕捉肉眼可能遺漏的錯誤的工具。以下是一些用於審計程式碼的經典與現代工具組合:

  1. Slither:一款具備 40 多種偵測器的靜態分析工具,能偵測缺陷。非常適合快速掃描,也能列印出合約詳情。GitHub
  2. Mythril:一款針對多條鏈的 EVM bytecode 分析工具,能發現符號式問題。屬於 MythX 的一部分。GitHub
  3. Securify:由 Ethereum Foundation 支持的掃描工具,能偵測 37 種以上的缺陷,具備精確的靜態分析能力。網站(已不再提供)。
  4. Foundry:一套現代化的 Solidity 測試/fuzzing 框架,提供在抓出邏輯 bug 上表現出色的不變量測試(invariant testing)。Book
  5. Certora:一款形式驗證工具,能以數學方式證明程式碼符合規格。Website
  6. Echidna:一款基於屬性的漏洞模糊測試(fuzzer)工具。GitHub

除了這 6 個工具之外,你也可以與像 Code4rena 這樣的平台合作,進行懸賞制審計。

保護你的智能合約:結語

作為開發者,你的目標很單純:打造不會被駭的合約,確保用戶資金安全,並讓你的應用程式順暢運作。在這個過程中,你應該採用上述的最佳做法,並秉持「安全設計優先」的心態。

使用可升級的 proxy(透過 OpenZeppelin Upgrades)、模組化程式碼,以及帳戶抽象化(例如 ERC-4337,能提供更好的使用體驗與安全性——詳情可參考我們的 ERC-4337 指南)。執行單元測試、形式驗證、專業審計、執行期監控(例如透過 Fortress),並在 Immunefi 上設立漏洞懸賞計畫。你的用戶(以及專案的成敗)都仰賴這些措施。

想了解更多關於合約安全性的資源,可以參考以下連結:

常見問題

撰寫 Solidity 智能合約時,最重要的安全模式有哪些?

最關鍵的模式包括使用 checks-effects-interactions、實作 reentrancy guard、驗證所有輸入、明確設定可見性修飾詞,以及採用基於角色權限的正確存取控制。

我該如何防止合約中的 reentrancy 攻擊?

使用 OpenZeppelin 的 ReentrancyGuard 修飾詞、遵循 checks-effects-interactions 模式,在進行外部呼叫之前先更新合約狀態,並考慮採用 pull-over-push 模式,讓用戶自行提領資金。

為什麼身分驗證應該使用 msg.sender 而非 tx.origin?

tx.origin 可能被惡意的中間合約利用,誘騙你的程式碼誤以為原始用戶已獲授權,而 msg.sender 永遠是直接呼叫者,因此在存取控制上更為可靠。

我該如何防範 Solidity 中的算術溢位與下溢?

升級到 Solidity 0.8.x 或更新版本,它內建了溢位/下溢檢查機制,發生錯誤時會自動 revert。若使用舊版本,則應使用像 OpenZeppelin 的 SafeMath 這類經過充分測試的函式庫。

測試與安全審計在智能合約安全性中扮演什麼角色?

完整的單元測試、使用 Foundry 等工具進行 fuzzing、以及使用 Slither 進行靜態分析,都有助於發現邊界情況與漏洞,而獨立的安全審計則能在主網部署前找出邏輯缺陷。

我該如何在 Solidity 中安全地處理 delegatecall?

只在使用受信任且經過審計的實作時使用 delegatecall,絕不用於用戶控制的位址,在合約之間維持完全相同的儲存版面配置,並優先採用像 OpenZeppelin 可升級合約這類成熟的 proxy 模式。

我該如何保護智能合約免受閃電貸攻擊?

對關鍵操作使用時間加權平均價格(TWAP)或 Chainlink oracle,而非即時價格;為關鍵操作實作跨多個區塊的冷卻期;要求超額抵押;並在狀態變更後驗證協議的健康狀況。

智能合約安全測試應該使用哪些工具?

熱門工具包括用於靜態分析的 Slither、用於測試與 fuzzing 的 Foundry、用於 bytecode 分析的 Mythril,以及可在部署前進行全面漏洞掃描的 Securify。

Background gradient

打造區塊鏈魔法

Alchemy 結合最強大的 Web3 開發者產品與工具,並提供資源、社群與卓越的支援。