名称:multiversx-flash-loan-patterns 描述:用于MultiversX智能合约的原子借贷-执行-验证模式。适用于构建闪电贷、原子交换、临时授权,或任何在单笔交易内借出资产、执行回调并验证还款的操作。
MultiversX原子借贷-执行-验证模式
一种用于临时借出资产、执行外部回调并验证还款的操作模式——所有这些都在单笔交易内原子性地完成。
解决什么问题?
您想将代币借给合约,让其执行任意逻辑,并在交易完成前保证还款(加手续费)。如果还款失败,整个交易将回滚。
何时使用
| 场景 | 是否使用此模式? |
|---|---|
| 闪电贷 | 是——典型用例 |
| 带验证的原子交换 | 是——发送代币,验证对方返还 |
| 临时授权(执行后返回) | 是——借出代币用于计算,验证返还 |
| 跨分片操作 | 否——原子性要求在同一分片 |
| 简单转账 | 否——杀鸡用牛刀 |
安全检查清单
- 重入防护——防止嵌套操作
- 分片验证——调用者必须在同一分片(原子性要求)
- 端点验证——回调不能是内置函数
- 还款验证——在回调后检查合约余额
- 防护清理——始终清除重入标志
核心流程:防护 → 发送 → 执行 → 验证 → 清除
#[endpoint(atomicOperation)]
fn atomic_operation(
&self,
asset: TokenId,
amount: BigUint,
target_contract: ManagedAddress,
callback_endpoint: ManagedBuffer,
) {
// 1. 重入防护
self.require_not_ongoing();
// 2. 分片验证(原子性要求同一分片)
self.require_same_shard(&target_contract);
// 3. 端点验证
self.require_valid_endpoint(&callback_endpoint);
// 4. 计算预期还款
let fee = &amount * self.fee_bps().get() / 10_000u64;
let balance_before = self.blockchain().get_sc_balance(&asset.clone().into(), 0);
// 5. 设置防护
self.operation_ongoing().set(true);
// 6. 发送代币并调用目标
self.tx()
.to(&target_contract)
.raw_call(callback_endpoint)
.single_esdt(&asset, 0, &amount)
.sync_call();
// 7. 验证还款
let balance_after = self.blockchain().get_sc_balance(&asset.into(), 0);
require!(
balance_after >= balance_before + &fee,
"还款不足"
);
// 8. 清除防护
self.operation_ongoing().set(false);
}
重入防护
#[storage_mapper("operationOngoing")]
fn operation_ongoing(&self) -> SingleValueMapper<bool>;
fn require_not_ongoing(&self) {
require!(
!self.operation_ongoing().get(),
"操作已在进行中"
);
}
为何需要:没有此防护,恶意回调可能重入操作端点,创建嵌套操作以绕过还款检查。
分片验证
fn require_same_shard(&self, target_address: &ManagedAddress) {
let target_shard = self.blockchain().get_shard_of_address(target_address);
let contract_shard = self.blockchain().get_shard_of_address(
&self.blockchain().get_sc_address()
);
require!(
target_shard == contract_shard,
"目标必须在同一分片"
);
}
为何需要:跨分片调用在不同区块/轮次中执行,破坏原子性。回调将在单独交易中运行,允许在发送和验证之间进行操纵。
端点验证
fn require_valid_endpoint(&self, endpoint: &ManagedBuffer<Self::Api>) {
require!(
!endpoint.is_empty() && !self.blockchain().is_builtin_function(endpoint),
"无效的回调端点"
);
}
为何需要:内置函数(代币转账、ESDT操作)可能在不执行预期回调的情况下重定向代币,绕过还款逻辑。
重入防护示例
不好
// 不要:无重入防护——恶意回调重入并再次借贷
#[endpoint(flashLoan)]
fn flash_loan(&self, asset: TokenId, amount: BigUint, target: ManagedAddress) {
let balance_before = self.blockchain().get_sc_balance(&asset.clone().into(), 0);
self.tx().to(&target).raw_call("execute").single_esdt(&asset, 0, &amount).sync_call();
let balance_after = self.blockchain().get_sc_balance(&asset.into(), 0);
require!(balance_after >= balance_before, "未还款"); // 被重入绕过!
}
好
// 做:在发送前设置重入防护,在验证后清除
#[endpoint(flashLoan)]
fn flash_loan(&self, asset: TokenId, amount: BigUint, target: ManagedAddress) {
self.require_not_ongoing(); // 阻止嵌套调用
self.operation_ongoing().set(true);
let balance_before = self.blockchain().get_sc_balance(&asset.clone().into(), 0);
self.tx().to(&target).raw_call("execute").single_esdt(&asset, 0, &amount).sync_call();
let balance_after = self.blockchain().get_sc_balance(&asset.into(), 0);
require!(balance_after >= balance_before, "未还款");
self.operation_ongoing().set(false);
}
反模式
1. 忘记清除防护
// 错误——如果验证失败,防护将永远保持设置
self.operation_ongoing().set(true);
self.tx().to(&target).raw_call(endpoint).sync_call();
// 如果这个require失败,防护永远不会被清除!
require!(balance_after >= expected, "未还款");
self.operation_ongoing().set(false);
注意:在MultiversX中,如果require!失败,交易回滚,所以防护也会回滚。但在基于回调的流程中,请注意您所处的执行上下文。
2. 检查余额不正确
// 错误——检查特定存储值而非实际合约余额
let repaid = self.deposits(&asset).get();
// 正确——检查实际链上余额
let balance_after = self.blockchain().get_sc_balance(&asset.into(), 0);
3. 无分片验证
// 错误——跨分片调用会悄悄破坏原子性
fn flash_loan(&self, borrower: ManagedAddress, /* ... */) {
// 如果借款人在不同分片,sync_call变成异步
self.tx().to(&borrower).raw_call(endpoint).sync_call();
}
模板
#[multiversx_sc::module]
pub trait AtomicOperationModule {
#[storage_mapper("operationOngoing")]
fn operation_ongoing(&self) -> SingleValueMapper<bool>;
#[storage_mapper("feeBps")]
fn fee_bps(&self) -> SingleValueMapper<u64>;
fn require_not_ongoing(&self) {
require!(!self.operation_ongoing().get(), "操作已在进行中");
}
fn require_same_shard(&self, target: &ManagedAddress) {
let target_shard = self.blockchain().get_shard_of_address(target);
let self_shard = self.blockchain().get_shard_of_address(&self.blockchain().get_sc_address());
require!(target_shard == self_shard, "必须在同一分片");
}
fn require_valid_endpoint(&self, endpoint: &ManagedBuffer<Self::Api>) {
require!(
!endpoint.is_empty() && !self.blockchain().is_builtin_function(endpoint),
"无效的端点"
);
}
}