智能合約的安全性直接關係到投資者的財產與信任。雖然開發框架已經提供許多安全工具,但仍有三種漏洞——重入、時間戳依賴與整數溢位,經常被開發者忽略或誤用,導致可被利用的攻擊面。
本文將以輕鬆有趣的方式,拆解這三種常見漏洞:說明它們是什麼、為何容易被攻擊、如何寫出安全的程式碼,以及實際範例與防禦技巧,幫你在上線前先把這些漏洞「塞」進安全的馬桶。
常見漏洞類型概覽
以下列出三種最常見、也最容易被忽略的智能合約漏洞:
- 重入(Reentrancy)
- 時間戳依賴(Timestamp Dependency)
- 整數溢位(IntOverflow / IntUnderflow)
1️⃣ 重入(Reentrancy)
什麼是重入?
當合約在完成一筆交易前,將控制權交給另一個合約或外部帳戶,而那個對方再回呼本身,導致同一筆交易被執行多次。
簡易範例(Vulnerable)
pragma solidity ^0.8.0;
contract VulnerableBank {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 _amount) external {
require(balances[msg.sender] >= _amount, "Insufficient balance");
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Failed to send Ether");
balances[msg.sender] -= _amount; // ← 重入點
}
}
為什麼會被攻擊?
call{value} 先把 Ether 傳給外部帳戶,若該帳戶有 fallback 或 receive 函式再次呼叫 withdraw(),就能在尚未扣除 balance 前重複提款。
防禦技巧
- 先修改 state,再發送錢
balances[msg.sender] -= _amount; // 先扣除
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Failed to send Ether");
- 使用
ReentrancyGuard 或互斥鎖
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
// …
function withdraw(uint256 _amount) external nonReentrant { /* … */ }
}
2️⃣ 時間戳依賴(Timestamp Dependency)
什麼是時間戳?
區塊鏈的 block.timestamp(或 now)代表礦工打包區塊時的時間,範圍大約 ±15 秒。
常見陷阱
依賴 block.timestamp 決定遊戲規則、利率或隨機數,礦工可微調時間來影響結果。
範例(Vulnerable)
pragma solidity ^0.8.0;
contract Lottery {
address public owner;
uint256 public deadline;
address[] public participants;
constructor(uint256 _duration) {
owner = msg.sender;
deadline = block.timestamp + _duration; // 只用時間戳
}
function enter() external payable {
require(block.timestamp < deadline, "Lottery closed");
participants.push(msg.sender);
}
// … 其它抽獎邏輯
}
防禦方法
- 使用區塊號(block.number)作為定時
deadlineBlock = block.number + _durationBlocks;
- 限制時間範圍:在允許的 ±10 秒內判斷,減少礦工可調節空間。
- 不要用時間戳做隨機數:採用更安全的隨機來源(如 Chainlink VRF)。
3️⃣ 整數溢位 / 欠位(IntOverflow / IntUnderflow)
概念說明
Solidity 0.8.x 已內建檢查,但舊版或自訂數學函式仍可能溢位。
範例(Vulnerable)
pragma solidity ^0.6.12;
contract Token {
mapping(address => uint256) balances;
uint256 totalSupply = 1000 * 10**18;
function transfer(address _to, uint256 _value) public returns (bool) {
if (balances[msg.sender] < _value) revert();
balances[msg.sender] -= _value;
balances[_to] += _value; // 可能溢位
return true;
}
}
```**如何避免?**1.**使用 OpenZeppelin 的 SafeMath(舊版 Solidity)**```solidity
using SafeMath for uint256;
balances[_to] = balances[_to].add(_value);
2.直接使用 Solidity 0.8.x 的安全檢查3.在測試階段使用 forge test --ffi 或 Hardhat 的 chai-as-promised 來驗證數學運算4.審計時檢查所有 add, sub 位置---
小結
| 漏洞 |
典型攻擊方式 |
防禦關鍵 |
| 重入 |
連續提款、資金被盜 |
行為先改 state → 發送錢;使用 ReentrancyGuard |
| 時間戳依賴 |
礦工調整時間、隨機數被操縱 |
用 block.number 或限制 ±秒;避免時間戳做隨機 |
| 整數溢位 |
產生負值或過大 |
SafeMath 或 Solidity 0.8+ 安全檢查 |
提示:在開發過程中,務必使用自動化工具(Slither、MythX)和手動審計,確保合約在正式上鏈前不含上述漏洞。
參考資源