MultiversX智能合约开发Skill multiversx-smart-contracts

MultiversX智能合约开发技能涉及使用Rust语言、sc-meta和mxpy工具构建、测试和部署区块链智能合约,适用于代币创建、NFT铸造、质押、众筹等链上功能开发。关键词:智能合约、MultiversX、Rust、区块链开发、代币、NFT、DeFi、区块链编程、智能合约部署。

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

name: multiversx-smart-contracts description: 使用Rust构建MultiversX智能合约。当应用需要区块链逻辑、代币创建、NFT铸造、质押、众筹或任何需要自定义智能合约的链上功能时使用。

MultiversX智能合约开发

使用Rust、sc-meta和mxpy构建、测试和部署MultiversX智能合约。

前提条件

VM上可用的工具:

  • Rust(版本1.85.0+)
  • sc-meta - 智能合约元工具
  • mxpy - 用于部署的MultiversX Python CLI

如果未安装sc-meta:

cargo install multiversx-sc-meta --locked

创建新合约

使用sc-meta从模板创建新合约:

# 列出可用模板
sc-meta templates

# 从模板创建
sc-meta new --template adder --name my-contract

# 可用模板:
# - empty: 最小合约结构
# - adder: 基本算术操作
# - crypto-zombies: NFT游戏示例

项目结构

创建合约后,获得以下结构:

my-contract/
├── Cargo.toml           # 依赖项
├── src/
│   └── lib.rs           # 合约代码
├── meta/
│   ├── Cargo.toml       # 元包依赖项
│   └── src/
│       └── main.rs      # 构建工具入口
├── wasm/
│   ├── Cargo.toml       # WASM输出配置
│   └── src/
│       └── lib.rs       # WASM入口点
└── scenarios/           # 测试文件(可选)

Cargo.toml配置

[package]
name = "my-contract"
version = "0.0.0"
edition = "2024"

[lib]
path = "src/lib.rs"

[dependencies.multiversx-sc]
version = "0.64.0"

[dev-dependencies.multiversx-sc-scenario]
version = "0.64.0"

基本合约结构

#![no_std]

multiversx_sc::imports!();

#[multiversx_sc::contract]
pub trait MyContract {
    #[init]
    fn init(&self, initial_value: BigUint) {
        self.stored_value().set(initial_value);
    }

    #[upgrade]
    fn upgrade(&self) {
        // 合约升级时调用
    }

    #[endpoint]
    fn add(&self, value: BigUint) {
        self.stored_value().update(|v| *v += value);
    }

    #[view(getValue)]
    fn get_value(&self) -> BigUint {
        self.stored_value().get()
    }

    #[storage_mapper("storedValue")]
    fn stored_value(&self) -> SingleValueMapper<BigUint>;
}

核心注解

合约和模块级别

注解 目的
#[multiversx_sc::contract] 标记特质为主合约(每个包一个)
#[multiversx_sc::module] 标记特质为可重用模块
#[multiversx_sc::proxy] 创建用于调用其他合约的代理

方法级别

注解 目的
#[init] 构造函数,部署时调用
#[upgrade] 合约升级时调用
#[endpoint] 公共可调用方法
#[view] 只读公共方法
#[endpoint(customName)] 自定义ABI名称的端点
#[view(customName)] 自定义ABI名称的视图

支付注解

注解 目的
#[payable] 接受任何代币支付(自SDK v0.64.0起是#[payable("*")]的简写)
#[payable("EGLD")] 仅接受EGLD
#[payable("TOKEN-ID")] 接受特定代币

事件注解

注解 目的
#[event("eventName")] 定义合约事件
#[indexed] 标记事件字段为可搜索主题

回调注解

注解 目的
#[callback] 用于传统异步调用的回调
#[promises_callback] 用于基于承诺的异步调用的回调(与register_promise()一起使用)

存储映射器

SingleValueMapper

存储单个值。

#[storage_mapper("owner")]
fn owner(&self) -> SingleValueMapper<ManagedAddress>;

// 使用
self.owner().set(caller);
let owner = self.owner().get();
self.owner().is_empty();
self.owner().clear();
self.owner().update(|v| *v = new_value);

VecMapper

存储索引数组(1起始索引)。

#[storage_mapper("items")]
fn items(&self) -> VecMapper<BigUint>;

