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。
- 为什么: 它使用链表结构(每个条目4个存储写入)。比
2. 安全模式
算术安全
- 总是使用
BigUint用于代币、价格和金融数学。- 为什么: 防止溢出/下溢,并匹配VM的原生大整数实现。
- 避免
u64/u32用于金钱。只用于循环计数器或小ID。
重入保护
- 检查-效果-交互:
- 检查: 验证输入(
require!)。 - 效果: 更新存储(扣除余额,更新状态)。
- 交互: 发送代币或调用其他合约。
- 检查: 验证输入(
- 异步调用: MultiversX 异步调用在重入相同执行上下文方面比同步调用更安全,但状态变化发生在单独的交易(回调)中。
- 回调验证: 总是在
#[callback]函数中验证状态。不要假设异步调用成功仅仅因为已发送。
访问控制
- 使用
#[only_owner]用于管理函数。 - 对于细粒度控制,使用
multiversx-sc-modulescrate 中的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而不是标准RustVec,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>;
只适用于同分片。只读。键必须完全匹配目标合约。