NEAR智能合约审计:分片与跨合约调用

本文深入探讨了NEAR协议的Nightshade分片技术及其跨合约调用的实现机制,重点分析了异步跨合约调用带来的潜在安全风险。文章通过具体案例展示了如何利用时间窗口进行攻击,并提出了通过状态锁定、回调验证和失败回滚等关键安全措施来防范这些风险,从而保障智能合约的安全性。

NEAR:分片 & 跨合约调用

这是分为三个部分系列文章的第一部分。随便看看其他的:

介绍

NEAR 协议实现了一种创新的分片解决方案,称为“Nightshade”,旨在解决高可扩展性问题,同时保持安全性和可用性。本文探讨了 NEAR 分片实现的技术细节,以及跨合约调用如何在具有安全含义的这种架构中工作。

Nightshade 分片的魔力

像区块链这样的分布式系统中的分片是一种通过将网络(这里是区块链)划分为多个“分片”来提高网络速度和容量的方法。这种划分将允许每个分片独立于其他分片处理交易——理论上增加了交易吞吐量。

Nightshade 分片

NEAR 实现了他们称为“Nightshade”的分片方法。每个分片并行处理网络交易的子集。NEAR 分片在每个区块中创建分片(“块”),而不是分片链。因此,它只维护一个链,并允许异步(“跨分片”)交易。

以下是 Nightshade 与 Beacon 链相比的简化图示: 信标链与 Nightshade 对比图 1. Nightshade 分片图示。来源:LiNEAR 协议博客

动态重新分片

NEAR 的分片允许一种称为“动态重新分片”的功能。 它允许 NEAR 根据网络需求自动调整分片数量。 NEAR 验证者也只需要跟踪其指定分片的状态。

跨合约调用:分片之间的桥梁

由于 NEAR 分片技术的性质,其跨合约调用是异步且独立的。 这对应该如何处理回调有重要的影响。

组成部分

NEAR 中的跨合约调用通过 promise 系统进行操作。 每个跨合约交互都涉及两个关键组件:发起调用和处理结果。 这是在底层发生的事情:

当合约 A 需要与合约 B 交互时,它使用 NEAR 的 SDK 创建一个 promise。 这个 promise 代表了对合约 B 调用的未来结果。 该过程是异步的,这意味着合约 A 不会闲置地等待合约 B 响应 - 它在等待回调时继续进行其他操作。

以下是如何在代码中实现此目的的示例:

##[near_bindgen]
impl MyContract {
    pub fn initiate_cross_contract_call(&mut self) -> Promise {
        Promise::new("contract-b.near".parse().unwrap())
            .function_call(
                "process_request".to_string(),
                json!({
                    "data": "example"
                }).to_string().into_bytes(),
                0, // attached deposit
                Gas(5_000_000_000_000) // attached gas
            )
            .then(
                Promise::new(env::current_account_id())
                    .function_call(
                        "callback_function".to_string(),
                        Vec::new(),
                        0,
                        Gas(2_000_000_000_000)
                    )
            )
    }

    #[private]
    pub fn callback_function(&mut self, #[callback_result] call_result: Result<String, PromiseError>) {
        match call_result {
            Ok(result) => {
                // Handle successful result
            }
            Err(_) => {
                // Handle error case
            }
        }
    }
}

安全注意事项

这些跨合约调用的异步特性引入了潜在的漏洞,必须仔细考虑。

竞争条件是重要的安全挑战之一。 在跨合约调用及其回调执行之间的这段时间内(通常为 1-2 个区块),你的合约保持活动状态且可调用。 这意味着恶意用户可能会利用此窗口来操纵合约状态。

让我们检查两个具体的场景,说明正确的实现和潜在的漏洞。

场景 1:正常运行流程

正常运行流程图 在此图中,我们看到一个合法的用户“Alice”与智能合约交互:

  1. Alice 通过存入 100N 来启动
  2. 合约使用 100N 调用 stake() 以接收 200 个代币
  3. 外部合约处理交换
  4. promise 结果从外部合约返回
  5. 2 个区块后,回调执行以减少内部 NEAR 余额

