三明治交易是一种利用闪电贷和 AMM 自动化做市商的策略,通过在交易对中同时进行两笔交易来实现利润。这种策略被广泛应用于去中心化交易所(DEX)和其他区块链金融应用中。然而,由于区块链的去中心化和透明性,这种策略也会成为攻击者的目标。本文将从三明治交易的原理出发,深入探讨一次针对以太坊三明治机器人的攻击事件,分析攻击者的手段和攻击原理。同时,我们也将介绍一些区块链上的基本概念和技术,以帮助读者更好地理解本文内容。
三明治攻击是通过抢跑 (front-running) 与跟跑 (back-running) 组合形成的攻击手法。如果一个用户大量买入一种数字货币,那么该数字货币的价格便会上涨。如果能发现这样的大单交易,并且在大单交易之前买入该数字货币,在大单交易成交后立刻卖出该数字货币,那么这样便能通过该大单交易引发的价格差获取利润。
由于这种攻击手法用两笔交易”夹”住了目标的交易,因此三明治机器人也称为夹子机器人。
交易顺序如何决定
当交易者想要发起一个交易时,交易者会为交易设定一个 gas price。矿工会挑选 gas price 高的交易优先进行打包,因为高 gas price 一定程度上代表了了打包该交易能获得的收益。
而实际上矿工的收益还由最终执行合约使用的 gas 数量决定,因此攻击者会想方设法优化代码以此来降低合约使用的 gas 数量,进而降低攻击所需要的成本。
1.在 mempool 中监听发向 dex(去中心化交易所)的交易
3.根据合约当前的状态计算出该交易可以引发的价格波动,进而计算出利润
4.如果利润大于发起交易的成本(gas),那么就发起两笔交易:第一笔交易是与目标交易对象相同的买入交易,并设定一个高于目标交易的 gas price,使该交易抢先在目标交易达成之前买入(front run);第二笔交易是卖出交易,设定一个略低于目标交易的 gas price,使该交易在目标交易达成后立刻卖出(back run)。
三明治攻击并非是稳定赚钱的,因为正如交易者的交易会在内存池一样,攻击者的交易同样也会在内存池,因此同样也会遭受到其他三明治机器人的攻击。当各方开始争夺这个三明治时,为了让自己的交易最先执行,贿赂给矿工的 gas 费会不断升高,直到这个 gas 费几乎等同于这次攻击能够获取的利润。
如果有一种方式能直接与矿工联系、或者干脆由自己来在区块中排序,而不是将自己的交易广播到内存池,那么对于普通的交易者而言他可以免受三明治攻击,而对于攻击者而言,他也能提高攻击的成功率,并且免受其他机器人的干扰,暗池由此而来。
通过暗池避免了被抢跑攻击: Escaping the Dark Forest
https://samczsun.com/escaping-the-dark-forest/
Defi-Cartel/salmonella: Wrecking sandwich traders for fun and profit
攻击者部署了一种恶意代币,当非合约拥有者 transfer 该代币时合约只发送代币数量的 10%。之后攻击者以一个稍低的 gas price 发起诱饵交易,当 gas price 下降后则取消该交易,直到该交易被机器人放进一个 bundle 中打包执行。由于该合约对非拥有者只发送目标数量的 10%,因此机器人的资金将被攻击者”掠走”。
正常来说交易发起者通过将交易广播到交易池,让交易被矿工得知。矿工根据每笔交易的 gas price 来挑选能为他获得最大利润的交易打包到区块中。由于公开的交易池有被抢跑的风险,因此就有了暗池的出现,交易发起者可以通过将交易发送给特定的矿工或矿池来避免抢跑风险。这种方式被称为暗池交易。暗池交易虽然可以保证交易不被公开,但由于暗池并非是人人都有渠道去使用的,有些人认为这有违于以太坊作为一个去中心化、开放且透明的平台的原则,因此一个名为 FlashBots 的组织推出了一系列项目来创建一个“permissionless, transparent, and fair”的 MEV 生态。简单来说就是让人人都有暗池用。
We believe that without the adoption of neutral, public, open-source infrastructure for permissionless MEV extraction, MEV risks becoming an insiders’ game. We commit as an organization to releasing reference implementations for participation in fair, ethical, and politically neutral MEV extraction. By doing so, we hope to prevent the properties of Ethereum from being eroded by trust-based dark pools or proprietary channels which are key points of security weakness.
FlashBots 引入了”searcher”,”transcation bundles”以及”block template”的概念。搜索者在交易池中搜索交易并将几个交易打包为一个 bundle,这些 bundle 可以在 FlashBots 协议中被矿工按照拍卖的形式购买,最终由矿工组成 block template,以期望从中获得最大化的 MEV。使用 MEV-Geth 客户端能够让矿工更容易地获取 MEV 收益,同时也提高了交易池的效率和安全性。
FlashBots 引入了新的概念:searcher、builder 和 relayer,用于协助矿工或验证人获取 MEV 收益。searcher 在交易池中搜索交易,并将几个交易打包成一个 bundle;builder 则将多个 bundle 组装成一个可以被验证人使用的区块模板,并将其发送到 relayer;relayer 选择最有利润的区块模板并将其分发给相应的验证人进行区块构建。
Overview | Flashbots Docs — 概述 | Flashbots 文档
https://docs.flashbots.net/flashbots-auction/overview
如果攻击者可以获取并修改 bundle 中的交易,那么还是有机会对使用 flashbots 的交易进行攻击的。
Frontrunning As A Service
FlashBots Auction 的出现一方面能够让使用 flashbots 的交易避免被抢跑,但另外一方面同时也给了攻击者很大的便利。由于一个 bundle 是一组已排序好且未完成的以太坊交易,因此攻击者可以通过在内存池中搜索交易,并且将自己的交易与目标交易打包成一个 bundle 递交给 builder。并且小费是以 eth 形式转账给矿工的,这些交易不需要支付 Gas 费,失败的交易也不需要支付成本。只有当交易搜索者的捆绑交易被包含在一个区块中,捆绑交易中的小费才会支付给矿工,这大大降低了攻击者的成本。
Relayer, Validator, and PBS
现在假设我们是一个恶意的验证者,在以太坊潜伏了很长时间,终于等到了一次出块的机会,那么我们应该如何利用这个机会让我们的利益最大化?我们知道,来自 relayer 的区块往往都蕴含着很大的获利机会,因此 searcher 才会将这些交易打包为 bundle 投递给 block builder。那么此时作为 propersor,我们是否有可能抢跑这些 bundle 中的交易,将那些 bundle 中的利润化为己有呢?
在 POS 的机制下,如果我们既可以排序区块内交易的顺序又可以出块,相当于我们有了一次无风险且必定成功的抢跑机会。我们可以找出 bundle 中能获利的交易,将那笔利润变为自己的。于是便有了 PBS 机制(Proposer / Builder Separation),目的在于分离 proposer 与 builder 这两个角色。
实际上 relayer 正是为了防止这样情况的出现而设计的,relayer 并不会将 builder 构建的区块内容直接发送给 validator,而是仅仅发送区块头以告诉 validator 该区块可获取的利润。当确保 validator 会提议该区块出块后 (在 validator 对该区块头进行签名之后) 才会将区块的全部内容发送给 validator,这保证了 validator 无法再通过对区块进行修改来作恶。
·如果 relayer 是一个恶意 relayer,伪造了一个高利润的区块头(实际并不存在)发送给 validator 并被采用,那么该 validator 则会损失一个出块的利润。
·如果通过某种手段能够提前获取到 bundle 的内容,则恶意的 validator 就可以通过这个出块机会的时候获利。
Relayer 如何确保 proposer 会提议该区块
relay 在收到 proposer 签名的区块头后,会将该签名后的区块头发送到 beacon chain 供其他 validator 验证,以此来确保该 proposer 正确出块。
Block proposal | ethereum.org
https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/block-proposal/
以太坊将一个周期称为 epoch,每个 epoch 有 32 个 slot,每个 slot 代表一个出块机会。每个 slot 间隔 12s,每个 epoch 间隔 6.4min。
每个 epoch 将选举出 validator 委员会以验证区块的合法性,每个 slot 将选举一个 proposer 来出块。
The validator selection is fixed two epochs in advance as a way to protect against certain kinds of seed manipulation.
每个验证者是随机选出的,通过一个名为“RANDAO”的伪随机算法实现,该算法结合了每个区块提议者的 hash 与一个随着每个区块更新的种子。
def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex:
"""
Return the beacon proposer index at the current slot.
"""
epoch = get_current_epoch(state)
seed = hash(get_seed(state, epoch, DOMAIN_BEACON_PROPOSER) + int_to_bytes(state.slot, length=8))
indices = get_active_validator_indices(state, epoch)
return compute_proposer_index(state, indices, seed)
def compute_proposer_index(state: BeaconState, indices: Sequence[ValidatorIndex], seed: Bytes32) -> ValidatorIndex:
"""
Return from ``indices`` a random index sampled by effective balance.
"""
assert len(indices) > 0
MAX_RANDOM_BYTE = 2**8 - 1
i = uint64(0)
total = uint64(len(indices))
while True:
candidate_index = indices[compute_shuffled_index(i % total, total, seed)]
random_byte = hash(seed + uint_to_bytes(uint64(i
effective_balance = state.validators[candidate_index].effective_balance
if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte:
return candidate_index
i += 1
可以看到 proposer 的选择主要来源于这个 seed,那么这个 seed 是从哪里来呢?
proof of stake – How are block proposers selected in Ethereum 2.0? – Ethereum Stack Exchange
prysm-spike/randao.go at d5ac70f0406b445a276ee61ba10fdf0eb6aafa0f · silesiacoin/prysm-spike
def get_seed(state: BeaconState, epoch: Epoch, domain_type: DomainType) -> Hash:
"""
Return the seed at ``epoch``.
"""
mix = get_randao_mix(state, Epoch(epoch + EPOCHS_PER_HISTORICAL_VECTOR - MIN_SEED_LOOKAHEAD - 1)) # Avoid underflow
return hash(domain_type + int_to_bytes(epoch, length=8) + mix)
def get_randao_mix(state: BeaconState, epoch: Epoch) -> Hash:
"""
Return the randao mix at a recent ``epoch``.
"""
return state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR]
MINSEEDLOOKAHEAD 被设置为 1 Upgrading Ethereum | 3.2.3 Preset
https://eth2book.info/altair/part3/config/preset/#min_seed_lookahead
可以看出 seed 由当前 epoch-2 的 randao_mixes 决定。
Current and past RANDAO values are stored in the beacon state(https://eth2book.info/bellatrix/part3/containers/state/#beaconstate) in the randao_mixes field. The current value is updated by process_randao(https://eth2book.info/bellatrix/part3/transition/block/#def_process_randao)with every block that the beacon chain processes.
那么这个 randao_mixes 又是如何计算的?
每个区块的所有交易计算结束后会调用 process_block,进而调用 process_randao
def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_block_header(state, block)
process_randao(state, block.body)
process_eth1_data(state, block.body)
process_operations(state, block.body)
def process_randao(state: BeaconState, body: BeaconBlockBody) -> None:
epoch = get_current_epoch(state)
# Verify RANDAO reveal
proposer = state.validators[get_beacon_proposer_index(state)]
signing_root = compute_signing_root(epoch, get_domain(state, DOMAIN_RANDAO))
assert bls.Verify(proposer.pubkey, signing_root, body.randao_reveal)
# Mix in RANDAO reveal
mix = xor(get_randao_mix(state, epoch), hash(body.randao_reveal))
state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mi
根据这段代码可以看出,randaomix 来自之前的值与 randaoreveal。
randao_reveal 是出块者对该区块的签名
class BeaconBlockBody(Container):
randao_reveal: BLSSignature
eth1_data: Eth1Data # Eth1 data vote
graffiti: Bytes32 # Arbitrary data
# Operations
proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS]
attestations: List[Attestation, MAX_ATTESTATIONS]
deposits: List[Deposit, MAX_DEPOSITS]
voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]
·当前每个 slot 的 proposer 的选择是根据一个 seed 通过伪随机算法选取的
·seed 可以根据当前 epoch-2 的 randao_mixes 计算出来
·每个区块在出块时通过将区块的签名写入 randaoreveal 为 randaomixes 增加随机性
不难看出 proposer 在 epoch-2 到 epoch 这个时段内都是可以预测的。
4.3 发生了一起针对 sandwich bot 的攻击,攻击者通过利用漏洞的方式拿到了三明治的 bundle 并重新打包,套取了三明治机器人的钱。
https://twitter.com/samczsun/status/1642848556590723075?s=09&scene=ccm&logParams={“location”:”ccm_default”}&lang=zh-CN
tx hash:0xd2edf726fd3a7f179c1a93343e5c0c6ed13417837deb6fc61601d1ce9380e8d
cmevbot 合约:0xE8c060F8052E07423f71D445277c61AC5138A2e5操纵 mevbot 的钱包:0xe2cA3167B89b8Cf680D63B06E8AeEfc5E4EBe907
mevbot 在 uniswap 用 2, 454.100770074916814848 weth 买了 4.507832705794128225 STG
0x4b2a2d03b3dc136ef94ebe2f3bc36231b104172bcb598104730898f7d81a55db
0x84cb986d0427e48a2a72be89d78f438b3a3c58d1
0xe73f1576af5573714404a2e3181f7336d3d978f9
攻击者的合约在 uniswap 用 158.143384582646949233 STG 买了 2, 454.105651828533863963 WETH,将 mevbot 的钱全部划走
攻击者部署的 uniswap 合约,2023.3.16 部署部署钱包:0x84cb986d0427e48a2a72be89d78f438b3a3c58d1
Uniswap V2: STG 5 | Address 0x410fb10ba8af78a1e191fe3067208d3212ded961 | Etherscan
Validator #552061 | Mainnet Beacon Chain (Phase 0) Ethereum 2.0 Explorer
攻击发生在 4.2,目前以太坊上有大概 50w 的 validator。因此如果以后有类似的共识层上的攻击,那么这个攻击的周期可能是半个月左右。
Chris Hager ⚡? on Twitter: “The issue was a relay publishing the payload to the proposer even though parts of the beacon block were incorrect, preventing the relay from publishing the original block first. This allowed the proposer to access the block contents before another block had been finalized.” / Twitter
https://github.com/flashbots/mev-boost-relay/pull/330/commits
原始流程:验证 validator 的签名->返回区块体内容->将签名的区块头发布到 beacon chain
patch 后的流程:验证 validator 签名->将签名的区块头发送到 beacon chain->如果成功则返回区块体内容
可以通过 patch 得出其关键就在于将区块头发布到 beacon chain 这个动作的成功才能返回区块体的内容,反之则会被攻击。
但是首先我们要知道校验 validator 头校验的是哪些部分
校验签名就不说了,校验签名之后还会校验该 header 是不是 relay 发送给 proposer 的那个,使用函数 EqExecutionPayloadToHeader 校验
该函数会将 header 转为名为 ExecutionPayload 的结构体并计算 hash,以此判断 proposer 是不是真的签名了该头。所以校验的内容就是如下字段:
type ExecutionPayload struct {
ParentHash Hash `json:"parent_hash" ssz-size:"32"`
FeeRecipient Address `json:"fee_recipient" ssz-size:"20"`
StateRoot Root `json:"state_root" ssz-size:"32"`
ReceiptsRoot Root `json:"receipts_root" ssz-size:"32"`
LogsBloom Bloom `json:"logs_bloom" ssz-size:"256"`
Random Hash `json:"prev_randao" ssz-size:"32"`
BlockNumber uint64 `json:"block_number,string"`
GasLimit uint64 `json:"gas_limit,string"`
GasUsed uint64 `json:"gas_used,string"`
Timestamp uint64 `json:"timestamp,string"`
ExtraData ExtraData `json:"extra_data" ssz-max:"32"`
BaseFeePerGas U256Str `json:"base_fee_per_gas" ssz-max:"32"`
BlockHash Hash `json:"block_hash" ssz-size:"32"`
Transactions []hexutil.Bytes `json:"transactions" ssz-max:"1048576,1073741824" ssz-size:"?,?"`
}
要想获取到 relay 的 block body 中的内容,攻击者首先要通过验证,也就是攻击者签名了 relay 发送的 header。但是由于 relay 会将 block header 发布到 beacon chain 让其他 validator 去验证该 proposer 提议的区块是否与 relay 发送给 proposer 的区块一致,因此 proposer 仅拿到这个区块是没用的,proposer 还需要阻止他签名的正常 header 被发布才能防止其他 validator 验证他发布的区块。
patch 中将区块头发布到 beacon chain 这个行为提前,并且如果这个动作失败则停止继续发送区块体的内容,这说明攻击者正是利用了发布这个动作的失败使该区块头没有被发布到 beacon chain 上,使其他 validator 无法验证攻击者的区块。
https://twitter.com/samczsun/status/1642848567781105664&scene=ccm&logParams={“location”:”ccm_default”}&lang=zh-CN
Unfortunately, if the signed block was invalid, then it would never be accepted by the network, so there would be no race at all. By setting both the parent root and the state root to zero, that’s exactly what the malicious validator did.
如果区块是非法区块,那么该区块将不会被接收。如图所示,攻击者通过将 parentroot 与 stateroot 字段置 0 使该区块非法。此时我们再回头看 relay 中校验的代码,会发现 ExecutionPayload 中并没有 parentroot 与 stateroot 相关的字段(有同名字段 state_root,但是并不是同一个)。攻击者通过修改这两个字段制造了既能通过 relay 校验但又无法被发布到 beacon chain 上 header。而由于 relay 并不关心发布到 beacon chain 上的结果,因此攻击者即拿到了 block 的全部内容,又躲过了其他 validator 的校验。
接下来就很简单了,攻击者拆解了整个 block,将自己的诱饵交易(大额买入)改为卖出,轻松换走了夹子机器人的钱。
https://security.feishu.cn/link/safety?target=https://beaconscan.com/validator/552061&scene=ccm&logParams={“location”:”ccm_default”}&lang=zh-CN
有一下三种行为的 proposer 会被 slash:
·By proposing and signing two different blocks for the same slot
·By attesting to a block that “surrounds” another one (effectively changing history)
·By “double voting” by attesting to two candidates for the same block
当有 validator 发现了 proposer 在同一个 slot 签署了两个不同区块头后,他会将这个 proposer 签署的两个区块头作为证据,然后作为一个消息广播到 beacon chain 上。然后将这个消息出块的 proposer 他们将获得 slash 的奖励。
本次攻击的 proposer 被 slash 的证据:
Block Slot 6142320 | Mainnet Beacon Chain (Phase 0) Ethereum 2.0 Explorer
https://beaconscan.com/slot/6142320#proposerslashing
Upgrading Ethereum | 2.8.7 Slashing — 升级以太坊 | 2.8.7 罚没
https://eth2book.info/altair/part2/incentives/slashing/#slashing
攻击者被 slash 后会立即损失 1/32 的以太坊,然后排队退出,并被设置一个 36 天的余额提取周期(剩下 31 个 eth 可以在 36 后提取,该期间无法提取,并且之后会有第二次惩罚)。
在其可提取期的一半(被削减后 18 天),被削减的验证者将受到第二次惩罚。
Correlation penalty=min(B,2SB/T)
crements in the list of slashed validators over the last 36 days, B my effective balance, and T the total increments, the calculation looks as follows.
slashing multiplier 目前似乎已经变为了 3
不管怎样,代价最多为攻击者所质押的 32 个 eth,而攻击者通过此次攻击所获得的收益早已远远大于这个数字。
原文始发于微信公众号(零鉴科技):Don't Eat My Sandwich