MultiversX智能合约最佳实践指南Skill mvx_sc_best_practices

本技能提供编写安全、高效、符合最佳实践的MultiversX智能合约的全面指南,涵盖存储优化、安全模式、测试方法、代码结构等关键方面,适用于区块链开发者、智能合约工程师和审计人员。关键词:MultiversX, 智能合约, Rust, 最佳实践, 安全, 气效率, 存储, 测试, 区块链开发。

智能合约 0 次安装 0 次浏览 更新于 3/21/2026

name: mvx_sc_best_practices description: 开发、审计和优化MultiversX智能合约(Rust)的专家指南。

MultiversX 智能合约最佳实践

本技能提供使用 multiversx-sc 框架在 MultiversX 上编写安全、气效率高和符合习惯的智能合约的专家级指导。

1. 存储优化(关键)

存储是最昂贵的资源。

  • SingleValueMapper: 用于单个项目(标志、配置、ID)。
    • : 最便宜(约1个槽写入)。
    • 模式: #[storage_mapper("myValue")] fn my_value(&self) -> SingleValueMapper<MyType>;
  • VecMapper: 用于需要索引访问的有序列表。
    • 警告: 绝不迭代 一个可能无限增长的 VecMapper 在链上。这是一个DoS向量(气循环)。
    • : 中等。
  • UnorderedSetMapper: 用于唯一集合或白名单。
    • : 在插入前检查存在性。适合 O(1) 成员检查。
  • MapMapper: 避免 除非严格必要。
    • 为什么: 它使用链表结构(每个条目4个存储写入)。比 SingleValueMapper 贵约4倍。
    • 替代方案: 如果不需要迭代键,使用一个键由哈希或复合键的 SingleValueMapper

2. 安全模式

算术安全

  • 总是使用 BigUint 用于代币、价格和金融数学。
    • 为什么: 防止溢出/下溢,并匹配VM的原生大整数实现。
  • 避免 u64/u32 用于金钱。只用于循环计数器或小ID。

重入保护

  • 检查-效果-交互:
    1. 检查: 验证输入(require!)。
    2. 效果: 更新存储(扣除余额,更新状态)。
    3. 交互: 发送代币或调用其他合约。
  • 异步调用: MultiversX 异步调用在重入相同执行上下文方面比同步调用更安全,但状态变化发生在单独的交易(回调)中。
  • 回调验证: 总是在 #[callback] 函数中验证状态。不要假设异步调用成功仅仅因为已发送。

访问控制

  • 使用 #[only_owner] 用于管理函数。
  • 对于细粒度控制,使用 multiversx-sc-modules crate 中的 only_admin 模块。它提供了管理多个管理员的标准实现。

3. 数据流与测试

传输-执行模式

  • 当发送代币到合约时,优先使用 MultiESDTNFTTransfer(内置函数)而不是2个交易(批准 + 传输自)。
  • 在合约中,使用 #[payable] 接受代币和 self.call_value().all() 检查它们。

测试(Mandos/场景)

  • Mandos(.scen.json 是集成测试的强制要求。
  • 覆盖所有路径:
    • 快乐路径。
    • 错误路径(期望状态 4)。
  • 白盒测试: 使用 #[cfg(test)] 模块与 multiversx_sc_scenario::imports::* 来测试内部函数而无需部署。

4. 代码结构

  • 端点: 公共函数 #[endpoint]
  • 视图: 只读 #[view]
  • 私有: 辅助函数(无注释,或纯Rust)。
  • 事件: #[event] 用于索引,但不要将关键数据仅存储在事件中。

5. 常见陷阱 / “尖锐边缘”

  • 代币标识符验证: 总是验证 token_id。不要假设用户发送了正确的代币。
  • 气限制: 注意块气限制(1.5B 气)。大循环将回滚。
  • 托管类型: 使用 ManagedBuffer, ManagedAddress, ManagedVec 而不是标准Rust Vec, String 以避免序列化开销。

6. 生产模式(高级)

基于缓存的气优化

使用一个 Drop-特质缓存结构来批量存储读/写:

pub struct StorageCache<'a, C: crate::storage::StorageModule> {
    sc_ref: &'a C,
    pub field_a: BigUint<C::Api>,
    pub field_b: BigUint<C::Api>,
}

impl<'a, C: crate::storage::StorageModule> StorageCache<'a, C> {
    pub fn new(sc_ref: &'a C) -> Self {
        StorageCache {
            field_a: sc_ref.field_a().get(),
            field_b: sc_ref.field_b().get(),
            sc_ref,
        }
    }
}

impl<C: crate::storage::StorageModule> Drop for StorageCache<'_, C> {
    fn drop(&mut self) {
        self.sc_ref.field_a().set(&self.field_a);
        self.sc_ref.field_b().set(&self.field_b);
    }
}

错误常量组织

// errors.rs — 用于气效率的静态字节字符串
pub static ERROR_NOT_ACTIVE: &[u8] = b"Not active";
pub static ERROR_UNAUTHORIZED: &[u8] = b"Unauthorized";
pub static ERROR_ZERO_AMOUNT: &[u8] = b"Zero amount";

事件特质组合

#[multiversx_sc::module]
pub trait EventsModule {
    #[event("deposit")]
    fn deposit_event(&self, #[indexed] caller: &ManagedAddress, amount: &BigUint);
}

视图端点分离

将所有 #[view] 端点放在一个专门的 views.rs 模块中以清晰。

验证模块模式

将所有 require! 检查集中在一个 validation.rs 模块中,以便安全规则在一个地方可审计。

跨合约存储读取

使用 #[storage_mapper_from_address("key")] 读取其他合约的存储而无需异步调用开销:

#[storage_mapper_from_address("reserve")]
fn external_reserve(&self, addr: ManagedAddress, token: &TokenIdentifier)
    -> SingleValueMapper<BigUint, ManagedAddress>;

只适用于同分片。只读。键必须完全匹配目标合约。