最终状态显示 Alice 拥有 0N 余额和收到的 200 个代币。 这是预期的安全行为,其中合约在整个异步操作中保持适当的状态管理。

场景 2:潜在的漏洞利用

但是,如果没有适当的保护,恶意用户可能会利用跨合约调用的异步特性: 漏洞利用运行流程图

在此图中,我们看到攻击者如何可能利用该系统:

  1. 攻击者存入 100N
  2. 在第一次回调完成之前,他们使用 100N 多次调用 stake()(在本例中为 2 次)
  3. 外部合约处理多个交换请求
  4. Promise 结果从外部合约返回
  5. 回调在 2 个区块后执行,导致两种可能的结果:
    • 成功案例:NEAR 余额减少到 0,但攻击者收到 400 个代币
    • 错误案例:由于第二次回调后尝试减少余额时发生下溢,因此会发生 panic

此漏洞的存在是因为原始实现未阻止在处理期间进行多次 stake() 调用。

缓解漏洞

为防止此类漏洞,请实施以下基本的安全措施:

  • 回调函数需要是公共的,但只能由合约调用
    • 在函数上方添加 #[private] 装饰器。 这确保只有合约可以调用回调函数,而外部用户不能。
  • 确保合约在调用和回调之间没有处于可利用的状态
  • 如果外部调用失败,则在回调中手动回滚任何状态更改
    • 在回调函数中分配了足够的 gas 以将资金转回

以下是如何在代码中实现此目的的示例:

##[near_bindgen]
impl StakingContract {
    pub fn stake(&mut self) -> Promise {
        // Add state lock to prevent multiple calls
        assert!(!self.processing, "Already processing a stake operation");
        self.processing = true;

        // Store initial state for potential rollback
        self.last_stake_amount = self.stake_balance;

        Promise::new(self.staking_target.clone())
            .function_call(
                "stake_tokens".to_string(),
                // ... stake parameters ...
                Gas(5_000_000_000_000)
            )
            .then(Self::ext(env::current_account_id())
                .with_static_gas(Gas(2_000_000_000_000))
                .stake_callback())
    }

    #[private]
    pub fn stake_callback(&mut self, #[callback_result] call_result: Result<(), PromiseError>) {
        // Always reset processing flag in callback
        let processing = std::mem::replace(&mut self.processing, false);
        assert!(processing, "Callback called without active processing");

        match call_result {
            Ok(_) => {
                // Verify state changes are valid
                assert!(
                    self.stake_balance >= self.last_stake_amount,
                    "Invalid state change detected"
                );
            }
            Err(_) => {
                // Rollback any state changes
                self.stake_balance = self.last_stake_amount;
                env::log_str("Stake operation failed, state rolled back");
            }
        }
    }
}

结论

这篇文章介绍了 NEAR 区块链中分片所包含的本质,并触及了跨合约调用及其安全含义的一些复杂性。

请记住,这种类型的漏洞并非我们的质押操作示例所独有。 如果没有得到适当的保护,任何修改合约状态的跨合约调用都可能容易受到类似的竞争条件的影响。 最好假设恶意用户会尝试利用调用和回调执行之间的时间窗口。

测试和理解这些安全措施对于开发人员和安全审核员来说非常重要。 它可以提高协议的安全性,保护用户的资产,并保持整个生态系统的安全和可信。 我们鼓励所有开发人员和安全审核员随时了解情况,并积极主动地查找和缓解这些类型的漏洞。

在 Sigma Prime,我们致力于保护和加强各种区块链网络和协议。 如果你正在构建解决方案并希望利用我们在该领域的最先进的安全专业知识,请联系我们

  • 原文链接: blog.sigmaprime.io/near-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
sigmaprime
sigmaprime
江湖只有他的大名,没有他的介绍。