// 使用
self.items().push(&value);
let item = self.items().get(1); // 1起始索引!
self.items().set(1, &new_value);
let len = self.items().len();
for item in self.items().iter() { }
self.items().swap_remove(1);

SetMapper

唯一集合,O(1)查找,保持插入顺序。

#[storage_mapper("whitelist")]
fn whitelist(&self) -> SetMapper<ManagedAddress>;

// 使用
self.whitelist().insert(address); // 如果重复返回false
self.whitelist().contains(&address); // O(1)
self.whitelist().remove(&address);
for addr in self.whitelist().iter() { }

UnorderedSetMapper

类似SetMapper,但当顺序不重要时更高效。

#[storage_mapper("participants")]
fn participants(&self) -> UnorderedSetMapper<ManagedAddress>;

MapMapper

键值对。昂贵 - 不需要迭代时避免使用。

#[storage_mapper("balances")]
fn balances(&self) -> MapMapper<ManagedAddress, BigUint>;

// 使用
self.balances().insert(address, amount);
let balance = self.balances().get(&address); // 返回Option
self.balances().contains_key(&address);
self.balances().remove(&address);
for (addr, bal) in self.balances().iter() { }

LinkedListMapper

双链表,用于队列操作。

#[storage_mapper("queue")]
fn queue(&self) -> LinkedListMapper<BigUint>;

// 使用
self.queue().push_back(value);
self.queue().push_front(value);
self.queue().pop_front();
self.queue().pop_back();

FungibleTokenMapper

管理可替代代币,内置ESDT操作。

#[storage_mapper("token")]
fn token(&self) -> FungibleTokenMapper;

// 使用
self.token().issue_and_set_all_roles(...);
self.token().mint(amount);
self.token().burn(amount);
self.token().get_balance();

NonFungibleTokenMapper

管理NFT/SFT/META-ESDT代币。

#[storage_mapper("nft")]
fn nft(&self) -> NonFungibleTokenMapper;

// 使用
self.nft().nft_create(amount, &attributes);
self.nft().nft_add_quantity(nonce, amount);
self.nft().get_all_token_data(nonce);

WhitelistMapper

非迭代O(1)成员检查。当不需要迭代时比SetMapper更轻量。

#[storage_mapper("allowedTokens")]
fn allowed_tokens(&self) -> WhitelistMapper<TokenId>;

// 使用
self.allowed_tokens().add(&token_id);
self.allowed_tokens().contains(&token_id); // O(1)
self.allowed_tokens().require_whitelisted(&token_id); // 如果缺失则恐慌
self.allowed_tokens().remove(&token_id);

BiDiMapper

双向映射 - 键和值之间的双向查找(两者必须唯一)。

#[storage_mapper("idToAddress")]
fn id_to_address(&self) -> BiDiMapper<u64, ManagedAddress>;

// 使用
self.id_to_address().insert(1u64, address.clone());
let addr = self.id_to_address().get_value(&1u64);
let id = self.id_to_address().get_id(&address);
self.id_to_address().contains_id(&1u64);
self.id_to_address().remove_by_id(&1u64);

UniqueIdMapper

管理唯一ID池,用于随机或顺序分配。

#[storage_mapper("nftIds")]
fn nft_ids(&self) -> UniqueIdMapper<Self::Api>;

// 使用
self.nft_ids().set_initial_len(1000); // ID 1..=1000
let id = self.nft_ids().swap_remove(index); // 弹出随机ID
self.nft_ids().len(); // 剩余ID

OrderedBinaryTreeMapper

自平衡二叉搜索树,用于有序链上数据。

#[storage_mapper("orderBook")]
fn order_book(&self) -> OrderedBinaryTreeMapper<Self::Api, u64>;

QueueMapper

FIFO队列,支持后推和前弹。

#[storage_mapper("pending")]
fn pending(&self) -> QueueMapper<ManagedAddress>;

// 使用
self.pending().push_back(address);
let next = self.pending().pop_front(); // Option
self.pending().len();

AddressToIdMapper

地址和自增u64 ID之间的双向映射。对于通过数字ID跟踪许多用户的合约,Gas高效。

#[storage_mapper("users")]
fn users(&self) -> AddressToIdMapper;

