Solidity 合約安全,常見漏洞(第三篇)
ERC20 代幣問題
如果你只處理受信任的 ERC20 代幣,這些問題大多不適用。然而,當與任意的或部分不受信任的 ERC20 代幣交互時,就有一些需要注意的地方。
ERC20:轉(zhuǎn)賬扣費
當與不信任的代幣打交道時,你不應(yīng)該認為你的余額一定會增加那么多。一個 ERC20 代幣有可能這樣實現(xiàn)它的轉(zhuǎn)賬函數(shù),如下所示:
contract ERC20 {
// internally called by transfer() and transferFrom()
// balance and approval checks happen in the caller
function _transfer(address from, address to, uint256 amount) internal returns (bool) {
fee = amount * 100 / 99;
balanceOf[from] -= to;
balanceOf[to] += (amount - fee);
balanceOf[TREASURY] += fee;
emit Transfer(msg.sender, to, (amount - fee));
return true;
}
}
這種代幣對每筆交易都會征收 1%的稅。因此,如果一個智能合約與該代幣進行如下交互,我們將得到意想不到的回退或資產(chǎn)被盜。
contract Stake {
mapping(address => uint256) public balancesInContract;
function stake(uint256 amount) public {
token.transferFrom(msg.sender, address(this), amount);
balancesInContract[msg.sender] += amount; // 這是錯誤的
}
function unstake() public {
uint256 toSend = balancesInContract[msg.sender];
delete balancesInContract[msg.sender];
// this could revert because toSend is 1% greater than
// the amount in the contract. Otherwise, 1% will be "stolen"http:// from other depositors.
token.transfer(msg.sender, toSend);
}
}
ERC20: rebase 的代幣
Rebasing 代幣由 Olympus DAO 的 sOhm 代幣 和 Ampleforth 的 AMPL 代幣所推廣。Coingecko 維護了一個 Rebasing ERC20 代幣的列表。
當一個代幣回溯時,總發(fā)行量會發(fā)生變化,每個人的余額會根據(jù)回溯的方向而增加或減少。
在處理 rebase 代幣時,以下代碼可能會被破壞:
contract WillBreak {
mapping(address => uint256) public balanceHeld;
IERC20 private rebasingToken
function deposit(uint256 amount) external {
balanceHeld[msg.sender] = amount;
rebasingToken.transferFrom(msg.sender, address(this), amount);
}
function withdraw() external {
amount = balanceHeld[msg.sender];
delete balanceHeld[msg.sender];
// 錯誤, amount 也許會超出轉(zhuǎn)出范圍
rebasingToken.transfer(msg.sender, amount);
}
}
許多合約的解決方案是簡單地不允許 rebase 代幣。然而,我們可以修改上面的代碼,在將賬戶余額轉(zhuǎn)給接受者之前檢查 balanceOf(address(this))。那么,即使余額發(fā)生變化,它仍然可以工作。
ERC20: ERC777 在 ERC20 上的包裹
ERC20,如果按照標準實現(xiàn),ERC20 代幣沒有轉(zhuǎn)賬鉤子(hook),因此 transfer 和 transferFrom 不會有重入問題。
帶有轉(zhuǎn)賬鉤子的代幣有應(yīng)用優(yōu)勢,這就是為什么所有的 NFT 標準都實現(xiàn)了它們,以及為什么 ERC777 被最終確定。然而,這已經(jīng)引起了足夠的混亂,以至于 Openzeppelin 廢止了 ERC777 庫。
如果你只想讓你的協(xié)議與那些行為像 ERC20 代幣但有轉(zhuǎn)賬 hook 的代幣兼容,那么這只是一個簡單的問題,把 transfer 和 transferFrom 函數(shù)當作它們會向接收者進行一個函數(shù)調(diào)用即可。
這種 ERC777 的重入發(fā)生在 Uniswap 身上(如果你好奇,Openzeppelin 在這里記錄了這個漏洞)。
ERC20: 不是所有的 ERC20 代幣轉(zhuǎn)賬都會返回 true
ERC20 規(guī)范規(guī)定,ERC20 代幣在轉(zhuǎn)賬成功時必須返回 true。因為大多數(shù) ERC20 的實現(xiàn)不可能失敗,除非授權(quán)不足或轉(zhuǎn)賬的金額太多,大多數(shù)開發(fā)者已經(jīng)習慣于忽略 ERC20 代幣的返回值,并假設(shè)一個失敗的 trasfer 將被回退。
坦率地說,如果你只與一個你知道其行為的受信任的 ERC20 代幣打交道,這并不重要。但在處理任意的 ERC20 代幣時,必須考慮到這種行為上的差異。
在許多合約中都有一個隱含的期望,即失敗的轉(zhuǎn)賬應(yīng)該總是回退,而不是返回錯誤,因為大多數(shù) ERC20 代幣沒有返回錯誤的機制,所以這導致了很多混亂。
使這個問題更加復雜的是,一些 ERC20 代幣并不遵循返回 true 的協(xié)議,特別是 Tether。一些代幣在轉(zhuǎn)賬失敗后會回退,這將導致回退的結(jié)果冒泡到調(diào)用者。因此,一些庫包裹了 ERC20 代幣的轉(zhuǎn)賬調(diào)用,以回退恢復并返回一個布爾值。下面是一些實現(xiàn)方法:
參考:Openzeppelin SafeTransfer 及 Solady SafeTransfer (大大地提高了 Gas 效率)
ERC20: 地址投毒
這不是一個智能合約的漏洞,但為了完整起見,我們在這里提到它。
轉(zhuǎn)賬零代幣是 ERC20 規(guī)范所允許的。這可能會導致前端應(yīng)用程序的混亂,并可能欺騙用戶,讓他們錯誤的以為他們最近將代幣發(fā)送給了某地址。Metamask在這個線程中有更多關(guān)于這個問題的內(nèi)容。
ERC20: 查看代碼,規(guī)避跑路
(在 web3 術(shù)語中,“rugged"意味著’'跑路”, 直譯是"從你腳下拉出地毯" 。)
沒有什么能阻止有人在 ERC20 代幣上添加函數(shù),讓他們隨意創(chuàng)建、轉(zhuǎn)賬和銷毀代幣–或自毀或升級。所以從根本上說,ERC20 代幣的 “無需信任” 程度是有限制的。
借貸協(xié)議中的邏輯錯誤
當考慮到基于 DeFi 協(xié)議的借貸如何被破壞時,考慮在軟件層面?zhèn)鞑サ?bug 并影響商業(yè)邏輯層面是很有幫助的。形成和完成一個債券合約有很多步驟。這里有一些需要考慮的攻擊向量。
貸款人損失的方式
- 使到期本金減少(可能為零)而不進行任何支付的漏洞。
- 當貸款沒有償還或抵押物降到閾值以下時,買方的抵押物不能被清算。
- 如果協(xié)議有一個轉(zhuǎn)移債務(wù)所有權(quán)的機制,這可能是一個從貸款人那里偷取債券的方式。
- 貸款本金或付款的到期日被不適當?shù)匾频揭院蟮娜掌凇?/li>
借款人損失的方式
- 償還本金時沒有減少本金債務(wù)的 bug。
- 一個 bug 或 gas 攻擊使用戶無法進行支付。
- 本金或利率被非法提高。
- 預言機的操縱導致抵押物貶值。
- 貸款本金或付款的到期日被不適當?shù)匾频揭粋€較早的日期。
如果抵押品從協(xié)議中被抽走,那么貸款人和借款人都會損失,因為借款人沒有動力去償還貸款,而借款人則會損失本金。
正如上面所看到的,DeFi 協(xié)議被 "黑 "的范圍比從協(xié)議中抽走一堆錢(通常成為新聞的那類事件)要多得多。
抵押(staking)協(xié)議中的漏洞
成為新聞的那種黑客是抵押協(xié)議被黑掉數(shù)百萬美元,但這并不是唯一要面對的問題,抵押協(xié)議可能面臨的問題有:
- 獎勵能否延遲支付,或過早地被索???
- 獎勵能否被不適當?shù)販p少或增加?在更糟糕的情況下,能否阻止用戶獲得任何獎勵?
- 人們能否索取不屬于他們的本金或獎勵,在最壞的情況下,會耗盡協(xié)議所有資金?
- 存放的資產(chǎn)會不會被卡在協(xié)議中(部分或全部),或被不適當?shù)匮舆t提?。?/li>
- 相反,如果質(zhì)押需要時間承諾,用戶是否可以在承諾時間之前提???
- 如果支付的是不同的資產(chǎn)或貨幣,其價值是否可以在相關(guān)的智能合約范圍內(nèi)被操縱?如果協(xié)議 mint 自己的代幣來獎勵流動性提供者或質(zhì)押者,這一點是相關(guān)的。
- 如果存在預期和披露出的本金損失的風險因素,這種風險是否可以被不適當?shù)夭倏v?
- 協(xié)議的關(guān)鍵參數(shù)是否有管理、中心化或治理風險?
需要關(guān)注的關(guān)鍵是代碼中涉及 "資金退出 "部分的代碼。
還有一個 "資金入口 "的漏洞也要尋找。
- 有權(quán)參與協(xié)議中的資產(chǎn)抵押的用戶能否被不適當?shù)刈柚梗?/li>
用戶收到的獎勵有一個隱含的風險回報和一個預期的資金時間價值。明確這些假設(shè)是什么,以及協(xié)議會怎樣偏離預期是很有幫助的。
未檢查的返回值
有兩種方法來調(diào)用外部智能合約:1)用接口定義調(diào)用函數(shù);2)使用.call 方法。如下圖所示:
contract A {
uint256 public x;
function setx(uint256 _x) external {
require(_x > 10, "x must be bigger than 10");
x = _x;
}
}
interface IA {
function setx(uint256 _x) external;
}
contract B {
function setXV1(IA a, uint256 _x) external {
a.setx(_x);
}
function setXV2(address a, uint256 _x) external {
(bool success, ) =
a.call(abi.encodeWithSignature("setx(uint256)", _x));
// success is not checked!
}
}
在合約 B 中,如果 _x 小于 10,setXV2 會默默地失敗。當一個函數(shù)通過.call 方法被調(diào)用時,被調(diào)用者可以回退,但父函數(shù)不會回退。必須檢查返回成功的值,并且代碼行為必須相應(yīng)地分支。
msg.value 在一個循環(huán)中
在循環(huán)中使用 msg.value 是很危險的,因為這可能會讓發(fā)起者 重復使用 msg.value。
這種情況可能會出現(xiàn)在 payable 的 multicalls 中。Multicalls 使用戶能夠提交一個交易列表,以避免重復支付 21,000 的 Gas 交易費。然而,msg.value 在通過函數(shù)循環(huán)執(zhí)行時被 “重復使用”,有可能使用戶雙花。
這就是Opyn Hack的根本原因。
私有變量
私有變量在區(qū)塊鏈上仍然是可見的,所以敏感信息不應(yīng)該被存儲在那里。如果它們不能被訪問,驗證者如何能夠處理取決于其值的交易?私有變量不能從外部的 Solidity 合約中讀取,但它們可以使用以太坊客戶端在鏈外讀取。
要讀取一個變量,你需要知道它的存儲槽。在下面的例子中,myPrivateVar 的存儲槽是 0。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PrivateVarExample {
uint256 private myPrivateVar;
constructor(uint256 _initialValue) {
myPrivateVar = _initialValue;
}
}
下面是讀取已部署的智能合約的私有變量的 javascript 代碼
const Web3 = require("web3");
const PRIVATE_VAR_EXAMPLE_ADDRESS = "0x123..."; // Replace with your contract address
async function readPrivateVar() {
const web3 = new Web3("http://localhost:8545"); // Replace with your provider's URL
// Read storage slot 0 (where 'myPrivateVar' is stored)
const storageSlot = 0;
const privateVarValue = await web3.eth.getStorageAt(
PRIVATE_VAR_EXAMPLE_ADDRESS,
storageSlot
);
console.log("Value of private variable 'myPrivateVar':",
web3.utils.hexToNumberString(privateVarValue));
}
readPrivateVar();
不安全的代理調(diào)用
委托調(diào)用(Delegatecall)不應(yīng)該被用于不受信任的合約,因為它把所有的控制權(quán)都交給了委托接受者。在這個例子中,不受信任的合約偷走了合約中所有的以太幣。文章來源:http://www.zghlxwxcb.cn/news/detail-702304.html
contract UntrustedDelegateCall {
constructor() payable {
require(msg.value == 1 ether);
}
function doDelegateCall(address _delegate, bytes calldata data) public {
(bool ok, ) = _delegate.delegatecall(data);
require(ok, "delegatecall failed");
}
}
contract StealEther {
function steal() public {
// you could also selfdestruct here
// if you really wanted to be mean
(bool ok,) =
tx.origin.call{value: address(this).balance}("");
require(ok);
}
function attack(address victim) public {
UntrustedDelegateCall(victim).doDelegateCall(
address(this),
abi.encodeWithSignature("steal()"));
}
}
升級與代理有關(guān)的 bug
我們無法在一個章節(jié)中對這個話題進行公正的解釋。大多數(shù)升級錯誤通??梢酝ㄟ^使用 Openzeppelin 的hardhat 插件和閱讀它所保護的問題來避免出錯。
作為一個快速的總結(jié),以下是與智能合約升級有關(guān)的問題:文章來源地址http://www.zghlxwxcb.cn/news/detail-702304.html
- 自毀(self-destruct)和委托調(diào)用(delegatecall)不應(yīng)該在執(zhí)行合約中使用。
- 必須注意在升級過程中,存儲變量不能相互覆蓋
- 在執(zhí)行合約中應(yīng)避免調(diào)用外部庫,因為不可能預測它們會如何影響存儲訪問。
- 部署者決不能忽視調(diào)用初始化函數(shù)
- 在基類合約中沒有包括間隙(gap)變量,以防止在基類合約中加入新的變量時發(fā)生存儲碰撞(這由 hardhat 插件自動處理)。
- 不可變(immutable)變量中的值在升級時不會被保留
- 非常不鼓勵在構(gòu)造函數(shù)中做任何事情,因為未來的升級必須執(zhí)行相同的構(gòu)造函數(shù)邏輯以保持兼容性。
到了這里,關(guān)于Solidity 合約安全,常見漏洞(第三篇)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!