// 使用
let id: u64 = self.users().get_or_create_id(&caller); // 自动分配下一个ID
let addr: ManagedAddress = self.users().get_address(id);
self.users().contains(&caller); // O(1)检查

UserMapper

类似AddressToIdMapper,但专门设计用于用户管理,带有计数跟踪。

#[storage_mapper("user")]
fn user_mapper(&self) -> UserMapper;

// 使用
let user_id = self.user_mapper().get_or_create_user(&address);
let user_count = self.user_mapper().get_user_count();
let address = self.user_mapper().get_user_address(user_id);

MapStorageMapper

映射,其中值本身是存储映射器(嵌套存储)。当每个键需要自己的复杂存储结构时使用。

#[storage_mapper("vaults")]
fn vaults(&self) -> MapStorageMapper<ManagedAddress, SingleValueMapper<BigUint>>;

TimelockMapper

带时间锁的值 - 存储当前和未来值以及解锁时间戳。当区块时间超过解锁点时自动转换值。

#[storage_mapper("admin")]
fn admin(&self) -> TimelockMapper<ManagedAddress>;

// 使用 - 带延迟调度更改
self.admin().update_and_lock(new_admin, unlock_timestamp);
let current = self.admin().get(); // 解锁前返回当前值,解锁后返回未来值

数据类型

核心类型

类型 描述
BigUint 无符号任意精度整数
BigInt 有符号任意精度整数
ManagedBuffer 字节数组(字符串、原始数据)
ManagedAddress 32字节地址
EsdtTokenIdentifier ESDT代币ID(例如,“TOKEN-abc123”)
TokenId EGLD和ESDT的统一代币标识符(EGLD表示为EGLD-000000
Payment 统一支付:TokenId + nonce + NonZeroBigUint金额
NonZeroBigUint BigUint,在构造时保证非零
EgldOrEsdtTokenIdentifier 遗留:EGLD或ESDT代币ID(首选TokenId
EsdtTokenPayment 遗留:代币ID + nonce + 金额(首选Payment

创建值

// BigUint
let amount = BigUint::from(1000u64);
let zero = BigUint::zero();

// ManagedBuffer(字符串)
let buffer = ManagedBuffer::from("hello");

// 地址
let caller = self.blockchain().get_caller();

// 代币标识符
let token = EsdtTokenIdentifier::from("TOKEN-abc123");

// 统一代币标识符(EGLD或ESDT)
let token_id = TokenId::from("EGLD-000000"); // EGLD
let token_id = TokenId::from("TOKEN-abc123"); // ESDT

// NonZeroBigUint
let nz_amount = NonZeroBigUint::new_or_panic(BigUint::from(1000u64));

支付类型:Payment vs EgldOrEsdtTokenPayment

方面 Payment<M>(v0.64+) EgldOrEsdtTokenPayment<M>(遗留)
代币ID TokenId(EGLD = EGLD-000000 EgldOrEsdtTokenIdentifier
金额 NonZeroBigUint(拒绝零) BigUint(允许零)
状态 新代码首选 生产环境中广泛使用
// 新(v0.64+):带TokenId + NonZeroBigUint的Payment
let payment: Payment<Self::Api> = self.call_value().single();
// payment.token_identifier是TokenId,payment.amount是NonZeroBigUint

// 遗留:带BigUint的EgldOrEsdtTokenPayment
let legacy = self.call_value().egld_or_single_esdt();
// legacy.token_identifier是EgldOrEsdtTokenIdentifier,legacy.amount是BigUint

支付处理

错误

// 不要:使用BigUint处理支付金额 — 允许零值转账
let amount: BigUint = self.call_value().egld_value().clone_value();
let payment = EsdtTokenPayment::new(token, 0, amount); // 遗留类型,无零检查

正确

// 做:使用NonZeroBigUint和Payment — 零在类型级别被拒绝
let payment = self.call_value().single(); // 返回带NonZeroBigUint的Payment
// payment.amount是NonZeroBigUint — 保证非零

错误

// 不要:使用遗留的EgldOrEsdtTokenIdentifier
let token: EgldOrEsdtTokenIdentifier = self.call_value().egld_or_single_esdt().token_identifier;

正确

// 做:使用统一的TokenId — 统一处理EGLD和ESDT
let token: TokenId = self.call_value().single().token_identifier;
// EGLD表示为"EGLD-000000"

错误

// 不要:为跨合约调用使用遗留send() API
self.send().direct_esdt(&recipient, &token, 0, &amount);
self.send_raw().direct_egld_execute(&to, &amount, 0, &ManagedBuffer::new());

正确

// 做:为所有转账和跨合约调用使用Tx构建器API
self.tx().to(&recipient).payment(payment).transfer();
self.tx().to(&addr).typed(proxy::Proxy).some_endpoint(arg).sync_call();

接收EGLD

#[payable("EGLD")]
#[endpoint]
fn deposit_egld(&self) {
    let payment = self.call_value().egld();
    // payment是BigUint(EGLD金额)
}

接收任何单一支付(EGLD或ESDT,统一)

#[payable]
#[endpoint]
fn deposit(&self) {
    let payment = self.call_value().single();
    // payment.token_identifier : TokenId
    // payment.token_nonce : u64
    // payment.amount : NonZeroBigUint(保证非零)
}

接收多个支付

#[payable]
#[endpoint]
fn multi_deposit(&self) {
    let payments = self.call_value().all();
    // 返回ManagedRef<PaymentVec> — 统一的EGLD + ESDT支付
    for payment in payments.iter() {
        // payment是Payment
    }
}

接收精确N个支付

#[payable]
#[endpoint]
fn dual_deposit(&self) {
    let [token_a, token_b] = self.call_value().array();
    // 正好2个支付,否则崩溃
}

可选单一支付

#[payable]
#[endpoint]
fn optional_deposit(&self) {
    let maybe_payment = self.call_value().single_optional();
    // 返回Option<Ref<Payment>> — 零个或一个支付
}

发送代币

// 发送EGLD
self.tx()
    .to(&recipient)
    .egld(&amount)
    .transfer();

// 发送Payment(需要NonZeroBigUint)
if let Some(amount_nz) = amount.into_non_zero() {
    self.tx()
        .to(&recipient)
        .payment(Payment::new(token_id, nonce, amount_nz))
        .transfer();
}

// 发送多个支付
self.tx()
    .to(&recipient)
    .payment(&payments)
    .transfer();

// 仅当非空时转账
self.tx()
    .to(&recipient)
    .egld(&amount)
    .transfer_if_not_empty();

注意: 自SDK v0.55.0起,EGLD和ESDT可以在同一多转账交易中一起发送。

事件

#[event("deposit")]
fn deposit_event(
    &self,
    #[indexed] caller: &ManagedAddress,
    #[indexed] token: &TokenId,
    amount: &BigUint,
);

// 发出事件
self.deposit_event(&caller, &token_id, &amount);

模块

将大型合约拆分为模块:

// 在src/storage.rs中
#[multiversx_sc::module]
pub trait StorageModule {
    #[storage_mapper("owner")]
    fn owner(&self) -> SingleValueMapper<ManagedAddress>;
}

// 在src/lib.rs中
mod storage;

#[multiversx_sc::contract]
pub trait MyContract: storage::StorageModule {
    #[init]
    fn init(&self) {
        self.owner().set(self.blockchain().get_caller());
    }
}

错误处理

// 使用require!
#[endpoint]
fn withdraw(&self, amount: BigUint) {
    let caller = self.blockchain().get_caller();
    require!(
        caller == self.owner().get(),
        "只有所有者可以提现"
    );
    require!(amount > 0, "金额必须为正");
}

// 使用sc_panic!
if condition_failed {
    sc_panic!("操作失败");
}

构建合约

构建工作区中的所有合约

sc-meta all build

构建单个合约

cd my-contract/meta
cargo run build

带选项构建

# 带锁定依赖项构建
sc-meta all build --locked

# 调试构建,带WAT输出
cd meta && cargo run build-dbg

构建输出

构建后,在output/中找到输出:

  • my-contract.wasm - 合约字节码
  • my-contract.abi.json - 合约ABI

测试

基于Rust的测试

tests/文件夹中创建测试:

use multiversx_sc_scenario::*;

fn world() -> ScenarioWorld {
    let mut blockchain = ScenarioWorld::new();
    blockchain.register_contract(
        "mxsc:output/my-contract.mxsc.json",
        my_contract::ContractBuilder,
    );
    blockchain
}

#[test]
fn test_deploy() {
    let mut world = world();
    world.run("scenarios/deploy.scen.json");
}

运行测试

# 运行所有测试
sc-meta test

# 运行特定测试
cargo test test_deploy

使用mxpy部署

部署新合约

mxpy contract deploy \
    --bytecode output/my-contract.wasm \
    --proxy https://devnet-api.multiversx.com \
    --chain D \
    --pem wallet.pem \
    --gas-limit 60000000 \
    --arguments 1000 \
    --send

升级现有合约

mxpy contract upgrade <contract-address> \
    --bytecode output/my-contract.wasm \
    --proxy https://devnet-api.multiversx.com \
    --chain D \
    --pem wallet.pem \
    --gas-limit 60000000 \
    --send

调用合约端点

mxpy contract call <contract-address> \
    --proxy https://devnet-api.multiversx.com \
    --chain D \
    --pem wallet.pem \
    --gas-limit 5000000 \
    --function "add" \
    --arguments 100 \
    --send

查询视图函数

mxpy contract query <contract-address> \
    --proxy https://devnet-api.multiversx.com \
    --function "getValue"

网络端点

网络 代理URL 链ID
Devnet https://devnet-api.multiversx.com D
Testnet https://testnet-api.multiversx.com T
Mainnet https://api.multiversx.com 1

高级模式

使用代理的跨合约调用

// 定义代理特质
#[multiversx_sc::proxy]
pub trait OtherContract {
    #[endpoint]
    fn some_endpoint(&self, value: BigUint);
}

// 使用代理
#[endpoint]
fn call_other(&self, other_address: ManagedAddress, value: BigUint) {
    self.tx()
        .to(&other_address)
        .typed(other_contract_proxy::OtherContractProxy)
        .some_endpoint(value)
        .sync_call();
}

从同步调用中检索反向转账

当被调用的合约发送代币回来时(例如,DEX交换返回),使用ReturnsBackTransfers捕获它们:

#[endpoint]
fn swap_and_store(&self, dex: ManagedAddress, token_in: TokenId, amount: NonZeroBigUint) {
    let back_transfers = self.tx()
        .to(&dex)
        .typed(dex_proxy::DexProxy)
        .swap(token_in, amount)
        .returns(ReturnsBackTransfersReset) // 重置避免陈旧积累
        .sync_call();

    let payments = back_transfers.into_payment_vec();
    for payment in &payments {
        self.received_tokens(&payment.token_identifier)
            .update(|bal| *bal += payment.amount.as_big_uint());
    }
}

关键规则:

  • 在一个端点中多次同步调用时,使用ReturnsBackTransfersReset(而不是ReturnsBackTransfers) — 否则反向转账会积累。
  • 当只需要EGLD金额时,使用ReturnsBackTransfersEGLD
  • 当期望正好一个ESDT代币返回时,使用ReturnsBackTransfersSingleESDT
  • 调用.into_payment_vec()以获取带v0.64 Payment项(TokenId + NonZeroBigUint)的PaymentVec

带回调的异步调用

#[endpoint]
fn async_call(&self, other_address: ManagedAddress) {
    self.tx()
        .to(&other_address)
        .typed(other_contract_proxy::OtherContractProxy)
        .some_endpoint()
        .callback(self.callbacks().my_callback())
        .async_call_and_exit();
}

#[callback]
fn my_callback(&self, #[call_result] result: ManagedAsyncCallResult<BigUint>) {
    match result {
        ManagedAsyncCallResult::Ok(value) => {
            // 处理成功
        }
        ManagedAsyncCallResult::Err(err) => {
            // 处理错误
        }
    }
}

带反向转账的承诺

对于返回代币的跨分片调用,使用承诺+回调:

#[endpoint]
fn cross_shard_swap(&self, dex: ManagedAddress, token: EgldOrEsdtTokenIdentifier, nonce: u64, amount: BigUint) {
    let gas_limit = self.blockchain().get_gas_left() - 20_000_000;
    self.tx()
        .to(&dex)
        .typed(dex_proxy::DexProxy)
        .swap(token, nonce, amount)
        .gas(gas_limit)
        .callback(self.callbacks().swap_callback())
        .gas_for_callback(10_000_000)
        .register_promise();
}

#[promises_callback]
fn swap_callback(&self) {
    let back_transfers = self.blockchain().get_back_transfers();
    let payments = back_transfers.into_payment_vec();
    for payment in &payments {
        self.received_tokens(&payment.token_identifier)
            .update(|bal| *bal += payment.amount.as_big_uint());
    }
}

代币发行(模块化方法)

处理代币发行的推荐方法是从框架(multiversx-sc-modules)导入和继承EsdtModule。此模块提供统一的issue_token方法,可用于在MultiversX上发行任何类型的代币(可替代、不可替代、半可替代、元、动态)。

#[multiversx_sc::contract]
pub trait MyContract: multiversx_sc_modules::esdt::EsdtModule {

    // 注意:只有可替代和元代币有小数位数
    // 示例:发行可替代代币
    #[payable("EGLD")]
    #[endpoint(issueFungible)]
    fn issue_fungible(
        &self,
        token_display_name: ManagedBuffer,
        token_ticker: ManagedBuffer,
        num_decimals: usize,
    ) {
        // 调用从EsdtModule继承的issue_token方法
        self.issue_token(
            token_display_name,
            token_ticker,
            EsdtTokenType::Fungible,
            OptionalValue::Some(num_decimals),
        );
    }

    // 示例:发行NFT
    #[payable("EGLD")]
    #[endpoint(issueNft)]
    fn issue_nft(
        &self,
        token_display_name: ManagedBuffer,
        token_ticker: ManagedBuffer,
    ) {
        self.issue_token(
            token_display_name,
            token_ticker,
            EsdtTokenType::NonFungible,
            OptionalValue::None,
        );
    }
}

代币铸造

类似地,EsdtModule提供mint方法,用于创建已由合约发行的代币的新单位。

#[multiversx_sc::contract]
pub trait MyContract: multiversx_sc_modules::esdt::EsdtModule {

    #[endpoint(mintTokens)]
    fn mint_tokens(&self, amount: BigUint) {
        // 使用从EsdtModule继承的mint方法铸造代币。
        // token_id由模块的存储管理。
        // 对于可替代代币,nonce为0。
        self.mint(0, &amount);
    }
}

代码示例

众筹合约模式

#![no_std]
use multiversx_sc::{chain_core::types::TimestampSeconds, imports::*};

#[multiversx_sc::contract]
pub trait Crowdfunding {
    #[init]
    fn init(&self, target: BigUint, deadline: TimestampSeconds, token_id: TokenId) {
        self.target().set(target);
        self.deadline().set(deadline);
        require!(token_id.is_valid(), "提供的代币无效");
        self.cf_token_identifier().set(token_id);
    }

    #[payable]
    #[endpoint]
    fn fund(&self) {
        require!(self.blockchain().get_block_timestamp_millis() < self.deadline().get(), "筹资期已结束");
        let payment = self.call_value().single();
        require!(payment.token_identifier == self.cf_token_identifier().get(), "代币错误");
        self.deposit(&self.blockchain().get_caller())
            .update(|deposit| *deposit += payment.amount.as_big_uint());
    }

    #[endpoint]
    fn claim(&self) {
        require!(self.blockchain().get_block_timestamp_millis() >= self.deadline().get(), "筹资期未结束");
        let caller = self.blockchain().get_caller();
        let token_id = self.cf_token_identifier().get();

        if self.get_current_funds() >= self.target().get() {
            require!(caller == self.blockchain().get_owner_address(), "非所有者");
            if let Some(bal) = self.get_current_funds().into_non_zero() {
                self.tx().to(&caller).payment(Payment::new(token_id, 0, bal)).transfer();
            }
        } else {
            let deposit = self.deposit(&caller).get();
            require!(deposit > 0, "无存款");
            self.deposit(&caller).clear();
            if let Some(dep) = deposit.into_non_zero() {
                self.tx().to(&caller).payment(Payment::new(token_id, 0, dep)).transfer();
            }
        }
    }

    #[view(getCurrentFunds)]
    fn get_current_funds(&self) -> BigUint {
        self.blockchain().get_sc_balance(&self.cf_token_identifier().get(), 0)
    }

    #[storage_mapper("target")]
    fn target(&self) -> SingleValueMapper<BigUint>;
    #[storage_mapper("deadline")]
    fn deadline(&self) -> SingleValueMapper<TimestampSeconds>;
    #[storage_mapper("tokenIdentifier")]
    fn cf_token_identifier(&self) -> SingleValueMapper<TokenId>;
    #[storage_mapper("deposit")]
    fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper<BigUint>;
}

关键知识

错误:不需要迭代时使用MapMapper

// 错误 - MapMapper昂贵(4*N + 1存储条目)
#[storage_mapper("balances")]
fn balances(&self) -> MapMapper<ManagedAddress, BigUint>;

正确:使用带地址键的SingleValueMapper

// 正确 - 当不需要迭代时高效
#[storage_mapper("balance")]
fn balance(&self, user: &ManagedAddress) -> SingleValueMapper<BigUint>;

错误:大型模块和函数

// 错误 - 所有内容在一个文件中
#[multiversx_sc::contract]
pub trait MyContract {
    // 500+行代码...
}

正确:拆分为模块

// 正确 - 组织化模块
mod storage;
mod logic;
mod events;

#[multiversx_sc::contract]
pub trait MyContract:
    storage::StorageModule +
    logic::LogicModule +
    events::EventsModule
{
    #[init]
    fn init(&self) { }
}

错误:重复错误消息

// 错误 - 重复字符串增加合约大小
require!(amount > 0, "金额必须为正");
require!(other_amount > 0, "金额必须为正");

正确:静态错误消息

// 正确 - 单一定义
const ERR_AMOUNT_POSITIVE: &str = "金额必须为正";

require!(amount > 0, ERR_AMOUNT_POSITIVE);
require!(other_amount > 0, ERR_AMOUNT_POSITIVE);

一起发送EGLD + ESDT(自v0.55.0起)

// 支持:EGLD和ESDT可以在多转账中组合
// 使用带TokenId的Payment进行统一处理
let mut payments = ManagedVec::new();
if let Some(egld_nz) = egld_amount.into_non_zero() {
    payments.push(Payment::new(TokenId::from("EGLD-000000"), 0, egld_nz));
}
if let Some(esdt_nz) = esdt_amount.into_non_zero() {
    payments.push(Payment::new(TokenId::from(token_id), 0, esdt_nz));
}
self.tx().to(&recipient).payment(&payments).transfer();

生产模式

对于生产级合约,这些附加技能涵盖高级模式:

Gas优化的存储缓存

使用基于Drop的缓存在入口批量存储读取和在出口批量写入。参见multiversx-cache-patterns技能。

// 一次性加载所有状态,在内存中修改,在作用域退出时提交
let mut cache = StorageCache::new(self);
cache.total_supply += &amount;
cache.fee_reserve += &fee;
// Drop自动写回两个值

跨合约存储读取

使用#[storage_mapper_from_address]直接读取另一个合约的存储,无需异步调用。参见multiversx-cross-contract-storage技能。

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

生产项目结构

对于多模块合约,遵循模块化架构模式。参见multiversx-project-architecture技能。

src/
  lib.rs          # 仅特质组合
  storage.rs      # 所有存储映射器
  cache/mod.rs    # 基于Drop的缓存
  views.rs        # #[view]端点
  config.rs       # 管理员配置端点
  events.rs       # 事件定义
  validation.rs   # 输入验证
  errors.rs       # 静态错误常量
  helpers.rs      # 业务逻辑

DeFi金融数学

对于借贷、质押或DEX合约,使用半向上取整和标准化精度级别。参见multiversx-defi-math技能。

附加专业化技能

  • multiversx-flash-loan-patterns — 带安全保护的闪电贷实现
  • multiversx-factory-manager — 部署和管理子合约
  • multiversx-vault-pattern — 用于多步操作的内存代币跟踪

文档链接

始终查阅官方文档:

验证清单

完成前验证:

  • [ ] 使用sc-meta new --template <template> --name <name>创建合约
  • [ ] lib.rs顶部有#![no_std]
  • [ ] 主特质上有#[multiversx_sc::contract]
  • [ ] 为部署定义了#[init]函数
  • [ ] 存储映射器使用适当类型(不需要迭代时避免MapMapper)
  • [ ] 支付端点有#[payable(...)]注解
  • [ ] 错误消息使用require!
  • [ ] 合约使用sc-meta all build成功构建
  • [ ] 输出文件存在:output/<name>.wasmoutput/<name>.abi.json
  • [ ] 测试通过sc-meta test(如果测试存在)
  • [ ] 部署前在devnet上测试